Skip to main content

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

message
Uint8Array
required
The message to sign as a Uint8Array. Use TextEncoder to convert strings to Uint8Array.Example:
const message = new TextEncoder().encode('Hello, Solana!');
wallet
Wallet
required
The Solana wallet to use for signing.Example:
wallet: user.wallet

Options Object

The second parameter is an optional configuration object:
uiConfig
object
Configuration for the signing confirmation modal.

Hook Callbacks

onSuccess
(result: { signature: Uint8Array; signedMessage: Uint8Array }) => void
Callback executed after successful signing. Receives the signature and signed message as Uint8Array.
onError
(error: Error) => void
Callback executed if signing fails or user cancels.

Returns

signature
Uint8Array
The signature produced by the wallet as a Uint8Array. Use bs58.encode() to convert to a base58 string.
signedMessage
Uint8Array
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

npm 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

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';
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);
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');
}
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

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