توقيع الرسالة يثبت أن المستخدم يتحكم في محفظة، دون معاملة، ودون رسوم، ودون أي استدعاء للشبكة. تستخدم التطبيقات الرسائل الموقّعة لتسجيل دخول المستخدمين، أو لتسجيل قبول المستخدم للشروط، أو لربط محفظة بحساب موجود، أو لتفويض إجراء يحدث خارج السلسلة، مثل تقديم طلب.
توقّع محافظ سولانا الرسائل بطريقتين:
- الرسائل خارج السلسلة (
solana:signOffchainMessage) تغلّف نصك في صيغة قياسية، تحددها مواصفات توقيع الرسائل خارج السلسلة (OCMS). لا يمكن الخلط بين هذه الصيغة وبين المعاملة، وهي تسمّي الحسابات التي يجب أن توقّعها، وهي الصيغة التي توقّع بها محافظ الأجهزة. - الرسائل الخام (
solana:signMessage) توقّع البايتات التي يمررها تطبيقك بالضبط، دون بادئة ودون موقّعين معلنين. تدعمها معظم المحافظ البرمجية، لكن لا شيء في البايتات يدل على أنها رسالة وليست معاملة.
استخدم الرسائل خارج السلسلة حيث تدعمها المحفظة، وارجع إلى الرسائل الخام حيث لا تدعمها. يستخدم باقي هذا الدليل تسجيل الدخول كمثال تطبيقي، لكن خطوات التوقيع والتحقق نفسها تنطبق على أي رسالة.
اختيار صيغة الرسالة
| رسالة خارج السلسلة | رسالة خام | معاملة | |
|---|---|---|---|
| ميزة المحفظة | signOffchainMessage | signMessage | signTransaction |
| دعم محافظ الأجهزة | يعتمد على الجهاز | لا | نعم |
| مقروءة للمستخدم | نعم | المحافظ البرمجية فقط | يعتمد على الجهاز |
| تعلن الموقّعين المطلوبين | نعم | لا | نعم |
| استدعاءات RPC والرسوم | لا شيء | لا شيء | Blockhash، والإرسال، والرسوم، والتأكيد |
لماذا لا نستخدم معاملة؟
لم يكن لدى محافظ الأجهزة سابقًا أي طريقة لتوقيع رسالة مقروءة. لجأ كثير من التطبيقات إلى مطالبة هؤلاء المستخدمين بتوقيع معاملة فارغة بدلًا من ذلك. هذا يعمل، لكنه يضيف زمن استجابة وتكلفة ومخاطر:
- رسوم الشبكة. تدفع المعاملة رسومًا حتى عندما لا تفعل شيئًا.
- استدعاءات RPC متعددة. يجلب تطبيقك blockhash، ثم يجري المحاكاة، ثم يرسل، ثم يستعلم بشكل متكرر عن التأكيد.
- زمن استجابة إضافي. كل استدعاء من هذه الاستدعاءات هو رحلة ذهاب وإياب قبل أن يتم تسجيل دخول المستخدم.
- لا شيء مقروء. يوافق المستخدم على بايتات معاملة، لا على بيان بما يوافق عليه.
- مخاطر سلوكية. مطالبة المستخدمين بتوقيع معاملة لمجرد تسجيل الدخول تجعل خداعهم لتوقيع معاملة ضارة أسهل.
الرسالة خارج السلسلة تتجاوز كل ذلك. رحلة الذهاب والإياب الوحيدة هي إلى خادمك الخاص.
ما الذي توقّعه المحفظة
تبدأ الرسالة خارج السلسلة بنطاق توقيع ثابت، وهو \xffsolana offchain.
لا تبدأ أي معاملة صالحة بهذه البايتات، لذا لا يمكن أبدًا استخدام رسالة موقّعة
كمعاملة. ثم تسرد النسخة 1 من الصيغة الموقّعين المطلوبين
وتحمل الرسالة كنص UTF-8، دون حد للطول.
تبني المحفظة هذه البايتات، وتوقّعها، وتعيدها مع التوقيع. تغطي المواصفات الصيغة كاملة، بما في ذلك النسخة 0 القديمة.
اختيار طريقة تسجيل الدخول
لا تدعم كل المحافظ الرسائل خارج السلسلة بعد. تحقق من ميزات الحساب المتصل قبل أن تطلب توقيعًا، وارجع إلى البديل عندما تكون الميزة غير متوفرة:
- إذا كان الحساب يدعم
solana:signOffchainMessageمع النسخة 1 من الرسالة، فوقّع رسالة خارج السلسلة. - وإلا، فوقّع التحدي نفسه باستخدام
solana:signMessage. تدعمها معظم المحافظ البرمجية. - امنح مستخدمي محافظ الأجهزة التي لا تدعم الرسائل خيار اشتراك صريحًا، مثل مفتاح تبديل "أستخدم محفظة أجهزة"، يوقّع معاملة تسجيل دخول بدلًا من ذلك. لا توجّههم إلى هذا المسار افتراضيًا.
توقيع رسالة في المتصفح
ثبّت حزم Wallet Standard إلى جانب Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
تأخذ هذه الدوال المساعدة UiWalletAccount، مثل account الذي تعيده
useConnectedWallet في دليل React:
import {SolanaSignOffchainMessage,type SolanaSignOffchainMessageFeature} from "@solana/wallet-standard-features";import type { UiWalletAccount } from "@wallet-standard/ui";import { getWalletAccountFeature } from "@wallet-standard/ui-features";import { getWalletAccountForUiWalletAccount } from "@wallet-standard/ui-registry";type OffchainMessageFeature =SolanaSignOffchainMessageFeature[typeof SolanaSignOffchainMessage];export function supportsOffchainMessages(account: UiWalletAccount) {if (!account.features.includes(SolanaSignOffchainMessage)) return false;const feature = getWalletAccountFeature(account,SolanaSignOffchainMessage) as OffchainMessageFeature;return feature.supportedMessageVersions.includes(1);}export async function signOffchainMessage(account: UiWalletAccount,message: string) {const feature = getWalletAccountFeature(account,SolanaSignOffchainMessage) as OffchainMessageFeature;const [output] = await feature.signOffchainMessage({messageVersion: 1,account: getWalletAccountForUiWalletAccount(account),message,requiredSigners: [account.publicKey]});return output;}
تعيد signOffchainMessage القيمتين signedOffchainMessage، وهي البايتات الكاملة التي
وقّعتها المحفظة، وsignature. رمّز كليهما بصيغة Base64 وأرسلهما إلى خادمك مع
عنوان المحفظة.
اكتب التحدي بحيث يُقرأ جيدًا على الشاشة الصغيرة. اذكر اسم تطبيقك ونطاقه، واذكر ما يفعله التوقيع، وضمّن nonce للاستخدام مرة واحدة ووقت إصدار من خادمك:
swap.app wants you to sign in with your Solana account.Signing in costs nothing and does not submit a transaction.Nonce: 8f3a1c9eIssued at: 2026-09-29T17:04:00Z
التحقق من التوقيع على خادمك
التحقق هو فحص محلي للتوقيع ولا يحتاج إلى اتصال RPC. فكّ ترميز البايتات التي وقّعتها المحفظة، وتأكد من أنها تطابق التحدي الذي أصدرته، ثم تحقق من التوقيع:
import {address,assertOffchainMessageV1Equal,getBase64Encoder,getOffchainMessageDecoder,verifyOffchainMessageEnvelope,type OffchainMessageBytes,type SignatureBytes} from "@solana/kit";export async function verifySignIn(input: {walletAddress: string;expectedMessage: string;signedOffchainMessage: string;signature: string;}) {const signer = address(input.walletAddress);const base64 = getBase64Encoder();const content = base64.encode(input.signedOffchainMessage) as OffchainMessageBytes;const signature = base64.encode(input.signature) as SignatureBytes;const received = getOffchainMessageDecoder().decode(content);if (received.version !== 1) {throw new Error(`Expected a v1 message, got v${received.version}`);}assertOffchainMessageV1Equal(received, {version: 1,requiredSignatories: [{ address: signer }],content: input.expectedMessage});await verifyOffchainMessageEnvelope({content,signatures: { [signer]: signature }});}
تطلق assertOffchainMessageV1Equal خطأً إذا اختلف المحتوى أو الموقّعون عمّا
توقعته. وتطلق verifyOffchainMessageEnvelope خطأً إذا كان التوقيع
مفقودًا أو غير صالح. ابحث عن expectedMessage باستخدام nonce على خادمك بدلًا من
الوثوق بالنسخة التي يعيد العميل إرسالها. استهلك nonce بمجرد نجاح
التحقق، حتى لا يمكن استخدام التوقيع نفسه لتسجيل الدخول مرتين.
التحقق من رسالة خام
يغطي توقيع signMessage بايتات التحدي مباشرة، لذا لا يحتاج العميل
إلا إلى إرسال التوقيع وعنوان المحفظة. تحقق منه مقابل
التحدي الذي أصدرته:
import {address,getBase64Encoder,getPublicKeyFromAddress,getUtf8Encoder,signatureBytes,verifySignature} from "@solana/kit";export async function verifyRawSignIn(input: {walletAddress: string;expectedMessage: string;signature: string;}) {const publicKey = await getPublicKeyFromAddress(address(input.walletAddress));const signature = signatureBytes(getBase64Encoder().encode(input.signature));const message = getUtf8Encoder().encode(input.expectedMessage);if (!(await verifySignature(publicKey, signature, message))) {throw new Error("Invalid signature");}}
تنطبق قواعد nonce نفسها: ابحث عن expectedMessage باستخدام nonce واستهلكه
بمجرد نجاح التحقق.
تعيد @solana/kit تصدير هذه الدوال المساعدة من @solana/offchain-messages. استخدم
الحزمة المستقلة إذا كان خادمك لا يعتمد على Kit.
تسجيل الدخول باستخدام سولانا
يوحّد تسجيل الدخول باستخدام سولانا (SIWS)
التحدي نفسه. يمرر تطبيقك حقولًا منظمة، مثل
domain وstatement وnonce وissuedAt، إلى ميزة solana:signIn في المحفظة.
تنسقها المحفظة في رسالة قياسية، وتتصل إذا لزم الأمر،
وتوقّعها في خطوة واحدة.
تضيف النسخة 1.1.0 من solana:signIn الخيار useOffchainMessage. عندما
تمرر useOffchainMessage: { messageVersion: 1 }، تغلّف المحفظة نص SIWS
في رسالة خارج السلسلة من النسخة 1 مع اعتبار الحساب الموقّع هو الموقّع المطلوب الوحيد.
يمكن لمحافظ الأجهزة عرضها وتوقيعها، ويُبلغ الناتج عن
signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. أما المحافظ
التي تعلن النسخة 1.0.0 فلا تعرف هذا الخيار، لذا تحقق من نسخة الميزة
أولًا واحذفه بخلاف ذلك.
solana:signIn ميزة محفظة وليست ميزة حساب، لذا تأخذ هذه
الدوال المساعدة UiWallet:
import {SolanaSignIn,type SolanaSignInFeature,type SolanaSignInInput} from "@solana/wallet-standard-features";import type { UiWallet } from "@wallet-standard/ui";import { getWalletFeature } from "@wallet-standard/ui-features";type SignInFeature = SolanaSignInFeature[typeof SolanaSignIn];export function supportsOffchainSignIn(wallet: UiWallet) {if (!wallet.features.includes(SolanaSignIn)) return false;const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;return feature.version === "1.1.0";}export async function signIn(wallet: UiWallet, input: SolanaSignInInput) {const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;const [output] = await feature.signIn({...input,...(supportsOffchainSignIn(wallet) && {useOffchainMessage: { messageVersion: 1 }})});return output;}
ابنِ المُدخل على خادمك، مع nonce للاستخدام مرة واحدة ووقت issuedAt،
واحتفظ بنسخة مفهرسة بواسطة nonce. استهلك nonce بمجرد نجاح
التحقق من تسجيل الدخول.
من دون Wallet Adapter، تتحقق verifySignIn من @solana/wallet-standard-util
من ناتج SIWS العادي فقط. عندما يشير signedMessageFormat إلى رسالة
خارج السلسلة، تكون signedMessage رسالة خارج السلسلة محتواها هو نص SIWS.
أعد بناء النص المتوقع من المُدخل المخزّن وعنوان الموقّع باستخدام
createSignInMessageText من @solana/wallet-standard-util، ثم تحقق منه
كأي رسالة أخرى خارج السلسلة، كما في
التحقق من التوقيع على خادمك. يغطي
ملف README الخاص بـ @solana/offchain-messages
هذا التدفق في Kit.
للمحافظ
تضيف المحافظ الدعم عبر تنفيذ ميزة solana:signOffchainMessage في Wallet
Standard من @solana/wallet-standard-features:
- أبلغ عن
version: "1.0.0"واذكر نسخ الرسائل التي يمكنك توقيعها فيsupportedMessageVersions. حاليًا، هي[1]. - أنشئ مقدمة الإصدار 1 (preamble) من
requiredSigners، التي تقوم بفرزها والتحقق من عدم وجود تكرارات فيها. ارفض الطلب إذا لم يكن الحساب من بين الموقّعين المطلوبين. - اعرض نص الرسالة للمستخدم كاملاً قبل التوقيع، بما في ذلك على أجهزة الأجهزة الصلبة.
- أعد البايتات الدقيقة التي وقّعتها بصيغة
signedOffchainMessage، إلى جانب التوقيعsignatureبخوارزمية Ed25519. - لدعم تسجيل الدخول باستخدام سولانا (Sign In With Solana) خارج السلسلة، أبلِغ عن
version: "1.1.0"لـsolana:signIn. عندما تتضمن المدخلاتuseOffchainMessage، وقّع نص SIWS كرسالة خارج السلسلة من الإصدار 1، وأعد بايتاتها بصيغةsignedMessage، واضبطsignedMessageFormat. - واصل دعم
solana:signMessage. فهو لم يتغير، ولا تزال التطبيقات تستخدمه كخيار احتياطي.
Is this page helpful?