Sign Message
Sign arbitrary messages with a user’s embedded Solana wallet. Message signing is commonly used for authentication, proof of ownership, and off-chain authorization.
Overview
Solana message signing allows users to cryptographically sign messages using their wallet’s private key. This enables:
Proof of ownership : Verify control of a Solana address
Authentication : Sign messages to prove identity
Off-chain actions : Authorize actions without transaction fees
Session tokens : Create authenticated session credentials
Unlike Ethereum which uses personal_sign, Solana uses the signMessage method from the Solana Wallet Standard.
React SDK
Basic Usage
With Callbacks
With UI Customization
To sign a message, use the signMessage method from the useSignMessage hook: import { useMoonKey } from '@moon-key/react-auth' ;
import { useSignMessage } from '@moon-key/react-auth/solana' ;
import bs58 from 'bs58' ;
function SignMessageButton () {
const { user } = useMoonKey ();
const { signMessage } = useSignMessage ();
const handleSign = async () => {
if ( ! user ?. wallet ) {
alert ( 'No wallet found' );
return ;
}
const message = 'Hello from MoonKey on Solana!' ;
try {
const result = await signMessage ({
message: new TextEncoder (). encode ( message ),
wallet: user . wallet
});
// Convert Uint8Array signature to base58 string
const signature = bs58 . encode ( result . signature );
console . log ( 'Message signed:' , signature );
alert ( `Signature: ${ signature } ` );
} catch ( error ) {
console . error ( 'Failed to sign message:' , error );
}
};
return (
< button onClick = { handleSign } >
Sign Message
</ button >
);
}
Parameters
The message to sign as a Uint8Array. Use TextEncoder to convert strings to Uint8Array. Example: const message = new TextEncoder (). encode ( 'Hello, Solana!' );
The Solana wallet to use for signing. Example:
Options Object
The second parameter is an optional configuration object:
Configuration for the signing confirmation modal. Custom title for the confirmation modal.
Description text shown in the modal.
Text for the confirm button.
Text for the cancel button.
Whether to show the confirmation modal. Set to false to sign without confirmation.
Hook Callbacks
onSuccess
(result: { signature: Uint8Array; signedMessage: Uint8Array }) => void
Callback executed after successful signing. Receives the signature and signed message as Uint8Array.
Callback executed if signing fails or user cancels.
Returns
The signature produced by the wallet as a Uint8Array. Use bs58.encode() to convert to a base58 string.
The original message that was signed.
Message Encoding
String to Uint8Array
Convert string messages to Uint8Array using TextEncoder:
// Simple string message
const message = 'Hello, Solana!' ;
const encodedMessage = new TextEncoder (). encode ( message );
await signMessage ({
message: encodedMessage ,
wallet: user . wallet
});
Structured Messages
For structured authentication messages:
const authMessage = `
Welcome to My dApp!
Sign this message to verify your wallet ownership.
Wallet: ${ user . wallet . address }
Nonce: ${ nonce }
Timestamp: ${ Date . now () }
` ;
const encodedMessage = new TextEncoder (). encode ( authMessage );
const result = await signMessage ({
message: encodedMessage ,
wallet: user . wallet
});
Signature Encoding
Convert to Base58
Solana signatures are typically represented as base58 strings:
import bs58 from 'bs58' ;
const result = await signMessage ({
message: new TextEncoder (). encode ( 'Hello!' ),
wallet: user . wallet
});
// Convert Uint8Array to base58 string
const signature = bs58 . encode ( result . signature );
console . log ( 'Signature:' , signature );
Install bs58
Complete Examples
User Authentication
'use client' ;
import { useMoonKey } from '@moon-key/react-auth' ;
import { useSignMessage } from '@moon-key/react-auth/solana' ;
import bs58 from 'bs58' ;
import { useState } from 'react' ;
export default function SolanaAuth () {
const { user , isAuthenticated } = useMoonKey ();
const [ isVerifying , setIsVerifying ] = useState ( false );
const { signMessage } = useSignMessage ({
onSuccess : async ({ signature }) => {
const sig = bs58 . encode ( signature );
try {
// Verify signature on backend
const response = await fetch ( '/api/auth/verify-solana' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
address: user ?. wallet ?. address ,
signature: sig ,
message: authMessage
})
});
if ( response . ok ) {
alert ( 'Authentication successful!' );
} else {
alert ( 'Authentication failed' );
}
} catch ( error ) {
console . error ( 'Verification failed:' , error );
} finally {
setIsVerifying ( false );
}
},
onError : ( error ) => {
console . error ( 'Signing failed:' , error );
setIsVerifying ( false );
}
});
const handleAuthenticate = async () => {
if ( ! user ?. wallet ) {
alert ( 'Please connect your wallet first' );
return ;
}
setIsVerifying ( true );
const nonce = Math . random (). toString ( 36 ). substring ( 7 );
const authMessage = `
Welcome to My Solana dApp!
Please sign this message to authenticate.
Wallet: ${ user . wallet . address }
Nonce: ${ nonce }
Timestamp: ${ Date . now () }
` . trim ();
await signMessage ({
message: new TextEncoder (). encode ( authMessage ),
wallet: user . wallet
}, {
uiConfig: {
title: 'Authenticate' ,
description: 'Sign this message to verify your wallet ownership' ,
confirmButtonText: 'Sign & Authenticate'
}
});
};
if ( ! isAuthenticated ) {
return < p > Please sign in first </ p > ;
}
return (
< div className = "solana-auth" >
< h2 > Solana Authentication </ h2 >
< p > Wallet: { user ?. wallet ?. address } </ p >
< button
onClick = { handleAuthenticate }
disabled = { isVerifying }
>
{ isVerifying ? 'Verifying...' : 'Authenticate with Solana' }
</ button >
</ div >
);
}
Session Authorization
'use client' ;
import { useMoonKey } from '@moon-key/react-auth' ;
import { useSignMessage } from '@moon-key/react-auth/solana' ;
import bs58 from 'bs58' ;
export default function SessionAuth () {
const { user } = useMoonKey ();
const { signMessage } = useSignMessage ();
const createSession = async () => {
if ( ! user ?. wallet ) return ;
const sessionMessage = `
Create Session
This signature will be used to authenticate your requests.
Wallet: ${ user . wallet . address }
Expires: ${ new Date ( Date . now () + 86400000 ). toISOString () }
Session ID: ${ crypto . randomUUID () }
` . trim ();
try {
const result = await signMessage ({
message: new TextEncoder (). encode ( sessionMessage ),
wallet: user . wallet
}, {
uiConfig: {
title: 'Create Session' ,
description: 'Sign to create an authenticated session (valid for 24 hours)' ,
confirmButtonText: 'Create Session'
}
});
const signature = bs58 . encode ( result . signature );
// Create session on backend
const response = await fetch ( '/api/sessions/create' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
address: user . wallet . address ,
signature ,
message: sessionMessage
})
});
const { sessionToken } = await response . json ();
// Store session token
localStorage . setItem ( 'sessionToken' , sessionToken );
alert ( 'Session created successfully!' );
} catch ( error ) {
console . error ( 'Session creation failed:' , error );
}
};
return (
< button onClick = { createSession } >
Create Authenticated Session
</ button >
);
}
Proof of Ownership
'use client' ;
import { useMoonKey } from '@moon-key/react-auth' ;
import { useSignMessage } from '@moon-key/react-auth/solana' ;
import bs58 from 'bs58' ;
import { useState } from 'react' ;
export default function ProofOfOwnership () {
const { user } = useMoonKey ();
const { signMessage } = useSignMessage ();
const [ proof , setProof ] = useState <{ address : string ; signature : string } | null >( null );
const generateProof = async () => {
if ( ! user ?. wallet ) return ;
const timestamp = Date . now ();
const proofMessage = `
I am the owner of this Solana wallet.
Address: ${ user . wallet . address }
Timestamp: ${ timestamp }
Challenge: ${ crypto . randomUUID () }
` . trim ();
try {
const result = await signMessage ({
message: new TextEncoder (). encode ( proofMessage ),
wallet: user . wallet
}, {
uiConfig: {
title: 'Prove Ownership' ,
description: 'Sign to prove you own this Solana wallet' ,
confirmButtonText: 'Generate Proof'
}
});
const signature = bs58 . encode ( result . signature );
setProof ({
address: user . wallet . address ,
signature
});
console . log ( 'Proof generated:' , { address: user . wallet . address , signature });
} catch ( error ) {
console . error ( 'Proof generation failed:' , error );
}
};
return (
< div className = "proof-of-ownership" >
< h2 > Proof of Wallet Ownership </ h2 >
< button onClick = { generateProof } >
Generate Proof
</ button >
{ proof && (
< div className = "proof-result" >
< h3 > Ownership Proof </ h3 >
< div className = "proof-details" >
< p >< strong > Address: </ strong ></ p >
< code > { proof . address } </ code >
< p >< strong > Signature: </ strong ></ p >
< code className = "signature" > { proof . signature } </ code >
</ div >
< button onClick = { () => {
navigator . clipboard . writeText ( JSON . stringify ( proof , null , 2 ));
alert ( 'Proof copied to clipboard!' );
} } >
Copy Proof
</ button >
</ div >
) }
</ div >
);
}
Verifying Signatures
Client-side Verification
import { PublicKey } from '@solana/web3.js' ;
import nacl from 'tweetnacl' ;
import bs58 from 'bs58' ;
async function verifySolanaSignature (
message : string ,
signature : string ,
publicKey : string
) : Promise < boolean > {
try {
// Convert message to Uint8Array
const messageBytes = new TextEncoder (). encode ( message );
// Convert signature from base58 to Uint8Array
const signatureBytes = bs58 . decode ( signature );
// Convert public key to Uint8Array
const publicKeyBytes = new PublicKey ( publicKey ). toBytes ();
// Verify signature
const isValid = nacl . sign . detached . verify (
messageBytes ,
signatureBytes ,
publicKeyBytes
);
return isValid ;
} catch ( error ) {
console . error ( 'Verification failed:' , error );
return false ;
}
}
// Usage
const isValid = await verifySolanaSignature (
'Hello, Solana!' ,
signature ,
walletAddress
);
console . log ( 'Signature valid:' , isValid );
Install Dependencies
npm install @solana/web3.js tweetnacl bs58
UI Customization
Customize the signing modal:
await signMessage ({
message: new TextEncoder (). encode ( 'Sign this message' ),
wallet: user . wallet
}, {
uiConfig: {
title: 'Sign Message' ,
description: 'Please sign this message to continue' ,
confirmButtonText: 'Sign' ,
cancelButtonText: 'Cancel'
}
});
Hide Modal
await signMessage ({
message: new TextEncoder (). encode ( 'Background signature' ),
wallet: user . wallet
}, {
uiConfig: {
showWalletUI: false
}
});
Hiding the modal removes the user’s ability to review the message before signing. Only use this for trusted operations or after obtaining explicit user consent.
Best Practices
Include context in messages
Always provide clear context about what the user is signing: // ✅ Good - Clear and informative
const message = `
Welcome to My dApp!
By signing this message, you verify ownership of your wallet.
Wallet: ${ address }
Nonce: ${ nonce }
Timestamp: ${ timestamp }
` . trim ();
// ❌ Bad - Vague and unclear
const message = 'Sign this' ;
Use nonces to prevent replay attacks
Include a unique nonce in each message: const nonce = crypto . randomUUID ();
const message = `
Action: Login
Nonce: ${ nonce }
Timestamp: ${ Date . now () }
` . trim ();
// Verify nonce hasn't been used before
await verifyNonce ( nonce );
Add timestamps for expiration
Include timestamps to make signatures time-limited: const timestamp = Date . now ();
const message = `
Sign to authenticate
Timestamp: ${ timestamp }
Valid for: 5 minutes
` . trim ();
// On backend, check if signature is within valid time window
const fiveMinutes = 5 * 60 * 1000 ;
if ( Date . now () - timestamp > fiveMinutes ) {
throw new Error ( 'Signature expired' );
}
Store message with signature
Keep the original message to verify later: const result = await signMessage ({
message: new TextEncoder (). encode ( messageText ),
wallet: user . wallet
});
// Store both for verification
const proof = {
message: messageText ,
signature: bs58 . encode ( result . signature ),
address: user . wallet . address ,
timestamp: Date . now ()
};
await saveToDatabase ( proof );
Provide clear error messages: const { signMessage } = useSignMessage ({
onError : ( error : any ) => {
if ( error . code === 'USER_REJECTED' ) {
console . log ( 'User cancelled signing' );
return ;
} else if ( error . message ?. includes ( 'wallet' )) {
alert ( 'Wallet error. Please check your connection.' );
} else {
alert ( 'Failed to sign message: ' + error . message );
}
}
});
Troubleshooting
Signature verification fails
Common causes:
Message mismatch : Ensure exact same message is used for verification
Encoding issues : Use consistent encoding (TextEncoder for strings)
Signature format : Ensure signature is properly base58 encoded/decoded
Public key format : Verify public key is valid Solana address
// Ensure exact message match
const originalMessage = 'Hello, Solana!' ;
const messageBytes = new TextEncoder (). encode ( originalMessage );
// Sign
const result = await signMessage ({
message: messageBytes ,
wallet: user . wallet
});
// Verify with SAME original message
const isValid = await verifySolanaSignature (
originalMessage , // Use original, not messageBytes
bs58 . encode ( result . signature ),
user . wallet . address
);
In Node.js environments, you may need to polyfill: // For Node.js < 11
import { TextEncoder } from 'util' ;
// Or use buffer
const messageBytes = Buffer . from ( 'Hello, Solana!' , 'utf-8' );
Ensure bs58 is properly installed: npm install bs58
npm install --save-dev @types/bs58
// CommonJS
const bs58 = require ( 'bs58' );
// ES6
import bs58 from 'bs58' ;
Configure callbacks on hook initialization: // ✅ Correct
const { signMessage } = useSignMessage ({
onSuccess : ({ signature }) => {
console . log ( 'Success:' , bs58 . encode ( signature ));
},
onError : ( error ) => {
console . log ( 'Error:' , error );
}
});
// Then call it
await signMessage ({ message , wallet });
Next Steps