Giao dịch và Chỉ thị

Trên Solana, người dùng gửi giao dịch để tương tác với mạng lưới. Giao dịch chứa một hoặc nhiều chỉ thị xác định các thao tác cần xử lý. Logic thực thi cho các chỉ thị được lưu trữ trên chương trình được triển khai trên mạng Solana, trong đó mỗi chương trình định nghĩa tập hợp chỉ thị riêng của mình.

Dưới đây là những chi tiết quan trọng về xử lý giao dịch Solana:

  • Nếu một giao dịch bao gồm nhiều chỉ thị, các chỉ thị sẽ được thực thi theo thứ tự được thêm vào giao dịch.
  • Giao dịch là "nguyên tử" - tất cả các chỉ thị phải được xử lý thành công, nếu không toàn bộ giao dịch sẽ thất bại và không có thay đổi nào xảy ra.

Một giao dịch về cơ bản là một yêu cầu xử lý một hoặc nhiều chỉ thị. Bạn có thể xem giao dịch như một phong bì chứa các biểu mẫu. Mỗi biểu mẫu là một chỉ thị cho mạng lưới biết phải làm gì. Gửi giao dịch giống như gửi phong bì qua đường bưu điện để các biểu mẫu được xử lý.

Giao dịch đơn giản hóaGiao dịch đơn giản hóa

Điểm chính

  • Giao dịch Solana bao gồm các chỉ thị gọi các chương trình trên mạng lưới.
  • Giao dịch là nguyên tử - nếu bất kỳ chỉ thị nào thất bại, toàn bộ giao dịch sẽ thất bại và không có thay đổi nào xảy ra.
  • Các chỉ thị trong một giao dịch được thực thi theo thứ tự tuần tự.
  • Giới hạn kích thước giao dịch là 1232 byte.
  • Mỗi chỉ thị yêu cầu ba thông tin:
    1. Địa chỉ của chương trình cần gọi
    2. Các tài khoản mà chỉ thị đọc từ hoặc ghi vào
    3. Bất kỳ dữ liệu bổ sung nào cần thiết cho chỉ thị (ví dụ: đối số hàm)

Ví dụ chuyển SOL

Sơ đồ dưới đây biểu diễn một giao dịch với một chỉ thị duy nhất để chuyển SOL từ người gửi đến người nhận.

Trên Solana, "ví" là các tài khoản thuộc sở hữu của System Program. Chỉ chương trình sở hữu mới có thể thay đổi dữ liệu của tài khoản, vì vậy việc chuyển SOL yêu cầu gửi một giao dịch để gọi System Program.

Chuyển SOLChuyển SOL

Tài khoản người gửi phải ký (is_signer) giao dịch để cho phép System Program trừ số dư lamport của nó. Tài khoản người gửi và người nhận phải có khả năng ghi (is_writable) vì số dư lamport của họ thay đổi.

Sau khi gửi giao dịch, System Program xử lý chỉ thị chuyển tiền. System Program sau đó cập nhật số dư lamport của cả tài khoản người gửi và người nhận.

Quy trình chuyển SOLQuy trình chuyển SOL

Các ví dụ dưới đây cho thấy cách gửi một giao dịch chuyển SOL từ một tài khoản sang tài khoản khác. Xem mã nguồn chỉ thị chuyển tiền của System Program tại đây.

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.

Các thư viện client thường trừu tượng hóa các chi tiết để xây dựng chỉ thị chương trình. Nếu không có sẵn thư viện, bạn có thể tự xây dựng chỉ thị. Điều này yêu cầu bạn phải biết chi tiết triển khai của chỉ thị.

Các ví dụ dưới đây cho thấy cách tự xây dựng chỉ thị chuyển tiền. Tab Expanded Instruction tương đương về mặt chức năng với tab Instruction.

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

Trong các phần dưới đây, chúng ta sẽ tìm hiểu chi tiết về các giao dịch và hướng dẫn.

Instructions

Một instruction trên chương trình Solana có thể được hiểu như một hàm công khai mà bất kỳ ai sử dụng mạng Solana đều có thể gọi.

Bạn có thể xem chương trình Solana như một máy chủ web được lưu trữ trên mạng Solana, mà mỗi instruction giống như một điểm cuối API công khai mà người dùng có thể gọi để thực hiện các hành động cụ thể. Việc gọi một instruction tương tự như gửi một yêu cầu POST đến một điểm cuối API, cho phép người dùng thực thi logic nghiệp vụ của chương trình.

Để gọi instruction của một chương trình trên Solana, bạn cần tạo một Instruction với ba thông tin:

  • Program ID: Địa chỉ của chương trình chứa logic nghiệp vụ cho instruction đang được gọi.
  • Accounts: Danh sách tất cả các tài khoản mà instruction đọc từ hoặc ghi vào.
  • Instruction Data: Một mảng byte chỉ định instruction nào sẽ được gọi trên chương trình và bất kỳ đối số nào mà instruction yêu cầu.
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>,
}

Transaction InstructionTransaction Instruction

AccountMeta

Khi tạo một Instruction, bạn phải cung cấp mỗi tài khoản cần thiết dưới dạng AccountMeta. AccountMeta chỉ định những điều sau:

  • pubkey: Địa chỉ của tài khoản
  • is_signer: Liệu tài khoản có phải ký giao dịch hay không
  • is_writable: Liệu instruction có sửa đổi dữ liệu của tài khoản hay không
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,
}

Bằng cách chỉ định trước các tài khoản mà một chỉ thị đọc hoặc ghi, các giao dịch không sửa đổi cùng một tài khoản có thể thực thi song song.

Để biết chỉ thị yêu cầu những tài khoản nào, bao gồm tài khoản nào phải có khả năng ghi, chỉ đọc, hoặc ký giao dịch, bạn phải tham khảo việc triển khai chỉ thị được định nghĩa bởi chương trình.

Trong thực tế, bạn thường không phải tạo một Instruction thủ công. Hầu hết các nhà phát triển chương trình cung cấp thư viện khách với các hàm trợ giúp tạo chỉ thị cho bạn.

AccountMetaAccountMeta

Cấu trúc chỉ thị ví dụ

Chạy các ví dụ dưới đây để xem cấu trúc của chỉ thị chuyển 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.

Các ví dụ sau đây hiển thị đầu ra từ các đoạn mã trước đó. Định dạng chính xác khác nhau tùy thuộc vào SDK, nhưng mọi chỉ thị Solana đều yêu cầu các thông tin sau:

  • ID chương trình: Địa chỉ của chương trình sẽ thực thi chỉ thị.
  • Tài khoản: Danh sách các tài khoản mà chỉ thị yêu cầu. Đối với mỗi tài khoản, chỉ thị phải chỉ định địa chỉ, liệu nó có phải ký giao dịch hay không, và liệu nó sẽ được ghi vào hay không.
  • Dữ liệu: Một bộ đệm byte cho chương trình biết chỉ thị nào cần thực thi và bao gồm bất kỳ đối số nào mà chỉ thị yêu cầu.
{
"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
}
}

Giao dịch

Sau khi bạn đã tạo các chỉ thị muốn gọi, bước tiếp theo là tạo một Transaction và thêm các chỉ thị vào giao dịch. Một giao dịch Solana bao gồm:

  1. Chữ ký: Một mảng các chữ ký từ tất cả các tài khoản cần thiết làm người ký cho các chỉ thị trong giao dịch. Chữ ký được tạo ra bằng cách ký giao dịch Message với khóa riêng tư của tài khoản.
  2. Thông điệp: Thông điệp giao dịch bao gồm danh sách các chỉ thị được xử lý một cách nguyên tử.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

Định dạng giao dịchĐịnh dạng giao dịch

Cấu trúc của một thông điệp giao dịch bao gồm:

  • Tiêu đề thông điệp: Xác định số lượng người ký và tài khoản chỉ đọc.
  • Địa chỉ tài khoản: Một mảng các địa chỉ tài khoản cần thiết cho các chỉ thị trong giao dịch.
  • Blockhash gần đây: Hoạt động như một dấu thời gian cho giao dịch.
  • Chỉ thị: Một mảng các chỉ thị sẽ được thực thi.
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>,
}

Kích thước giao dịch

Các giao dịch Solana có giới hạn kích thước là 1232 byte. Giới hạn này xuất phát từ kích thước Maximum Transmission Unit (MTU) của IPv6 là 1280 byte, trừ đi 48 byte cho các tiêu đề mạng (40 byte IPv6 + 8 byte tiêu đề).

Tổng kích thước của một giao dịch (chữ ký và thông điệp) phải nằm dưới giới hạn này và bao gồm:

  • Chữ ký: 64 byte mỗi chữ ký
  • Thông điệp: Tiêu đề (3 byte), khóa tài khoản (32 byte mỗi khóa), blockhash gần đây (32 byte), và các chỉ thị

Định dạng giao dịchĐịnh dạng giao dịch

Tiêu đề thông điệp

Tiêu đề thông điệp xác định quyền hạn cho tài khoản trong giao dịch. Nó hoạt động kết hợp với địa chỉ tài khoản được sắp xếp nghiêm ngặt để xác định tài khoản nào là người ký và tài khoản nào có thể ghi.

  1. Số lượng chữ ký cần thiết cho tất cả các chỉ thị trong giao dịch.
  2. Số lượng tài khoản đã ký chỉ đọc.
  3. Số lượng tài khoản không ký chỉ đọc.
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,
}

Tiêu đề tin nhắnTiêu đề tin nhắn

Định dạng mảng nhỏ gọn

Một mảng nhỏ gọn trong tin nhắn giao dịch là một mảng được tuần tự hóa theo định dạng sau:

  1. Độ dài mảng (được mã hóa dưới dạng compact-u16)
  2. Các mục trong mảng được liệt kê lần lượt

Định dạng mảng nhỏ gọnĐịnh dạng mảng nhỏ gọn

Định dạng này được sử dụng để mã hóa độ dài của các mảng Địa chỉ tài khoảnHướng dẫn trong tin nhắn giao dịch.

Mảng địa chỉ tài khoản

Một tin nhắn giao dịch chứa một danh sách duy nhất gồm tất cả địa chỉ tài khoản cần thiết cho các hướng dẫn của nó. Mảng bắt đầu với một số compact-u16 cho biết số lượng địa chỉ mà nó chứa.

Để tiết kiệm không gian, giao dịch không lưu trữ quyền cho từng tài khoản riêng lẻ. Thay vào đó, nó dựa vào sự kết hợp của MessageHeader và thứ tự nghiêm ngặt của các địa chỉ tài khoản để xác định quyền.

Các địa chỉ luôn được sắp xếp theo cách sau:

  1. Tài khoản có thể ghi và là người ký
  2. Tài khoản chỉ đọc và là người ký
  3. Tài khoản có thể ghi và không phải người ký
  4. Tài khoản chỉ đọc và không phải người ký

MessageHeader cung cấp các giá trị được sử dụng để xác định số lượng tài khoản cho mỗi nhóm quyền.

Mảng nhỏ gọn của địa chỉ tài khoảnMảng nhỏ gọn của địa chỉ tài khoản

Blockhash gần đây

Mỗi giao dịch yêu cầu một blockhash gần đây phục vụ hai mục đích:

  1. Hoạt động như dấu thời gian cho thời điểm giao dịch được tạo
  2. Ngăn chặn các giao dịch trùng lặp

Một blockhash hết hạn sau 150 khối (khoảng 1 phút giả định thời gian khối là 400ms), sau đó giao dịch được coi là hết hạn và không thể được xử lý.

Bạn có thể sử dụng phương thức RPC getLatestBlockhash để lấy blockhash hiện tại và chiều cao khối cuối cùng mà blockhash sẽ còn hiệu lực.

Mảng các Chỉ thị

Một thông điệp giao dịch chứa một mảng các chỉ thị ở dạng CompiledInstruction. Các chỉ thị được chuyển đổi sang dạng này khi được thêm vào giao dịch.

Giống như mảng địa chỉ tài khoản trong thông điệp, nó bắt đầu với một độ dài compact-u16 theo sau là dữ liệu chỉ thị. Mỗi chỉ thị bao gồm:

  1. Chỉ mục ID Chương trình: Một chỉ mục trỏ đến địa chỉ của chương trình trong mảng địa chỉ tài khoản. Điều này xác định chương trình sẽ xử lý chỉ thị.
  2. Chỉ mục Tài khoản: Một mảng các chỉ mục trỏ đến các địa chỉ tài khoản cần thiết cho chỉ thị này.
  3. Instruction Data: Một mảng byte xác định chỉ thị nào sẽ được gọi trên chương trình và bất kỳ dữ liệu bổ sung nào cần thiết cho chỉ thị (ví dụ: các đối số hàm).
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>,
}

Mảng nhỏ gọn của các Chỉ thịMảng nhỏ gọn của các Chỉ thị

Ví dụ về Cấu trúc Giao dịch

Chạy các ví dụ dưới đây để xem cấu trúc của một giao dịch với một chỉ thị chuyển SOL đơn giản.

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.

Các ví dụ sau đây hiển thị đầu ra thông điệp giao dịch từ các đoạn mã trước đó. Định dạng chính xác khác nhau tùy thuộc vào SDK, nhưng bao gồm cùng thông tin.

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

Sau khi gửi giao dịch, bạn có thể truy xuất chi tiết của nó bằng phương thức RPC getTransaction. Phản hồi sẽ có cấu trúc tương tự như đoạn mã dưới đây. Ngoài ra, bạn có thể kiểm tra giao dịch bằng Solana Explorer.

"Chữ ký giao dịch" xác định duy nhất một giao dịch trên Solana. Bạn sử dụng chữ ký này để tra cứu chi tiết giao dịch trên mạng. Chữ ký giao dịch đơn giản là chữ ký đầu tiên trên giao dịch. Lưu ý rằng chữ ký đầu tiên cũng là chữ ký của người trả phí giao dịch.

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?