توقيع الرسائل ومصادقة المستخدمين

توقيع الرسالة يثبت أن المستخدم يتحكم في محفظة، دون معاملة، ودون رسوم، ودون أي استدعاء للشبكة. تستخدم التطبيقات الرسائل الموقّعة لتسجيل دخول المستخدمين، أو لتسجيل قبول المستخدم للشروط، أو لربط محفظة بحساب موجود، أو لتفويض إجراء يحدث خارج السلسلة، مثل تقديم طلب.

توقّع محافظ سولانا الرسائل بطريقتين:

  • الرسائل خارج السلسلة (solana:signOffchainMessage) تغلّف نصك في صيغة قياسية، تحددها مواصفات توقيع الرسائل خارج السلسلة (OCMS). لا يمكن الخلط بين هذه الصيغة وبين المعاملة، وهي تسمّي الحسابات التي يجب أن توقّعها، وهي الصيغة التي توقّع بها محافظ الأجهزة.
  • الرسائل الخام (solana:signMessage) توقّع البايتات التي يمررها تطبيقك بالضبط، دون بادئة ودون موقّعين معلنين. تدعمها معظم المحافظ البرمجية، لكن لا شيء في البايتات يدل على أنها رسالة وليست معاملة.

استخدم الرسائل خارج السلسلة حيث تدعمها المحفظة، وارجع إلى الرسائل الخام حيث لا تدعمها. يستخدم باقي هذا الدليل تسجيل الدخول كمثال تطبيقي، لكن خطوات التوقيع والتحقق نفسها تنطبق على أي رسالة.

اختيار صيغة الرسالة

رسالة خارج السلسلةرسالة خاممعاملة
ميزة المحفظةsignOffchainMessagesignMessagesignTransaction
دعم محافظ الأجهزةيعتمد على الجهازلانعم
مقروءة للمستخدمنعمالمحافظ البرمجية فقطيعتمد على الجهاز
تعلن الموقّعين المطلوبيننعملانعم
استدعاءات RPC والرسوملا شيءلا شيءBlockhash، والإرسال، والرسوم، والتأكيد

لماذا لا نستخدم معاملة؟

لم يكن لدى محافظ الأجهزة سابقًا أي طريقة لتوقيع رسالة مقروءة. لجأ كثير من التطبيقات إلى مطالبة هؤلاء المستخدمين بتوقيع معاملة فارغة بدلًا من ذلك. هذا يعمل، لكنه يضيف زمن استجابة وتكلفة ومخاطر:

  • رسوم الشبكة. تدفع المعاملة رسومًا حتى عندما لا تفعل شيئًا.
  • استدعاءات RPC متعددة. يجلب تطبيقك blockhash، ثم يجري المحاكاة، ثم يرسل، ثم يستعلم بشكل متكرر عن التأكيد.
  • زمن استجابة إضافي. كل استدعاء من هذه الاستدعاءات هو رحلة ذهاب وإياب قبل أن يتم تسجيل دخول المستخدم.
  • لا شيء مقروء. يوافق المستخدم على بايتات معاملة، لا على بيان بما يوافق عليه.
  • مخاطر سلوكية. مطالبة المستخدمين بتوقيع معاملة لمجرد تسجيل الدخول تجعل خداعهم لتوقيع معاملة ضارة أسهل.

الرسالة خارج السلسلة تتجاوز كل ذلك. رحلة الذهاب والإياب الوحيدة هي إلى خادمك الخاص.

A sign-in transaction fetches a blockhash, signs, simulates, sends, and polls for confirmation before the server can verify it. An off-chain message goes from a server challenge to a wallet signature to a server-side check, with no RPC calls and no fee.

ما الذي توقّعه المحفظة

تبدأ الرسالة خارج السلسلة بنطاق توقيع ثابت، وهو \xffsolana offchain. لا تبدأ أي معاملة صالحة بهذه البايتات، لذا لا يمكن أبدًا استخدام رسالة موقّعة كمعاملة. ثم تسرد النسخة 1 من الصيغة الموقّعين المطلوبين وتحمل الرسالة كنص UTF-8، دون حد للطول.

تبني المحفظة هذه البايتات، وتوقّعها، وتعيدها مع التوقيع. تغطي المواصفات الصيغة كاملة، بما في ذلك النسخة 0 القديمة.

A version 1 off-chain message is a 16-byte signing domain, a version byte, a signer count, each signer's 32-byte public key in sorted order, and the UTF-8 content. The envelope prefixes it with a signature count and one 64-byte signature per signer.

اختيار طريقة تسجيل الدخول

لا تدعم كل المحافظ الرسائل خارج السلسلة بعد. تحقق من ميزات الحساب المتصل قبل أن تطلب توقيعًا، وارجع إلى البديل عندما تكون الميزة غير متوفرة:

  1. إذا كان الحساب يدعم solana:signOffchainMessage مع النسخة 1 من الرسالة، فوقّع رسالة خارج السلسلة.
  2. وإلا، فوقّع التحدي نفسه باستخدام solana:signMessage. تدعمها معظم المحافظ البرمجية.
  3. امنح مستخدمي محافظ الأجهزة التي لا تدعم الرسائل خيار اشتراك صريحًا، مثل مفتاح تبديل "أستخدم محفظة أجهزة"، يوقّع معاملة تسجيل دخول بدلًا من ذلك. لا توجّههم إلى هذا المسار افتراضيًا.
When a wallet connects, check whether its account supports solana:signOffchainMessage version 1. If it does, sign an off-chain message. If not, sign a raw message with solana:signMessage, and let hardware wallet users opt into signing a sign-in transaction.

توقيع رسالة في المتصفح

ثبّت حزم Wallet Standard إلى جانب Kit:

Terminal
$
npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry

تأخذ هذه الدوال المساعدة UiWalletAccount، مثل account الذي تعيده useConnectedWallet في دليل React:

offchain-message.ts
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: 8f3a1c9e
Issued at: 2026-09-29T17:04:00Z

التحقق من التوقيع على خادمك

التحقق هو فحص محلي للتوقيع ولا يحتاج إلى اتصال RPC. فكّ ترميز البايتات التي وقّعتها المحفظة، وتأكد من أنها تطابق التحدي الذي أصدرته، ثم تحقق من التوقيع:

verify-sign-in.ts
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 بايتات التحدي مباشرة، لذا لا يحتاج العميل إلا إلى إرسال التوقيع وعنوان المحفظة. تحقق منه مقابل التحدي الذي أصدرته:

verify-raw-sign-in.ts
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:

sign-in.ts
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?