La firma di un messaggio dimostra che un utente controlla un wallet, senza transazioni, senza commissioni e senza chiamate di rete. Le app usano i messaggi firmati per far accedere gli utenti, registrare che un utente ha accettato i termini, collegare un wallet a un account esistente o autorizzare un'azione che avviene off-chain, come l'invio di un ordine.
I wallet Solana firmano i messaggi in due modi:
- Messaggi off-chain (
solana:signOffchainMessage) racchiudono il tuo testo in un formato standard, definito dalla specifica della firma di messaggi off-chain (OCMS). Il formato non può mai essere scambiato per una transazione, indica gli account che devono firmarlo ed è il formato firmato dagli hardware wallet. - Messaggi raw (
solana:signMessage) firmano esattamente i byte passati dalla tua app, senza prefisso e senza firmatari dichiarati. La maggior parte dei software wallet li supporta, ma nulla nei byte li identifica come messaggio anziché come transazione.
Usa i messaggi off-chain quando il wallet li supporta e ricorri ai messaggi raw quando non li supporta. Il resto di questa guida usa l'accesso come esempio pratico, ma gli stessi passaggi di firma e verifica si applicano a qualsiasi messaggio.
Scegliere un formato di messaggio
| Messaggio off-chain | Messaggio raw | Transazione | |
|---|---|---|---|
| Funzionalità del wallet | signOffchainMessage | signMessage | signTransaction |
| Supporto hardware wallet | Dipende dal dispositivo | No | Sì |
| Leggibile dall'utente | Sì | Solo software | Dipende dal dispositivo |
| Dichiara i firmatari richiesti | Sì | No | Sì |
| Chiamate RPC e commissioni | Nessuna | Nessuna | Blockhash, invio, commissione, conferma |
Perché non una transazione?
In passato gli hardware wallet non avevano modo di firmare un messaggio leggibile. Molte app ripiegavano sulla richiesta a questi utenti di firmare invece una transazione no-op. Funziona, ma aggiunge latenza, costi e rischi:
- Commissioni di rete. Una transazione paga una commissione, anche quando non fa nulla.
- Più chiamate RPC. La tua app recupera un blockhash, simula, invia e poi esegue il polling per la conferma.
- Latenza aggiuntiva. Ognuna di queste chiamate è un round trip prima che l'utente abbia effettuato l'accesso.
- Nulla di leggibile. L'utente approva i byte della transazione, non una dichiarazione di ciò a cui sta acconsentendo.
- Rischio comportamentale. Chiedere agli utenti di firmare una transazione solo per accedere li rende più facili da indurre a firmarne una dannosa.
Un messaggio off-chain evita tutto questo. L'unico round trip è verso il tuo server.
Che cosa firma il wallet
Un messaggio off-chain inizia con un dominio di firma fisso, \xffsolana offchain.
Nessuna transazione valida inizia con quei byte, quindi un messaggio firmato non può mai essere
usato come transazione. La versione 1 del formato elenca quindi i firmatari richiesti
e contiene il messaggio come testo UTF-8, senza limite di lunghezza.
Il wallet costruisce questi byte, li firma e li restituisce insieme alla firma. La specifica copre il formato completo, inclusa la versione legacy 0.
Scegliere un metodo di accesso
Non tutti i wallet supportano ancora i messaggi off-chain. Controlla le funzionalità dell'account connesso prima di richiedere una firma e usa un fallback quando la funzionalità manca:
- Se l'account supporta
solana:signOffchainMessagecon la versione 1 del messaggio, firma un messaggio off-chain. - Altrimenti, firma la stessa challenge con
solana:signMessage. La maggior parte dei software wallet la supporta. - Offri agli utenti di hardware wallet senza supporto per i messaggi un consenso esplicito, ad esempio un toggle "Sto usando un hardware wallet", che firmi invece una transazione di accesso. Non indirizzarli su questo percorso per impostazione predefinita.
Firmare un messaggio nel browser
Installa i pacchetti Wallet Standard insieme a Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
Questi helper accettano un UiWalletAccount, come l'account restituito da
useConnectedWallet nella guida 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 restituisce signedOffchainMessage, i byte completi firmati dal wallet,
e signature. Codifica entrambi in Base64 e inviali al tuo server con
l'indirizzo del wallet.
Scrivi la challenge in modo che si legga bene su uno schermo piccolo. Indica il nome della tua app e il dominio, spiega che cosa comporta la firma e includi un nonce monouso e un'ora di emissione provenienti dal tuo server:
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
Verificare la firma sul server
La verifica è un controllo locale della firma e non richiede alcuna connessione RPC. Decodifica i byte firmati dal wallet, conferma che corrispondano alla challenge che hai emesso, quindi controlla la firma:
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 genera un'eccezione se il contenuto o i firmatari differiscono da
quanto ti aspettavi. verifyOffchainMessageEnvelope genera un'eccezione se la firma è
mancante o non valida. Cerca expectedMessage per nonce sul tuo server invece di
fidarti della copia restituita dal client. Consuma il nonce una volta
verificato, così la stessa firma non può effettuare l'accesso due volte.
Verificare un messaggio raw
Una firma signMessage copre direttamente i byte della challenge, quindi il client
deve inviare solo la firma e l'indirizzo del wallet. Controllala rispetto alla
challenge che hai emesso:
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");}}
Si applicano le stesse regole per il nonce: cerca expectedMessage per nonce e consumalo
una volta verificato.
@solana/kit riesporta questi helper da @solana/offchain-messages. Usa
il pacchetto standalone se il tuo server non dipende da Kit.
Sign In With Solana
Sign In With Solana (SIWS)
standardizza la challenge stessa. La tua app passa campi strutturati, come
domain, statement, nonce e issuedAt, alla funzionalità solana:signIn
del wallet. Il wallet li formatta in un messaggio standard, si connette se necessario
e lo firma in un unico passaggio.
La versione 1.1.0 di solana:signIn aggiunge un'opzione useOffchainMessage. Quando
passi useOffchainMessage: { messageVersion: 1 }, il wallet racchiude il testo SIWS
in un messaggio off-chain versione 1 con l'account di firma come unico
firmatario richiesto. Gli hardware wallet possono visualizzarlo e firmarlo, e l'output riporta
signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. I wallet
che dichiarano la versione 1.0.0 non conoscono l'opzione, quindi controlla prima la versione
della funzionalità e altrimenti omettila.
solana:signIn è una funzionalità del wallet, non una funzionalità dell'account, quindi questi
helper accettano un 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;}
Costruisci l'input sul tuo server, con un nonce monouso e un orario issuedAt,
e conserva una copia indicizzata dal nonce. Consuma il nonce una volta verificato l'accesso.
Senza Wallet Adapter, verifySignIn da @solana/wallet-standard-util
controlla solo l'output SIWS semplice. Quando signedMessageFormat segnala un messaggio off-chain,
signedMessage è un messaggio off-chain il cui contenuto è il testo SIWS.
Ricostruisci il testo atteso dall'input memorizzato e dall'indirizzo di firma con
createSignInMessageText da @solana/wallet-standard-util, quindi verificalo
come qualsiasi altro messaggio off-chain, come in
Verificare la firma sul tuo server. Il
README di @solana/offchain-messages
copre questo flusso in Kit.
Per i wallet
I wallet aggiungono il supporto implementando la funzionalità Wallet Standard
solana:signOffchainMessage da @solana/wallet-standard-features:
- Riporta
version: "1.0.0"ed elenca le versioni dei messaggi che puoi firmare insupportedMessageVersions. Oggi è[1]. - Costruisci il preambolo della versione 1 a partire da
requiredSigners, che devi ordinare e controllare per individuare eventuali duplicati. Rifiuta la richiesta se l'account non è uno dei firmatari richiesti. - Mostra all'utente il testo del messaggio per intero prima della firma, anche sui dispositivi hardware.
- Restituisci i byte esatti che hai firmato come
signedOffchainMessage, insieme allasignatureEd25519. - Per supportare Sign In With Solana off-chain, indica
version: "1.1.0"persolana:signIn. Quando l'input includeuseOffchainMessage, firma il testo SIWS come messaggio off-chain di versione 1, restituisci i relativi byte comesignedMessagee impostasignedMessageFormat. - Continua a supportare
solana:signMessage. Non è cambiato e le app lo usano ancora come soluzione di ripiego.
Is this page helpful?