Podpisanie wiadomości dowodzi, że użytkownik kontroluje portfel – bez transakcji, bez opłaty i bez wywołań sieciowych. Aplikacje używają podpisanych wiadomości do logowania użytkowników, zapisywania faktu zaakceptowania regulaminu, powiązania portfela z istniejącym kontem lub autoryzowania działania wykonywanego off-chain, np. złożenia zamówienia.
Portfele Solana podpisują wiadomości na dwa sposoby:
- Wiadomości off-chain (
solana:signOffchainMessage) opakowują Twój tekst w standardowy format, zdefiniowany w specyfikacji podpisywania wiadomości off-chain (OCMS). Format ten nigdy nie może zostać pomylony z transakcją, wskazuje konta, które muszą go podpisać, i jest formatem podpisywanym przez portfele sprzętowe. - Surowe wiadomości (
solana:signMessage) podpisują dokładnie te bajty, które przekaże aplikacja, bez prefiksu i bez zadeklarowanych podpisujących. Obsługuje je większość portfeli programowych, ale nic w tych bajtach nie wskazuje, że to wiadomość, a nie transakcja.
Używaj wiadomości off-chain tam, gdzie portfel je obsługuje, a w pozostałych przypadkach wracaj do surowych wiadomości. W dalszej części przewodnika jako przykład posłuży logowanie, ale te same kroki podpisywania i weryfikacji dotyczą każdej wiadomości.
Wybierz format wiadomości
| Wiadomość off-chain | Surowa wiadomość | Transakcja | |
|---|---|---|---|
| Funkcja portfela | signOffchainMessage | signMessage | signTransaction |
| Obsługa portfeli sprzętowych | Zależy od urządzenia | Nie | Tak |
| Czytelna dla użytkownika | Tak | Tylko w portfelach programowych | Zależy od urządzenia |
| Deklaruje wymaganych podpisujących | Tak | Nie | Tak |
| Wywołania RPC i opłaty | Brak | Brak | Blockhash, wysyłka, opłata, potwierdzenie |
Dlaczego nie transakcja?
Portfele sprzętowe kiedyś nie miały możliwości podpisania czytelnej wiadomości. Wiele aplikacji prosiło więc takich użytkowników o podpisanie transakcji, która niczego nie robi (no-op). Działa to, ale zwiększa opóźnienia, koszty i ryzyko:
- Opłaty sieciowe. Transakcja wiąże się z opłatą, nawet jeśli nic nie robi.
- Wiele wywołań RPC. Aplikacja pobiera blockhash, symuluje, wysyła, a następnie odpytuje o potwierdzenie.
- Dodatkowe opóźnienie. Każde z tych wywołań to osobna runda komunikacji, zanim użytkownik zostanie zalogowany.
- Nic czytelnego. Użytkownik zatwierdza bajty transakcji, a nie oświadczenie o tym, na co się zgadza.
- Ryzyko behawioralne. Proszenie użytkowników o podpisanie transakcji tylko po to, by się zalogować, ułatwia nakłonienie ich do podpisania szkodliwej.
Wiadomość off-chain pomija to wszystko. Jedyna runda komunikacji odbywa się z Twoim własnym serwerem.
Co podpisuje portfel
Wiadomość off-chain zaczyna się od stałej domeny podpisu, \xffsolana offchain.
Żadna prawidłowa transakcja nie zaczyna się od tych bajtów, więc podpisana
wiadomość nigdy nie może zostać użyta jako transakcja. Wersja 1 formatu zawiera
następnie listę wymaganych podpisujących i przenosi wiadomość jako tekst UTF-8,
bez limitu długości.
Portfel buduje te bajty, podpisuje je i zwraca wraz z podpisem. Specyfikacja opisuje pełny format, w tym starszą wersję 0.
Wybierz metodę logowania
Nie każdy portfel obsługuje już wiadomości off-chain. Przed poproszeniem o podpis sprawdź funkcje połączonego konta, a gdy brakuje tej funkcji, użyj rozwiązania zastępczego:
- Jeśli konto obsługuje
solana:signOffchainMessagez wiadomością w wersji 1, podpisz wiadomość off-chain. - W przeciwnym razie podpisz to samo wyzwanie za pomocą
solana:signMessage. Obsługuje je większość portfeli programowych. - Użytkownikom portfeli sprzętowych bez obsługi wiadomości daj jawną opcję włączenia, np. przełącznik „Używam portfela sprzętowego”, który zamiast tego podpisuje transakcję logowania. Nie kieruj ich na tę ścieżkę domyślnie.
Podpisywanie wiadomości w przeglądarce
Zainstaluj pakiety Wallet Standard razem z Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
Te funkcje pomocnicze przyjmują UiWalletAccount, takie jak account zwracane
przez useConnectedWallet w przewodniku po 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 zwraca signedOffchainMessage – pełne bajty podpisane
przez portfel – oraz signature. Zakoduj oba w Base64 i wyślij na serwer razem
z adresem portfela.
Napisz wyzwanie tak, aby dobrze wyglądało na małym ekranie. Podaj nazwę aplikacji i domenę, wyjaśnij, do czego służy podpis, oraz dołącz jednorazowy nonce i czas wystawienia z Twojego serwera:
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
Weryfikacja podpisu na serwerze
Weryfikacja to lokalne sprawdzenie podpisu, które nie wymaga połączenia RPC. Zdekoduj bajty podpisane przez portfel, potwierdź, że odpowiadają wystawionemu przez Ciebie wyzwaniu, a następnie sprawdź podpis:
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 zgłasza błąd, jeśli treść lub podpisujący różnią
się od oczekiwanych. verifyOffchainMessageEnvelope zgłasza błąd, jeśli podpisu
brakuje lub jest nieprawidłowy. Wyszukuj expectedMessage według nonce na
serwerze, zamiast ufać kopii odesłanej przez klienta. Zużyj nonce po pomyślnej
weryfikacji, aby ten sam podpis nie mógł posłużyć do zalogowania dwa razy.
Weryfikacja surowej wiadomości
Podpis signMessage obejmuje bezpośrednio bajty wyzwania, więc klient musi
wysłać tylko podpis i adres portfela. Sprawdź go względem wystawionego
wyzwania:
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");}}
Obowiązują te same zasady dotyczące nonce: wyszukaj expectedMessage według
nonce i zużyj go po pomyślnej weryfikacji.
@solana/kit reeksportuje te funkcje pomocnicze z @solana/offchain-messages.
Użyj samodzielnego pakietu, jeśli Twój serwer nie zależy od Kit.
Sign In With Solana
Sign In With Solana (SIWS)
standaryzuje samo wyzwanie. Aplikacja przekazuje ustrukturyzowane pola, takie jak
domain, statement, nonce i issuedAt, do funkcji solana:signIn portfela.
Portfel formatuje je w standardową wiadomość, w razie potrzeby nawiązuje
połączenie i podpisuje ją w jednym kroku.
Wersja 1.1.0 funkcji solana:signIn dodaje opcję useOffchainMessage. Gdy
przekażesz useOffchainMessage: { messageVersion: 1 }, portfel opakowuje tekst
SIWS w wiadomość off-chain w wersji 1, w której jedynym wymaganym podpisującym
jest konto podpisujące. Portfele sprzętowe mogą ją wyświetlić i podpisać, a wynik
zawiera signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }.
Portfele deklarujące wersję 1.0.0 nie znają tej opcji, więc najpierw sprawdź
wersję funkcji, a w przeciwnym razie pomiń opcję.
solana:signIn jest funkcją portfela, a nie konta, dlatego te funkcje
pomocnicze przyjmują 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;}
Zbuduj dane wejściowe na serwerze, z jednorazowym nonce i czasem issuedAt,
i zachowaj kopię indeksowaną według nonce. Zużyj nonce po pomyślnej weryfikacji
logowania.
Bez Wallet Adapter funkcja verifySignIn z @solana/wallet-standard-util
sprawdza wyłącznie zwykłe dane wyjściowe SIWS. Gdy signedMessageFormat wskazuje
wiadomość off-chain, signedMessage jest wiadomością off-chain, której treścią
jest tekst SIWS. Odbuduj oczekiwany tekst z zapisanych danych wejściowych i adresu
podpisującego za pomocą createSignInMessageText z @solana/wallet-standard-util,
a następnie zweryfikuj go jak każdą inną wiadomość off-chain, tak jak w sekcji
Weryfikacja podpisu na serwerze.
README @solana/offchain-messages
opisuje ten przepływ w Kit.
Dla portfeli
Portfele dodają obsługę, implementując funkcję Wallet Standard
solana:signOffchainMessage z @solana/wallet-standard-features:
- Zgłoś
version: "1.0.0"i wymień wsupportedMessageVersionswersje wiadomości, które potrafisz podpisać. Obecnie jest to[1]. - Zbuduj preambułę wersji 1 na podstawie
requiredSigners, które sortujesz i sprawdzasz pod kątem duplikatów. Odrzuć żądanie, jeśli konto nie należy do wymaganych sygnatariuszy. - Przed podpisaniem wyświetl użytkownikowi pełny tekst wiadomości, również na urządzeniach sprzętowych.
- Zwróć dokładne bajty, które podpisałeś, jako
signedOffchainMessage, wraz z podpisem Ed25519 w polusignature. - Aby obsługiwać logowanie Sign In With Solana poza łańcuchem (off-chain), zgłaszaj
version: "1.1.0"dlasolana:signIn. Gdy dane wejściowe zawierająuseOffchainMessage, podpisz tekst SIWS jako wiadomość off-chain w wersji 1, zwróć jej bajty jakosignedMessagei ustawsignedMessageFormat. - Nadal obsługuj
solana:signMessage. Ta metoda pozostaje bez zmian, a aplikacje wciąż używają jej jako rozwiązania zapasowego.
Is this page helpful?