Skip to main content

Overview

Cavos supports signing typed data in SNIP-12 format using the session key. This is useful for authentication challenges, off-chain order signing, and proof of ownership without spending gas.

Signing Typed Data

import { useCavos } from '@cavos/react';

function SignDemo() {
  const { signMessage, walletStatus } = useCavos();

  const handleSign = async () => {
    const typedData = {
      types: {
        StarknetDomain: [
          { name: 'name', type: 'shortstring' },
          { name: 'version', type: 'shortstring' },
          { name: 'chainId', type: 'shortstring' },
        ],
        Message: [
          { name: 'content', type: 'felt' },
        ],
      },
      primaryType: 'Message',
      domain: {
        name: 'MyApp',
        version: '1',
        chainId: '0x534e5f5345504f4c4941', // SN_SEPOLIA
      },
      message: {
        content: '0x48656c6c6f', // arbitrary felt value
      },
    };

    const signature = await signMessage(typedData);
    console.log('Signature:', signature);
    // ['0x<SESSION_V1_magic>', '0x<r>', '0x<s>', '0x<session_pubkey>']
  };

  return (
    <button onClick={handleSign} disabled={!walletStatus.isReady}>
      Sign Message
    </button>
  );
}

Return Format

signMessage() returns a string[] array, not an object:
const signature: string[] = await signMessage(typedData);
// [SESSION_V1_magic, r, s, session_key]
This format is directly compatible with the account contract’s is_valid_signature entrypoint — pass it as-is.
[!IMPORTANT] The signature is not a plain ECDSA { r, s } pair. It is a SESSION_V1 formatted array that includes the session key reference, required for on-chain verification.

Verifying On-Chain

The signed message can be verified on-chain using the SRC-5 is_valid_signature entrypoint on the wallet contract:
import { RpcProvider, hash } from 'starknet';

const provider = new RpcProvider({ nodeUrl: 'https://...' });

// Compute the message hash the same way the contract does
const msgHash = hash.computeHashOnElements([
  hash.getSelectorFromName('StarknetDomain'),
  // ... domain fields
]);

const result = await provider.callContract({
  contractAddress: walletAddress,
  entrypoint: 'is_valid_signature',
  calldata: [
    msgHash,
    signature.length.toString(),
    ...signature,
  ],
});

// Returns 'VALID' (as a felt: 0x56414c4944) if signature is valid
const isValid = result[0] === '0x56414c4944';

Verifying Off-Chain

To verify without a contract call, fetch the current session key from the account and verify the ECDSA component directly:
import { ec, hash } from 'starknet';

// The session public key is at index 3 of the signature array
const sessionPubKey = signature[3];
const r = signature[1];
const s = signature[2];

// Recompute the message hash
const msgHash = '0x...'; // same as above

const valid = ec.starkCurve.verify(
  { r: BigInt(r), s: BigInt(s) },
  msgHash,
  sessionPubKey,
);
Off-chain verification does not confirm that the session key is currently valid on-chain (e.g., not revoked or expired). Use on-chain verification for security-critical checks.

Use Cases

Authentication Challenge

Prove wallet ownership without a transaction:
// Backend generates a challenge
const challenge = crypto.randomUUID();

// User signs it
const signature = await signMessage({
  types: {
    StarknetDomain: [
      { name: 'name', type: 'shortstring' },
      { name: 'version', type: 'shortstring' },
      { name: 'chainId', type: 'shortstring' },
    ],
    Auth: [{ name: 'challenge', type: 'felt' }],
  },
  primaryType: 'Auth',
  domain: { name: 'MyApp', version: '1', chainId: '0x534e5f5345504f4c4941' },
  message: { challenge: `0x${Buffer.from(challenge).toString('hex')}` },
});

// Backend verifies via is_valid_signature

Off-Chain Order Signing

DEXs and NFT marketplaces use off-chain signatures to allow users to place orders or list assets without paying gas:
const orderSignature = await signMessage({
  types: {
    StarknetDomain: [...],
    Order: [
      { name: 'token_in', type: 'felt' },
      { name: 'token_out', type: 'felt' },
      { name: 'amount', type: 'u256' },
      { name: 'price', type: 'u256' },
    ],
  },
  primaryType: 'Order',
  domain: { name: 'MyDEX', version: '1', chainId: '...' },
  message: {
    token_in: TOKEN_A,
    token_out: TOKEN_B,
    amount: { low: amountLow, high: '0' },
    price: { low: priceLow, high: '0' },
  },
});