Firmar un mensaje demuestra que un usuario controla una billetera, sin transacción, sin comisión y sin llamadas a la red. Las aplicaciones usan mensajes firmados para iniciar sesión, registrar que un usuario aceptó los términos, vincular una billetera a una cuenta existente o autorizar una acción que ocurre fuera de la cadena, como realizar un pedido.
Las billeteras de Solana firman mensajes de dos maneras:
- Mensajes fuera de la cadena (
solana:signOffchainMessage) envuelven tu texto en un formato estándar, definido por la especificación de firma de mensajes fuera de la cadena (OCMS). El formato nunca puede confundirse con una transacción, indica las cuentas que deben firmarlo y es el formato que firman las billeteras de hardware. - Mensajes sin procesar (
solana:signMessage) firman exactamente los bytes que tu aplicación les pasa, sin prefijo y sin firmantes declarados. La mayoría de las billeteras de software los admiten, pero nada en los bytes los marca como un mensaje en lugar de una transacción.
Usa mensajes fuera de la cadena cuando la billetera los admita y recurre a los mensajes sin procesar cuando no. En el resto de esta guía se usa el inicio de sesión como ejemplo práctico, pero los mismos pasos de firma y verificación se aplican a cualquier mensaje.
Elige un formato de mensaje
| Mensaje fuera de la cadena | Mensaje sin procesar | Transacción | |
|---|---|---|---|
| Función de la billetera | signOffchainMessage | signMessage | signTransaction |
| Compatibilidad con billeteras de hardware | Depende del dispositivo | No | Sí |
| Legible para el usuario | Sí | Solo en software | Depende del dispositivo |
| Declara los firmantes requeridos | Sí | No | Sí |
| Llamadas RPC y comisiones | Ninguna | Ninguna | Blockhash, envío, comisión, confirmación |
¿Por qué no una transacción?
Antes, las billeteras de hardware no tenían forma de firmar un mensaje legible. Muchas aplicaciones optaban por pedir a estos usuarios que firmaran una transacción sin efecto. Funciona, pero añade latencia, costo y riesgo:
- Comisiones de red. Una transacción paga una comisión, incluso cuando no hace nada.
- Múltiples llamadas RPC. Tu aplicación obtiene un blockhash, simula, envía y luego consulta repetidamente la confirmación.
- Latencia adicional. Cada una de esas llamadas es un viaje de ida y vuelta antes de que el usuario inicie sesión.
- Nada legible. El usuario aprueba los bytes de una transacción, no una declaración de aquello con lo que está de acuerdo.
- Riesgo de comportamiento. Pedir a los usuarios que firmen una transacción solo para iniciar sesión facilita engañarlos para que firmen una dañina.
Un mensaje fuera de la cadena evita todo eso. El único viaje de ida y vuelta es hacia tu propio servidor.
Qué firma la billetera
Un mensaje fuera de la cadena comienza con un dominio de firma fijo,
\xffsolana offchain. Ninguna transacción válida empieza con esos bytes, por lo
que un mensaje firmado nunca puede usarse como transacción. La versión 1 del
formato enumera a continuación los firmantes requeridos y transporta el mensaje
como texto UTF-8, sin límite de longitud.
La billetera construye estos bytes, los firma y los devuelve junto con la firma. La especificación cubre el formato completo, incluida la versión 0 heredada.
Elige un método de inicio de sesión
Aún no todas las billeteras admiten mensajes fuera de la cadena. Comprueba las funciones de la cuenta conectada antes de solicitar una firma y recurre a la alternativa cuando falte la función:
- Si la cuenta admite
solana:signOffchainMessagecon la versión 1 del mensaje, firma un mensaje fuera de la cadena. - De lo contrario, firma el mismo desafío con
solana:signMessage. La mayoría de las billeteras de software lo admiten. - Ofrece a los usuarios de billeteras de hardware sin compatibilidad con mensajes una opción explícita, como un interruptor «Estoy usando una billetera de hardware», que firme en su lugar una transacción de inicio de sesión. No los dirijas por este camino de forma predeterminada.
Firma un mensaje en el navegador
Instala los paquetes de Wallet Standard junto con Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
Estas utilidades reciben un UiWalletAccount, como el account que devuelve
useConnectedWallet en la guía 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 devuelve signedOffchainMessage, los bytes completos que
firmó la billetera, y signature. Codifica ambos en Base64 y envíalos a tu
servidor junto con la dirección de la billetera.
Redacta el desafío de modo que se lea bien en una pantalla pequeña. Indica el nombre de tu aplicación y su dominio, explica qué hace la firma e incluye un nonce de un solo uso y una hora de emisión generada por tu 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
Verifica la firma en tu servidor
La verificación es una comprobación local de la firma y no necesita conexión RPC. Decodifica los bytes que firmó la billetera, confirma que coinciden con el desafío que emitiste y luego comprueba 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 lanza un error si el contenido o los firmantes
difieren de lo que esperabas. verifyOffchainMessageEnvelope lanza un error si
la firma falta o no es válida. Busca expectedMessage por nonce en tu servidor
en lugar de confiar en la copia que devuelve el cliente. Consume el nonce una
vez verificado, para que la misma firma no pueda iniciar sesión dos veces.
Verifica un mensaje sin procesar
Una firma de signMessage cubre directamente los bytes del desafío, así que el
cliente solo necesita enviar la firma y la dirección de la billetera.
Comprúebala frente al desafío que emitiste:
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");}}
Se aplican las mismas reglas de nonce: busca expectedMessage por nonce y
consúmelo una vez verificado.
@solana/kit reexporta estas utilidades desde @solana/offchain-messages. Usa
el paquete independiente si tu servidor no depende de Kit.
Sign In With Solana
Sign In With Solana (SIWS)
estandariza el propio desafío. Tu aplicación pasa campos estructurados, como
domain, statement, nonce e issuedAt, a la función solana:signIn de la
billetera. La billetera los formatea en un mensaje estándar, se conecta si es
necesario y lo firma en un solo paso.
La versión 1.1.0 de solana:signIn añade la opción useOffchainMessage.
Cuando pasas useOffchainMessage: { messageVersion: 1 }, la billetera envuelve
el texto SIWS en un mensaje fuera de la cadena de la versión 1, con la cuenta
que firma como único firmante requerido. Las billeteras de hardware pueden
mostrarlo y firmarlo, y el resultado informa
signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Las
billeteras que anuncian la versión 1.0.0 no conocen la opción, así que
comprueba primero la versión de la función y omítela en caso contrario.
solana:signIn es una función de la billetera, no de la cuenta, por lo que
estas utilidades reciben 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;}
Construye la entrada en tu servidor, con un nonce de un solo uso y una hora
issuedAt, y conserva una copia indexada por el nonce. Consume el nonce una vez
que el inicio de sesión se verifique.
Sin Wallet Adapter, verifySignIn de @solana/wallet-standard-util comprueba
solo la salida SIWS simple. Cuando signedMessageFormat indica un mensaje fuera
de la cadena, signedMessage es un mensaje fuera de la cadena cuyo contenido es
el texto SIWS. Reconstruye el texto esperado a partir de la entrada almacenada y
la dirección firmante con createSignInMessageText de
@solana/wallet-standard-util y luego verifícalo como cualquier otro mensaje
fuera de la cadena, como en
Verifica la firma en tu servidor. El
README de @solana/offchain-messages
cubre este flujo en Kit.
Para billeteras
Las billeteras añaden compatibilidad implementando la función de Wallet
Standard solana:signOffchainMessage de @solana/wallet-standard-features:
- Informa
version: "1.0.0"y enumera las versiones de mensaje que puedes firmar ensupportedMessageVersions. Hoy, eso es[1]. - Construye el preámbulo de la versión 1 a partir de
requiredSigners, que debes ordenar y revisar para detectar duplicados. Rechaza la solicitud si la cuenta no es uno de los firmantes requeridos. - Muestra al usuario el texto del mensaje completo antes de firmar, también en dispositivos de hardware.
- Devuelve los bytes exactos que firmaste como
signedOffchainMessage, junto con lasignatureEd25519. - Para admitir Sign In With Solana fuera de la cadena, indica
version: "1.1.0"parasolana:signIn. Cuando la entrada incluyauseOffchainMessage, firma el texto de SIWS como un mensaje fuera de la cadena de la versión 1, devuelve sus bytes comosignedMessagey establecesignedMessageFormat. - Sigue admitiendo
solana:signMessage. No ha cambiado, y las aplicaciones aún lo usan como alternativa.
Is this page helpful?