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

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

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

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

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.

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 в офчейн-повідомлення версії 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, а потім перевірте його як будь-яке інше офчейн-повідомлення, як у розділі Перевірте підпис на сервері. Цей процес у Kit описано в README @solana/offchain-messages.

Для гаманців

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

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

Is this page helpful?