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ó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:
- Đị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ó 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 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 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
.
const transferAmount = 0.01; // 0.01 SOLconst 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.
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
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
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.
AccountMeta
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 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 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:
- 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. - 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ử.
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: 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.
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
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.
- Số lượng chữ ký cần thiết cho tất cả các chỉ thị trong giao dịch.
- Số lượng tài khoản đã ký chỉ đọc.
- Số lượng tài khoản không 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 đề 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:
- Độ dài mảng (được mã hóa dưới dạng compact-u16)
- 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 này được sử dụng để mã hóa độ dài của các mảng Địa chỉ tài khoản và Hướ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:
- 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ý
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ả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ư dấu thời gian cho thời điểm giao dịch được tạo
- 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:
- 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ị.
- 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.
- 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).
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ị
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 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}}]}
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.
{"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?