ملخص
تحتوي المعاملة على توقيعات + رسالة. تحتوي الرسالة على رأسية، وعناوين الحسابات، وتجزئة كتلة حديثة، وتعليمات مجمعة. الحد الأقصى للحجم المتسلسل: 1,232 بايت.
تحتوي
Transaction
على حقلين رئيسيين:
signatures: مصفوفة من التوقيعاتmessage: معلومات المعاملة، بما في ذلك قائمة التعليمات المراد معالجتها
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
رسم تخطيطي يوضح الجزأين من المعاملة
يجب ألا يتجاوز الحجم الإجمالي المتسلسل للمعاملة
PACKET_DATA_SIZE
(1,232 بايت). يساوي هذا الحد 1,280 بايت (الحد الأدنى لـ IPv6 MTU) مطروحًا منه 48
بايت لرؤوس الشبكة (40 بايت IPv6 + 8 بايت رأسية الجزء). تشمل الـ 1,232 بايت كلاً
من مصفوفة signatures وبنية message.
رسم تخطيطي يوضح تنسيق المعاملة وحدود الحجم
التوقيعات
حقل signatures هو مصفوفة مشفرة بشكل مضغوط من قيم
Signature.
كل Signature هو توقيع Ed25519 بحجم 64 بايت لـ Message المتسلسل، موقع
بالمفتاح الخاص لحساب الموقع. يلزم توقيع واحد لكل حساب موقع
مشار إليه بواسطة تعليمات المعاملة.
يتم إنتاج كل توقيع بواسطة مفتاح خاص. يُعدّ مكان وجود ذلك المفتاح — سواء أكان keypair محليًا، أو وحدة أمان تشفير سحابية (HSM/KMS)، أو خدمة محفظة مُدارة — قرارًا تصميميًا على مستوى الإنتاج. راجع التوقيع في بيئة الإنتاج.
ينتمي التوقيع الأول في المصفوفة إلى دافع الرسوم، وهو الحساب الذي يدفع الرسوم الأساسية للمعاملة ورسوم الأولوية. يعمل هذا التوقيع الأول أيضًا بوصفه معرّف المعاملة، المستخدَم للبحث عن المعاملة على الشبكة. يُشار إلى معرّف المعاملة بشكل شائع باسم توقيع المعاملة.
متطلبات دافع الرسوم:
- يجب أن يكون الحساب الأول في الرسالة (الفهرس 0) وأن يكون موقِّعًا.
- يجب أن يكون حسابًا مملوكًا لـ System Program أو حساب nonce (يتم التحقق منه
بواسطة
validate_fee_payer). - يجب أن يحتوي على ما يكفي من lamport لتغطية
rent_exempt_minimum + total_fee؛ وإلا فستفشل المعاملة معInsufficientFundsForFee.
الرسالة
حقل message هو
Message
بنية تحتوي على حمولة المعاملة:
header: ترويسة الرسالةaccount_keys: مصفوفة من عناوين الحسابات المطلوبة بواسطة تعليمات المعاملةrecent_blockhash: blockhash يعمل كطابع زمني للمعاملةinstructions: مصفوفة من التعليمات
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>,}
الترويسة
حقل header هو
MessageHeader
بنية تحتوي على ثلاثة حقول u8 تقسّم مصفوفة account_keys إلى مجموعات صلاحيات:
num_required_signatures: العدد الإجمالي للتوقيعات المطلوبة للمعاملة.num_readonly_signed_accounts: عدد الحسابات الموقَّعة للقراءة فقط.num_readonly_unsigned_accounts: عدد الحسابات غير الموقَّعة للقراءة فقط.
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,}
بادئات رسائل الإصدار القديم والرسائل ذات الإصدار
في رسالة معاملة قديمة، يكون البايت الأول من الرسالة هو
num_required_signatures، يليه البايتان الآخران MessageHeader. أما في رسالة
معاملة ذات إصدار، فإن البايت الأول يكون بادئة إصدار بدلاً من ذلك؛ ويبدأ
MessageHeader المكوّن من ثلاثة بايتات مباشرةً بعد تلك البادئة. راجع
المعاملات ذات الإصدار
للاطلاع على التخطيط الكامل لرسالة v0.
مخطط يوضح الأجزاء الثلاثة لترويسة الرسالة
عناوين الحسابات
الحقل
account_keys
هو مصفوفة مشفرة بصيغة مضغوطة من المفاتيح العامة. يُحدد كل إدخال حساباً يستخدمه
تعليمة واحدة على الأقل من تعليمات المعاملة. يجب أن تشتمل المصفوفة على جميع
الحسابات وأن تتبع هذا الترتيب الصارم:
- موقّع + قابل للكتابة
- موقّع + للقراءة فقط
- غير موقّع + قابل للكتابة
- غير موقّع + للقراءة فقط
يتيح هذا الترتيب الصارم دمج مصفوفة account_keys مع العدادات الثلاثة في
header الخاص بالرسالة لتحديد الصلاحيات لكل حساب دون الحاجة إلى
تخزين أعلام بيانات وصفية لكل حساب. تُقسّم عدادات الترويسة المصفوفة إلى مجموعات
الأذونات الأربع المذكورة أعلاه.
مخطط يوضح ترتيب مصفوفة عناوين الحسابات
الـ blockhash الأخير
الحقل recent_blockhash هو تجزئة بحجم 32 بايت تخدم غرضين:
- الطابع الزمني: يُثبت أن المعاملة تمت مؤخراً.
- منع التكرار: يمنع معالجة المعاملة ذاتها مرتين.
تنتهي صلاحية الـ blockhash بعد 150 slot. إذا لم يعد الـ blockhash صالحاً عند
وصول المعاملة، فسيتم رفضها بالخطأ BlockhashNotFound، ما لم تكن
معاملة nonce دائمة صالحة.
تتيح لك طريقة RPC getLatestBlockhash
الحصول على الـ blockhash الحالي وآخر ارتفاع للكتلة الذي سيكون فيه الـ
blockhash صالحاً.
التعليمات
حقل
instructions
هو مصفوفة مشفرة بصيغة مضغوطة من هياكل
CompiledInstruction.
كل CompiledInstruction يُشير إلى الحسابات عبر الفهرس في مصفوفة
account_keys بدلاً من استخدام المفتاح العام الكامل. ويحتوي على:
program_id_index: فهرس فيaccount_keysيُحدد البرنامج المراد استدعاؤه.accounts: مصفوفة من المؤشرات فيaccount_keysتُحدد الحسابات التي سيتم تمريرها إلى البرنامج.data: مصفوفة بايت تحتوي على 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>,}
مصفوفة مضغوطة من التعليمات
تنسيق المعاملة الثنائي
تُسلسَل المعاملات باستخدام مخطط ترميز مضغوط. جميع المصفوفات ذات الطول المتغير (التواقيع، ومفاتيح الحسابات، والتعليمات) مسبوقة بترميز طول compact-u16. يستخدم هذا التنسيق بايتًا واحدًا للقيم من 0 إلى 127، وبايتين أو ثلاثة بايتات للقيم الأكبر.
تخطيط المعاملة القديمة (على الشبكة):
| الحقل | الحجم | الوصف |
|---|---|---|
num_signatures | 1-3 بايت (compact-u16) | عدد التواقيع |
signatures | num_signatures × 64 بايت | تواقيع Ed25519 |
num_required_signatures | 1 بايت | الحقل 1 في MessageHeader |
num_readonly_signed | 1 بايت | الحقل 2 في MessageHeader |
num_readonly_unsigned | 1 بايت | الحقل 3 في MessageHeader |
num_account_keys | 1-3 بايت (compact-u16) | عدد مفاتيح الحسابات الثابتة |
account_keys | num_account_keys × 32 بايت | pubkey |
recent_blockhash | 32 بايت | تجزئة الكتلة |
num_instructions | 1-3 بايت (compact-u16) | عدد التعليمات |
instructions | متغير | مصفوفة من التعليمات المُجمَّعة |
تُسلسَل كل تعليمة مُجمَّعة على النحو التالي:
| الحقل | الحجم | الوصف |
|---|---|---|
program_id_index | 1 بايت | فهرس في مفاتيح الحسابات |
num_accounts | 1-3 بايت (compact-u16) | عدد مؤشرات الحسابات |
account_indices | num_accounts × 1 بايت | مؤشرات مفاتيح الحسابات |
data_len | 1-3 بايت (compact-u16) | طول instruction data |
data | data_len بايت | instruction data غير مُفسَّر |
حساب الحجم
بافتراض أن PACKET_DATA_SIZE = 1,232 بايت، يمكن حساب المساحة المتاحة على
النحو التالي:
Total = 1232 bytes- compact-u16(num_sigs) # 1 byte- num_sigs * 64 # signature bytes- 3 # message header- compact-u16(num_keys) # 1 byte- num_keys * 32 # account key bytes- 32 # recent blockhash- compact-u16(num_ixs) # 1 byte- sum(instruction_sizes) # per-instruction overhead + data
مثال: معاملة تحويل SOL
يوضح الرسم البياني أدناه كيف تعمل المعاملات والتعليمات معاً للسماح للمستخدمين بالتفاعل مع الشبكة. في هذا المثال، يتم تحويل SOL من حساب إلى آخر.
تشير البيانات الوصفية لحساب المُرسِل إلى أنه يجب أن يوقّع على المعاملة. يتيح ذلك لـ System Program خصم lamport. يجب أن يكون كلٌّ من حساب المُرسِل والمستلم قابلَين للكتابة، حتى يتسنى تغيير رصيد lamport الخاص بهما. لتنفيذ هذه التعليمة، يُرسل محفظة المُرسِل المعاملةَ التي تحتوي على توقيعه والرسالةَ التي تتضمن تعليمة تحويل SOL.
مخطط تحويل SOL
بعد إرسال المعاملة، تعالج System Program تعليمة التحويل وتُحدّث رصيد lamport لكلا الحسابين.
مخطط عملية تحويل SOL
تحقق من المستلم قبل إرسال SOL
يُضيف تحويل System Program lamport إلى أي حساب. لا يوجد فحص على مستوى البروتوكول للتحقق من أن المستلم يستطيع إعادة إخراج SOL. لا يمكن إخراج lamport إلا بواسطة البرنامج المالك للحساب، لذا فإن إرسال SOL إلى token mint أو برنامج أو PDA لا تتحكم فيه يُعرّضك لخطر الفقدان الدائم للأموال — لا يمكن إعادتها إلا من قِبَل جهة مخوّلة تحددها البرنامج المالك. لا يمكن استرداد SOL المُرسَل إلى token account إلا من قِبَل مالك ذلك الحساب، وليس المُرسِل أبداً.
تحويلات token عبر SPL محمية جزئياً بذاتها: يرفض Token Program أي تحويل لا تتطابق حساباته مع الـ mint المتوقعة. أما تحويلات SOL الأصلية فلا تمتلك هذه الحماية، لذا يجب على المُرسِل التحقق من المستلم قبل التوقيع. راجع التحقق من العنوان للاطلاع على منطق التصنيف الكامل.
يوضح المثال أدناه الكود المتعلق بالمخططات أعلاه. راجع
دالة transfer
الخاصة بـ System Program.
import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";import { systemProgram } from "@solana-program/system";const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n))).use(systemProgram());const sender = client.payer;const recipient = await generateKeyPairSigner();const LAMPORTS_PER_SOL = 1_000_000_000n;const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL// Check balance before transferconst { value: preBalance1 } = await client.rpc.getBalance(sender.address).send();const { value: preBalance2 } = await client.rpc.getBalance(recipient.address).send();// Create a transfer instruction for transferring SOL from sender to recipientconst transferInstruction = client.system.instructions.transferSol({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});const transactionSignature = await client.sendTransaction([transferInstruction]);// Check balance after transferconst { value: postBalance1 } = await client.rpc.getBalance(sender.address).send();const { value: postBalance2 } = await client.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.context.signature);
يوضح المثال التالي بنية معاملة تحتوي على تعليمات تحويل SOL واحدة.
import {createClient,generateKeyPairSigner,lamports,createTransactionMessage,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,appendTransactionMessageInstructions,pipe,signTransactionMessageWithSigners,getCompiledTransactionMessageDecoder} from "@solana/kit";import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";import { systemProgram } from "@solana-program/system";const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n))).use(systemProgram());const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();const sender = client.payer;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 = client.system.instructions.transferSol({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}}]}
التحقق من المستلم قبل التحويل
نظرًا لأن تحويل SOL ينجح إلى أي حساب، تحقق من المستلم قبل التوقيع. اجلب بيانات الحساب وأرسل فقط إلى محفظة System Program (أو عنوان على المنحنى غير ممول)؛ ارفض عمليات الإصدار، و token account، والبرامج، وعناوين PDA التي لا تتحكم فيها.
import {type Address,createSolanaRpc,fetchJsonParsedAccount,isOffCurveAddress} from "@solana/kit";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;/*** Throws if `recipient` cannot safely receive native SOL.** Only System Program wallets (or unfunded on-curve addresses) are safe. Any* other account locks the lamports because no authority can debit them.*/async function assertSafeSolRecipient(recipient: Address): Promise<void> {const account = await fetchJsonParsedAccount(rpc, recipient);if (!account.exists) {// Off-curve = a PDA with no account; reject conservatively.if (isOffCurveAddress(recipient)) {throw new Error("Recipient is a PDA with no account; SOL would be locked");}// On-curve = an unfunded wallet, safe to fund.return;}if (account.programAddress !== SYSTEM_PROGRAM) {throw new Error(`Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`);}}// A wallet: safe.await assertSafeSolRecipient("H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address);// The USDC mint: rejected before any SOL leaves the sender.await assertSafeSolRecipient("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address);
يتحقق هذا المقتطف من مستلمي SOL الأصليين. للاطلاع على التصنيف الكامل الذي يتعامل أيضًا مع إرسال رموز SPL (token account، وATAs، وToken-2022)، راجع التحقق من العنوان.
جلب تفاصيل المعاملة
بعد الإرسال، استرجع تفاصيل المعاملة باستخدام توقيع المعاملة وطريقة getTransaction RPC.
يمكنك أيضًا العثور على المعاملة باستخدام سولانا Explorer.
{"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"}
تُعرِّف الاستجابة الخام الحسابات بالفهرس وتخزّن التعليمات الداخلية (CPI) كمقاطع مشفّرة. لتحليل هذه التعليمات إلى عناوين والتنقل عبر شجرة التعليمات الكاملة، راجع استبطان المعاملات.
Is this page helpful?