Assinar uma mensagem prova que um 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 assinado por carteiras de hardware. - Mensagens brutas (
solana:signMessage) assinam exatamente os bytes que o seu aplicativo envia, sem prefixo e sem signatários declarados. A maioria das carteiras de software as suporta, mas nada nos bytes as identifica como uma mensagem em vez 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 sem efeito. Isso funciona, mas acrescenta latência, custo e 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.
- 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 os bytes de uma transação, não uma declaração do que está aceitando.
- Risco comportamental. Pedir aos usuários que assinem uma transação apenas para entrar os torna mais fáceis de enganar para assinarem uma transação maliciosa.
Uma mensagem off-chain evita tudo isso. A única ida e volta é ao 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 estiver disponível:
- Se a conta suportar
solana:signOffchainMessagecom a versão de mensagem 1, 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 de adesão, como um interruptor "Estou usando uma carteira de hardware", que assine uma transação de login em vez disso. Não os encaminhe 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 utilitários recebem um UiWalletAccount, como o account retornado por
useConnectedWallet no guia de 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 com
o endereço da carteira.
Escreva o desafio de modo que seja bem legível em uma tela pequena. Indique o nome do seu aplicativo e o domínio, explique o que a assinatura faz e inclua um nonce de uso único e um horário de emissão vindo do 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, em seguida, 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 devolve. Consuma o nonce assim que ele for
verificado, para que a mesma assinatura não possa ser usada para entrar duas vezes.
Verifique uma mensagem bruta
Uma assinatura de signMessage cobre diretamente os bytes do desafio, portanto o cliente
precisa enviar apenas a assinatura e o endereço da carteira. Compare-a com o
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 ele for verificado.
O @solana/kit reexporta esses utilitários 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. O seu aplicativo passa campos estruturados, como
domain, statement, nonce e issuedAt, para o recurso solana:signIn da carteira.
A carteira os formata em uma mensagem padrão, conecta-se, se necessário,
e a assina em uma única etapa.
A versão 1.1.0 de solana:signIn adiciona uma 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 que assina 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
utilitários 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 guarde 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 que assinou com
createSignInMessageText de @solana/wallet-standard-util e, em seguida, verifique-o
como qualquer outra mensagem off-chain, conforme descrito 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ê pode assinar emsupportedMessageVersions. Atualmente, isso é[1]. - Crie o preâmbulo da versão 1 a partir de
requiredSigners, que deve ser ordenado e verificado quanto a duplicados. Rejeite a solicitação se a conta não for um dos signatários obrigatórios. - Mostre o texto da mensagem ao usuário na íntegra antes da assinatura, inclusive em dispositivos de hardware.
- Retorne os bytes exatos que você assinou como
signedOffchainMessage, juntamente 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 SIWS como uma mensagem off-chain da versão 1, retorne os respectivos bytes comosignedMessagee definasignedMessageFormat. - Continue oferecendo suporte a
solana:signMessage. Ele permanece inalterado e os apps ainda o utilizam como alternativa.
Is this page helpful?