Підписання повідомлення доводить, що користувач контролює гаманець: без транзакції, без комісії та без мережевих викликів. Застосунки використовують підписані повідомлення, щоб виконувати вхід, фіксувати прийняття користувачем умов, прив’язувати гаманець до наявного облікового запису або авторизувати офчейн-дію, наприклад розміщення замовлення.
Гаманці Solana підписують повідомлення двома способами:
- Офчейн-повідомлення (
solana:signOffchainMessage) обгортають ваш текст у стандартний формат, визначений у специфікації підписування офчейн-повідомлень (OCMS). Цей формат неможливо сплутати з транзакцією, він називає облікові записи, які мають його підписати, і саме його підписують апаратні гаманці. - Необроблені повідомлення (
solana:signMessage) підписують саме ті байти, які передає ваш застосунок, без префікса та без оголошених підписантів. Їх підтримує більшість програмних гаманців, але ніщо в цих байтах не вказує, що це повідомлення, а не транзакція.
Використовуйте офчейн-повідомлення там, де гаманець їх підтримує, а де ні — переходьте на необроблені повідомлення. У цьому посібнику як приклад розглянуто вхід у систему, але ті самі кроки підписання й перевірки застосовуються до будь-якого повідомлення.
Оберіть формат повідомлення
| Офчейн-повідомлення | Необроблене повідомлення | Транзакція | |
|---|---|---|---|
| Можливість гаманця | signOffchainMessage | signMessage | signTransaction |
| Підтримка апаратних гаманців | Залежить від пристрою | Ні | Так |
| Зрозуміле користувачеві | Так | Лише в програмних гаманцях | Залежить від пристрою |
| Вказує обов’язкових підписантів | Так | Ні | Так |
| RPC-виклики та комісії | Немає | Немає | Blockhash, надсилання, комісія, підтвердження |
Чому не транзакція?
Раніше апаратні гаманці не мали способу підписати читабельне повідомлення. Багато застосунків просили таких користувачів натомість підписати порожню транзакцію. Це працює, але додає затримку, витрати та ризик:
- Комісії мережі. За транзакцію сплачується комісія, навіть якщо вона нічого не робить.
- Кілька RPC-викликів. Ваш застосунок отримує blockhash, виконує симуляцію, надсилає транзакцію, а потім опитує мережу для підтвердження.
- Додаткова затримка. Кожен із цих викликів — це окремий цикл запит-відповідь, поки користувач ще не ввійшов у систему.
- Нічого читабельного. Користувач схвалює байти транзакції, а не заяву про те, з чим він погоджується.
- Поведінковий ризик. Якщо просити користувачів підписувати транзакцію лише для входу, їх легше обманом змусити підписати шкідливу.
Офчейн-повідомлення оминає все це. Єдиний цикл запит-відповідь — це звернення до вашого власного сервера.
Що підписує гаманець
Офчейн-повідомлення починається з фіксованого домену підписання \xffsolana offchain.
Жодна коректна транзакція не починається з цих байтів, тому підписане повідомлення
ніколи не можна використати як транзакцію. Далі версія 1 формату перелічує обов’язкових
підписантів і містить повідомлення як текст UTF-8 без обмеження довжини.
Гаманець формує ці байти, підписує їх і повертає разом із підписом. Специфікація описує формат повністю, зокрема застарілу версію 0.
Оберіть спосіб входу
Не кожен гаманець уже підтримує офчейн-повідомлення. Перш ніж запитувати підпис, перевірте можливості підключеного облікового запису й переходьте на запасний варіант, якщо потрібної можливості немає:
- Якщо обліковий запис підтримує
solana:signOffchainMessageіз версією повідомлення 1, підпишіть офчейн-повідомлення. - В іншому разі підпишіть той самий запит (challenge) через
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 і надішліть їх на свій
сервер разом з адресою гаманця.
Складіть запит (challenge) так, щоб він добре читався на малому екрані. Вкажіть назву застосунку й домен, поясніть, що означає підписання, і додайте одноразовий 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.
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:
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?