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ị.
Giao dịch đơn giản hóa
Một giao dịch giống 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ì để các biểu mẫu được xử lý.
Đ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:
- Địa chỉ của chương trình cần gọi
- Các tài khoản mà chỉ thị đọc từ hoặc ghi vào
- 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 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ó thể 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 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.
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);
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
.
- 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);
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.
Việc gọi instruction của một chương trình yêu cầu ba thông tin quan trọng:
- Program ID: Chương trình chứa logic thực thi cho instruction
- Accounts: Danh sách các tài khoản mà instruction cần
- Instruction Data: Mảng byte chỉ định instruction cần gọi trên chương trình và bất kỳ đối số nào mà instruction yêu cầu
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 Instruction
AccountMeta
Mỗi tài khoản được yêu cầu bởi một instruction phải được cung cấp dưới dạng AccountMeta chứa:
pubkey
: Địa chỉ của tài khoảnis_signer
: Liệu tài khoản có phải ký giao dịch hay khôngis_writable
: Liệu instruction có sửa đổi dữ liệu của tài khoản hay không
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
Bằng cách chỉ định trước các tài khoản mà một instruction đọ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.
Cấu trúc Instruction mẫu
Chạy các ví dụ dưới đây để xem cấu trúc của một instruction chuyển 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));
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 instruction của 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 lệnh.
- Tài khoản: Danh sách các tài khoản mà lệnh yêu cầu. Đối với mỗi tài khoản, lệnh phải chỉ định địa chỉ của nó, liệu nó có phải ký giao dịch hay không, và liệu nó có được ghi vào hay không.
- Dữ liệu: Một bộ đệm byte cho chương trình biết lệnh nào cần thực thi và bao gồm bất kỳ đối số nào mà lệnh 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
Một giao dịch Solana transaction bao gồm:
- Chữ ký: Một mảng các chữ ký được bao gồm trong giao dịch.
- Thông điệp: Danh sách các lệnh được xử lý một cách nguyên tử.
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
Cấu trúc của một thông điệp giao dịch bao gồm:
- Tiêu đề thông điệp: Chỉ đị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 yêu cầu bởi các lệnh trong giao dịch.
- Blockhash gần đây: Hoạt động như một dấu thời gian cho giao dịch.
- Lệnh: Một mảng các lệnh sẽ được thực thi.
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>,}
Thông điệp giao dịch
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 đề phân mảnh).
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 lệnh
Định dạng giao dịch
Tiêu đề thông điệp
Tiêu đề thông điệp sử dụng ba byte để xác định đặc quyền tài khoản.
- Chữ ký bắt buộc
- Số lượng tài khoản đã ký chỉ đọc
- Số lượng tài khoản chưa ký chỉ đọc
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 đề thông điệp
Định dạng mảng nhỏ gọn
Một mảng nhỏ gọn trong thông điệp giao dịch là một mảng được tuần tự hóa theo định dạng sau:
- Độ dài mảng (được mã hóa dưới dạng compact-u16)
- Các phần tử mảng được liệt kê lần lượt
Đị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ản và Chỉ thị trong thông điệp giao dịch.
Mảng địa chỉ tài khoản
Một thông điệp giao dịch chứa một mảng địa chỉ tài khoản cần thiết cho các chỉ thị của nó. Mảng bắt đầu với một số compact-u16 cho biết có bao nhiêu địa chỉ trong đó. Các địa chỉ sau đó được sắp xếp theo đặc quyền của chúng, được xác định bởi tiêu đề thông điệp.
- Tài khoản có thể ghi và là người ký
- Tài khoản chỉ đọc và là người ký
- Tài khoản có thể ghi và không phải người ký
- Tài khoản chỉ đọc và không phải người ký
Mả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:
- Hoạt động như một dấu thời gian
- 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 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. Đây là một ví dụ
trên Solana Playground.
Mảng chỉ thị
Một thông điệp giao dịch chứa một mảng chỉ thị trong kiểu CompiledInstruction. Các chỉ thị được chuyển đổi sang kiểu 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 độ dài compact-u16 theo sau là dữ liệu chỉ thị. Mỗi chỉ thị chứa:
- Chỉ mục ID chương trình: Một chỉ mục u8 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ý lệnh.
- Chỉ mục tài khoản: Một mảng các chỉ mục u8 trỏ đến các địa chỉ tài khoản cần thiết cho lệnh này.
- Dữ liệu lệnh: Một mảng byte xác định lệnh nào sẽ được gọi trên chương trình và bất kỳ dữ liệu bổ sung nào mà lệnh yêu cầu (ví dụ: các đối số hàm).
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 lệnh
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 lệnh chuyển SOL đơn lẻ.
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));
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}}]}
Khi bạn lấy một giao dịch bằng chữ ký của nó sau khi gửi đến mạng, bạn sẽ nhận được phản hồi với cấu trúc sau.
Trường message
chứa các trường sau:
-
header
: Xác định quyền đọc/ghi và ký cho các địa chỉ trong mảngaccountKeys
-
accountKeys
: Mảng tất cả các địa chỉ tài khoản được sử dụng trong các lệnh của giao dịch -
recentBlockhash
: Blockhash được sử dụng để đánh dấu thời gian cho giao dịch -
instructions
: Mảng các lệnh để thực thi. Mỗiaccount
vàprogramIdIndex
trong một lệnh tham chiếu đến mảngaccountKeys
bằng chỉ mục. -
signatures
: Mảng bao gồm chữ ký cho tất cả các tài khoản được yêu cầu làm người ký bởi các lệnh trong giao dịch. Một chữ ký được tạo bằng cách ký thông điệp giao dịch sử dụng khóa riêng tương ứng cho một tài khoản.
{"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?