يثبت توقيع الرسالة أن المستخدم يتحكم في محفظة، دون معاملة ودون رسوم ودون أي استدعاء للشبكة. تستخدم التطبيقات الرسائل الموقّعة لتسجيل دخول المستخدمين، أو لتسجيل قبول المستخدم للشروط، أو لربط محفظة بحساب موجود، أو لتفويض إجراء يتم خارج السلسلة، مثل تقديم طلب.
توقّع محافظ سولانا الرسائل بطريقتين:
- الرسائل خارج السلسلة (
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، ثم تحقق منه
كأي رسالة خارج السلسلة أخرى، كما في
تحقق من التوقيع على خادمك. ويغطي
ملف @solana/offchain-messages README
هذا التدفق في Kit.
للمحافظ
تضيف المحافظ الدعم بتنفيذ ميزة Wallet Standard المسماة solana:signOffchainMessage
من @solana/wallet-standard-features:
- أبلغ عن
version: "1.0.0"واذكر نسخ الرسائل التي يمكنك توقيعها فيsupportedMessageVersions. وهي اليوم[1]. - أنشئ المقدمة (preamble) للإصدار 1 من
requiredSigners، بعد أن تقوم بفرزها والتحقق من عدم وجود تكرارات. ارفض الطلب إذا لم يكن الحساب من بين الموقّعين المطلوبين. - اعرض نص الرسالة للمستخدم كاملاً قبل التوقيع، بما في ذلك على أجهزة الأجهزة الصلبة (hardware).
- أعد البايتات الدقيقة التي وقّعتها في
signedOffchainMessage، مع التوقيعsignatureبخوارزمية Ed25519. - لدعم تسجيل الدخول عبر سولانا (Sign In With Solana) خارج السلسلة، أبلغ عن
version: "1.1.0"للمعرّفsolana:signIn. عندما تتضمن المدخلاتuseOffchainMessage، وقّع نص SIWS كرسالة خارج السلسلة من الإصدار 1، وأعد بايتاتها فيsignedMessage، وعيّنsignedMessageFormat. - واصل دعم
solana:signMessage. فهو لم يتغير، وما زالت التطبيقات تستخدمه كخيار احتياطي.
Is this page helpful?