Assinar uma mensagem prova que o usuário controla uma carteira, sem transação, sem taxa e sem chamada de rede. Os aplicativos usam mensagens assinadas para fazer login de usuários, registrar que um usuário aceitou os termos, vincular uma carteira a uma conta existente ou autorizar uma ação que acontece off-chain, como fazer um pedido.
As carteiras Solana assinam mensagens de duas formas:
- Mensagens off-chain (
solana:signOffchainMessage) envolvem o seu texto em um formato padrão, definido pela especificação de assinatura de mensagens off-chain (OCMS). O formato nunca pode ser confundido com uma transação, indica as contas que devem assiná-lo e é o formato que as carteiras de hardware assinam. - Mensagens brutas (
solana:signMessage) assinam exatamente os bytes que o seu aplicativo informa, sem prefixo e sem signatários declarados. A maioria das carteiras de software as suporta, mas nada nos bytes indica que se trata de uma mensagem e não de uma transação.
Use mensagens off-chain quando a carteira as suportar e recorra a mensagens brutas quando não suportar. O restante deste guia usa o login como exemplo prático, mas as mesmas etapas de assinatura e verificação se aplicam a qualquer mensagem.
Escolha um formato de mensagem
| Mensagem off-chain | Mensagem bruta | Transação | |
|---|---|---|---|
| Recurso da carteira | signOffchainMessage | signMessage | signTransaction |
| Suporte a carteiras de hardware | Depende do dispositivo | Não | Sim |
| Legível para o usuário | Sim | Somente software | Depende do dispositivo |
| Declara os signatários necessários | Sim | Não | Sim |
| Chamadas RPC e taxas | Nenhuma | Nenhuma | Blockhash, envio, taxa, confirmação |
Por que não uma transação?
As carteiras de hardware não tinham como assinar uma mensagem legível. Muitos aplicativos passaram a pedir a esses usuários que assinassem uma transação vazia (no-op). Isso funciona, mas aumenta a latência, o custo e o risco:
- Taxas de rede. Uma transação paga uma taxa, mesmo quando não faz nada.
- Várias chamadas RPC. O seu aplicativo busca um blockhash, simula, envia e depois consulta a confirmação repetidamente.
- Latência adicional. Cada uma dessas chamadas é uma ida e volta antes de o usuário fazer login.
- Nada legível. O usuário aprova bytes de transação, não uma declaração do que está concordando em fazer.
- Risco comportamental. Pedir aos usuários que assinem uma transação só para fazer login os torna mais fáceis de enganar para que assinem uma transação maliciosa.
Uma mensagem off-chain elimina tudo isso. A única ida e volta é até o seu próprio servidor.
O que a carteira assina
Uma mensagem off-chain começa com um domínio de assinatura fixo, \xffsolana offchain.
Nenhuma transação válida começa com esses bytes, portanto uma mensagem assinada
nunca pode ser usada como transação. A versão 1 do formato lista então os
signatários necessários e transporta a mensagem como texto UTF-8, sem limite de
tamanho.
A carteira monta esses bytes, assina-os e os devolve junto com a assinatura. A especificação cobre o formato completo, incluindo a versão 0 legada.
Escolha um método de login
Nem todas as carteiras suportam mensagens off-chain ainda. Verifique os recursos da conta conectada antes de solicitar uma assinatura e use a alternativa quando o recurso não existir:
- Se a conta suportar
solana:signOffchainMessagecom a versão 1 da mensagem, assine uma mensagem off-chain. - Caso contrário, assine o mesmo desafio com
solana:signMessage. A maioria das carteiras de software o suporta. - Ofereça aos usuários de carteiras de hardware sem suporte a mensagens uma opção explícita, como um botão "Estou usando uma carteira de hardware", que assina uma transação de login. Não os direcione por esse caminho por padrão.
Assine uma mensagem no navegador
Instale os pacotes do Wallet Standard junto com o Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
Esses auxiliares recebem um UiWalletAccount, como o account retornado por
useConnectedWallet no guia do 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 retorna signedOffchainMessage, os bytes completos que a
carteira assinou, e signature. Codifique ambos em Base64 e envie-os ao seu
servidor junto com o endereço da carteira.
Escreva o desafio de modo que fique legível em uma tela pequena. Informe o nome do seu aplicativo e o domínio, diga o que a assinatura faz e inclua um nonce de uso único e um horário de emissão gerados pelo seu servidor:
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
Verifique a assinatura no seu servidor
A verificação é uma checagem local da assinatura e não precisa de conexão RPC. Decodifique os bytes que a carteira assinou, confirme que correspondem ao desafio que você emitiu e depois verifique a assinatura:
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 lança um erro se o conteúdo ou os signatários
diferirem do esperado. verifyOffchainMessageEnvelope lança um erro se a
assinatura estiver ausente ou for inválida. Busque expectedMessage pelo nonce
no seu servidor, em vez de confiar na cópia que o cliente envia de volta.
Consuma o nonce assim que a verificação for bem-sucedida, para que a mesma
assinatura não possa autenticar duas vezes.
Verifique uma mensagem bruta
Uma assinatura de signMessage cobre diretamente os bytes do desafio, então o
cliente só precisa enviar a assinatura e o endereço da carteira. Verifique-a em
relação ao desafio que você emitiu:
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");}}
Aplicam-se as mesmas regras de nonce: busque expectedMessage pelo nonce e
consuma-o assim que a verificação for bem-sucedida.
@solana/kit reexporta esses auxiliares de @solana/offchain-messages. Use o
pacote independente se o seu servidor não depender do Kit.
Sign In With Solana
O Sign In With Solana (SIWS)
padroniza o próprio desafio. Seu aplicativo passa campos estruturados, como
domain, statement, nonce e issuedAt, ao recurso solana:signIn da
carteira. A carteira os formata em uma mensagem padrão, conecta, se necessário,
e a assina em uma única etapa.
A versão 1.1.0 de solana:signIn adiciona a opção useOffchainMessage.
Quando você passa useOffchainMessage: { messageVersion: 1 }, a carteira
envolve o texto SIWS em uma mensagem off-chain da versão 1, tendo a conta
assinante como único signatário necessário. As carteiras de hardware podem
exibi-la e assiná-la, e a saída informa
signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. As
carteiras que anunciam a versão 1.0.0 não conhecem a opção, portanto verifique
primeiro a versão do recurso e omita a opção caso contrário.
solana:signIn é um recurso da carteira, não da conta, portanto esses
auxiliares recebem um 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;}
Monte a entrada no seu servidor, com um nonce de uso único e um horário
issuedAt, e mantenha uma cópia indexada pelo nonce. Consuma o nonce assim que
o login for verificado.
Sem o Wallet Adapter, verifySignIn de @solana/wallet-standard-util verifica
apenas a saída SIWS simples. Quando signedMessageFormat indica uma mensagem
off-chain, signedMessage é uma mensagem off-chain cujo conteúdo é o texto SIWS.
Reconstrua o texto esperado a partir da entrada armazenada e do endereço
assinante com createSignInMessageText de @solana/wallet-standard-util e
depois verifique-o como qualquer outra mensagem off-chain, como em
Verifique a assinatura no seu servidor. O
README de @solana/offchain-messages
cobre esse fluxo no Kit.
Para carteiras
As carteiras adicionam suporte implementando o recurso solana:signOffchainMessage
do Wallet Standard, de @solana/wallet-standard-features:
- Informe
version: "1.0.0"e liste as versões de mensagem que você consegue assinar emsupportedMessageVersions. Hoje, isso é[1]. - Crie o preâmbulo da versão 1 a partir de
requiredSigners, que você deve ordenar e verificar quanto a duplicatas. Rejeite a solicitação se a conta não estiver entre os signatários obrigatórios. - Mostre ao usuário o texto da mensagem na íntegra antes de assinar, inclusive em dispositivos de hardware.
- Retorne os bytes exatos que você assinou como
signedOffchainMessage, junto com asignatureEd25519. - Para oferecer suporte ao Sign In With Solana off-chain, informe
version: "1.1.0"parasolana:signIn. Quando a entrada incluiruseOffchainMessage, assine o texto do SIWS como uma mensagem off-chain da versão 1, retorne os bytes dela comosignedMessagee definasignedMessageFormat. - Continue oferecendo suporte a
solana:signMessage. Ele não foi alterado, e os aplicativos ainda o usam como alternativa.
Is this page helpful?