Podpisanie wiadomości dowodzi, że użytkownik kontroluje portfel – bez transakcji, bez opłaty i bez wywołania sieciowego. Aplikacje używają podpisanych wiadomości, aby logować użytkowników, rejestrować akceptację regulaminu, łączyć portfel z istniejącym kontem lub autoryzować działanie wykonywane off-chain, np. złożenie 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). Tego formatu nigdy nie da się pomylić z transakcją, wskazuje on konta, które muszą go podpisać, i jest to format podpisywany przez portfele sprzętowe. - Surowe wiadomości (
solana:signMessage) podpisują dokładnie te bajty, które przekaże Twoja aplikacja, bez prefiksu i bez zadeklarowanych podpisujących. Większość portfeli programowych je obsługuje, ale nic w tych bajtach nie wskazuje, że są wiadomością, a nie transakcją.
Używaj wiadomości off-chain tam, gdzie portfel je obsługuje, a tam, gdzie nie, przechodź na surowe wiadomości. W dalszej części przewodnika jako przykład sł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, wysłanie, opłata, potwierdzenie |
Dlaczego nie transakcja?
Portfele sprzętowe kiedyś nie miały możliwości podpisania czytelnej wiadomości. Wiele aplikacji prosiło wtedy takich użytkowników o podpisanie pustej transakcji. To działa, 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 osobny obieg żądania, 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. Jedyny obieg żądania odbywa się do Twojego własnego serwera.
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 razem 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. Sprawdź funkcje podłączonego konta, zanim poprosisz o podpis, i przejdź na rozwiązanie awaryjne, gdy funkcji brakuje:
- Jeśli konto obsługuje
solana:signOffchainMessagew wersji wiadomości 1, podpisz wiadomość off-chain. - W przeciwnym razie podpisz to samo wyzwanie za pomocą
solana:signMessage. Większość portfeli programowych ją obsługuje. - Użytkownikom portfeli sprzętowych bez obsługi wiadomości daj jawną opcję zgody, np. przełącznik "Używam portfela sprzętowego", która podpisuje transakcję logowania zamiast tego. 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, np. account zwrócone przez
useConnectedWallet w przewodniku dla Reacta:
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 domeny, napisz, do czego służy podpis, i dodaj jednorazowy nonce oraz 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 i 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
tego, czego oczekiwano. verifyOffchainMessageEnvelope zgłasza błąd, jeśli podpisu
brakuje lub jest nieprawidłowy. Wyszukuj expectedMessage po nonce na swoim serwerze, zamiast
ufać kopii odsyłanej przez klienta. Zużyj nonce po pomyślnej
weryfikacji, aby ten sam podpis nie mógł posłużyć do zalogowania dwukrotnie.
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 przez Ciebie 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: wyszukuj expectedMessage po 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 łączy się
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 w wynikach pojawia się
signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Portfele,
które deklarują wersję 1.0.0, nie znają tej opcji, więc najpierw sprawdź wersję funkcji
i w przeciwnym razie pomiń opcję.
solana:signIn jest funkcją portfela, a nie konta, więc 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ę powiązaną z nonce. Zużyj nonce po pomyślnej weryfikacji
logowania.
Bez Wallet Adapter verifySignIn z @solana/wallet-standard-util
sprawdza tylko zwykłe wyniki 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. Ten przepływ w Kit opisuje
README pakietu @solana/offchain-messages.
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 wypisz wersje wiadomości, które możesz podpisywać, wsupportedMessageVersions. Obecnie jest to[1]. - Zbuduj preambułę w 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 pokaż użytkownikowi pełny tekst wiadomości, również na urządzeniach sprzętowych.
- Zwróć dokładne bajty, które podpisałeś, jako
signedOffchainMessage, razem z podpisem Ed25519signature. - Aby obsługiwać off-chain Sign In With Solana, zgłoś
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?