Підписування повідомлень та автентифікація користувачів

Підписання повідомлення підтверджує, що користувач контролює гаманець, без транзакції, комісії чи мережевих викликів. Застосунки використовують підписані повідомлення, щоб впускати користувачів, фіксувати прийняття ними умов, прив'язувати гаманець до існуючого облікового запису або авторизувати дію, що відбувається off-chain, наприклад розміщення замовлення.

Гаманці Solana підписують повідомлення двома способами:

  • Повідомлення off-chain (solana:signOffchainMessage) обгортають ваш текст у стандартний формат, визначений специфікацією підписання повідомлень off-chain (OCMS). Цей формат неможливо сплутати з транзакцією, він вказує облікові записи, які мають його підписати, і саме його підписують апаратні гаманці.
  • Необроблені повідомлення (solana:signMessage) підписують саме ті байти, які передає ваш застосунок, без префікса та без оголошених підписантів. Більшість програмних гаманців їх підтримують, але ніщо в цих байтах не вказує, що це повідомлення, а не транзакція.

Використовуйте повідомлення off-chain там, де гаманець їх підтримує, а там, де ні, переходьте на необроблені повідомлення. Далі в цьому посібнику як приклад розглянуто вхід у систему, але ті самі кроки підписання й перевірки застосовні до будь-якого повідомлення.

Виберіть формат повідомлення

Повідомлення off-chainНеоброблене повідомленняТранзакція
Функція гаманцяsignOffchainMessagesignMessagesignTransaction
Підтримка апаратних гаманцівЗалежить від пристроюНіТак
Зрозуміле для користувачаТакЛише в програмних гаманцяхЗалежить від пристрою
Оголошує обов'язкових підписантівТакНіТак
RPC-виклики та комісіїНемаєНемаєBlockhash, надсилання, комісія, підтвердження

Чому не транзакція?

Раніше апаратні гаманці не мали способу підписати зрозуміле повідомлення. Багато застосунків просили таких користувачів підписати порожню транзакцію. Це працює, але додає затримку, витрати та ризик:

  • Мережеві комісії. Транзакція потребує комісії, навіть якщо вона нічого не робить.
  • Кілька RPC-викликів. Ваш застосунок отримує blockhash, симулює, надсилає транзакцію, а потім опитує мережу для підтвердження.
  • Додаткова затримка. Кожен із цих викликів — це цикл запит-відповідь, поки користувач ще не увійшов у систему.
  • Нічого зрозумілого. Користувач схвалює байти транзакції, а не твердження про те, з чим він погоджується.
  • Поведінковий ризик. Якщо просити користувачів підписувати транзакцію лише для входу, їх легше обманом змусити підписати шкідливу.

Повідомлення off-chain усуває все це. Єдиний цикл запит-відповідь — до вашого власного сервера.

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.

Що підписує гаманець

Повідомлення off-chain починається з фіксованого домену підпису \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.

Виберіть спосіб входу

Не кожен гаманець уже підтримує повідомлення off-chain. Перш ніж запитувати підпис, перевірте функції підключеного облікового запису, а якщо потрібної функції немає, скористайтеся резервним варіантом:

  1. Якщо обліковий запис підтримує solana:signOffchainMessage із версією повідомлення 1, підпишіть повідомлення off-chain.
  2. В іншому разі підпишіть той самий виклик (challenge) через 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 і надішліть їх на ваш сервер разом з адресою гаманця.

Сформулюйте виклик (challenge) так, щоб його було зручно читати на малому екрані. Вкажіть назву застосунку й домен, поясніть, що саме дає підпис, і додайте одноразовий 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-з'єднання. Декодуйте байти, які підписав гаманець, переконайтеся, що вони збігаються з виданим вами викликом (challenge), а потім перевірте підпис:

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 охоплює байти виклику (challenge) безпосередньо, тому клієнту потрібно надіслати лише підпис і адресу гаманця. Перевірте його за викликом, який ви видали:

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.

Sign In With Solana

Sign In With Solana (SIWS) стандартизує сам виклик (challenge). Ваш застосунок передає структуровані поля, такі як domain, statement, nonce та issuedAt, у функцію гаманця solana:signIn. Гаманець форматує їх у стандартне повідомлення, за потреби підключається і підписує його за один крок.

Версія 1.1.0 функції solana:signIn додає опцію useOffchainMessage. Якщо передати useOffchainMessage: { messageVersion: 1 }, гаманець обгортає текст SIWS у повідомлення off-chain версії 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 повідомляє про повідомлення off-chain, signedMessage є повідомленням off-chain, вмістом якого є текст SIWS. Відтворіть очікуваний текст зі збережених вхідних даних і адреси підписанта за допомогою createSignInMessageText із @solana/wallet-standard-util, а потім перевірте його як будь-яке інше повідомлення off-chain, як описано в розділі Перевірка підпису на вашому сервері. Цей процес у Kit описано в README @solana/offchain-messages.

Для гаманців

Гаманці додають підтримку, реалізуючи функцію Wallet Standard solana:signOffchainMessage з @solana/wallet-standard-features:

  • Вкажіть version: "1.0.0" і перелічіть версії повідомлень, які ви можете підписувати, у supportedMessageVersions. Наразі це [1].
  • Сформуйте преамбулу версії 1 на основі requiredSigners, які ви сортуєте та перевіряєте на дублікати. Відхиліть запит, якщо акаунт не входить до числа обов’язкових підписувачів.
  • Перед підписанням повністю покажіть користувачеві текст повідомлення, зокрема й на апаратних пристроях.
  • Поверніть точні байти, які ви підписали, як signedOffchainMessage разом із підписом Ed25519 signature.
  • Щоб підтримувати офчейн-вхід через Solana (Sign In With Solana), повідомте version: "1.1.0" для solana:signIn. Якщо вхідні дані містять useOffchainMessage, підпишіть текст SIWS як офчейн-повідомлення версії 1, поверніть його байти як signedMessage і встановіть signedMessageFormat.
  • Продовжуйте підтримувати solana:signMessage. Він не змінився, і застосунки досі використовують його як резервний варіант.

Is this page helpful?