Skip to main content
The message to sign is:
Clicker Swap Comment:

Chain: ${chain}
Token Address: ${contractAddress}
Txn Hash: ${txnHash}
Comment: ${commentText}
To assist you with testing and debugging your swap comment signatures, we’ve provided the entire signature-verification.ts that we use in our own backend services and test suite.
import { getBase58Codec, getBase58Decoder, signBytes } from '@solana/kit';
import { timingSafeEqual, verify } from 'crypto';
import { createWalletClient, http, recoverMessageAddress } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
import { z } from 'zod';

const solanaPrefixRegex = /^(solana:)/;
const EVMAddress = z
  .string()
  .regex(/^0x[a-zA-Z0-9]{40}$/, 'Must be an Ethereum address starting with 0x')
  .transform(a => a.toLowerCase())
  .openapi({ type: 'string' });


export const removeSolPrefixTransformer = (val: string) => val.replace(solanaPrefixRegex, '');
export const isValidEVMAddress = (value: string) => EVMAddress.safeParse(value).success;

export const SWAP_COMMENT_MESSAGE_PREFIX = 'Clicker Swap Comment:';

export const createSwapCommentMessage = (
  chain: string,
  tokenAddress: string,
  txnHash: string,
  commentText: string
): string => {
  // If you find yourself updating this, update the mintilfy docs page too!
  const contractAddress = isValidEVMAddress(tokenAddress) ? tokenAddress.toLowerCase() : tokenAddress;
  return `${SWAP_COMMENT_MESSAGE_PREFIX}\n\nChain: ${chain}\nToken Address: ${contractAddress}\nTxn Hash: ${txnHash}\nComment: ${commentText}`;
};

// This is a test helper function - in production, signing should happen client-side
export const signMessage = async (message: string, privateKey: string | CryptoKeyPair): Promise<{ signature: string; signerAddress: string }> => {
  if (typeof privateKey === 'object' && 'privateKey' in privateKey) {
    // Solana signing using CryptoKeyPair from @solana/kit
    const signedBytes = await signBytes(privateKey.privateKey, Buffer.from(message, 'utf-8'));
    const signature = getBase58Decoder().decode(signedBytes);
    const signerAddress = getBase58Decoder().decode(
      Buffer.from(await crypto.subtle.exportKey('raw', privateKey.publicKey))
    );
    
    return { signature, signerAddress };
  } else if (typeof privateKey === 'string' && privateKey.startsWith('0x')) {
    // EVM signing using viem
    const account = privateKeyToAccount(privateKey as `0x${string}`);
    const client = createWalletClient({
      account,
      chain: mainnet,
      transport: http(),
    });
    
    const signature = await client.signMessage({ message });
    
    return { signature, signerAddress: account.address };
  } else {
    throw new Error('Invalid private key format. Expected EVM private key (0x...) or Solana CryptoKeyPair');
  }
};

export const verifySwapCommentSignature = async (
  chain: string,
  tokenAddress: string,
  txnHash: string,
  commentText: string,
  signature: string,
  signerAddress: string
): Promise<boolean> => {
  try {
    const contractAddress = isValidEVMAddress(tokenAddress) ? tokenAddress.toLowerCase() : tokenAddress;
    const message = createSwapCommentMessage(chain, contractAddress, txnHash, commentText);

    if (signerAddress.startsWith('solana:')) {
      const publicKeyBytes = getBase58Codec().encode(removeSolPrefixTransformer(signerAddress));
      const signatureBytes = getBase58Codec().encode(signature);
      return verify(
        null, // No digest algorithm for Ed25519
        Buffer.from(message, 'utf-8'),
        {
          key: Buffer.concat([
            Buffer.from('302a300506032b6570032100', 'hex'), // ASN.1 DER prefix for Ed25519
            Buffer.from(publicKeyBytes),
          ]),
          format: 'der',
          type: 'spki',
        },
        Buffer.from(signatureBytes)
      );
    } else {
      const formattedSignature = signature.startsWith('0x') ? signature : `0x${signature}`;
      const recoveredAddress = await recoverMessageAddress({
        message,
        signature: formattedSignature as `0x${string}`,
      });

      // Convert addresses to Buffer for constant-time comparison
      const recoveredBuffer = Buffer.from(recoveredAddress.toLowerCase().slice(2), 'hex');
      const expectedBuffer = Buffer.from(signerAddress.toLowerCase().slice(2), 'hex');

      return (
        recoveredBuffer.length === expectedBuffer.length &&
        timingSafeEqual(recoveredBuffer, expectedBuffer)
      );
    }
  } catch (error) {
    console.error('Signature verification failed:', error);
    return false;
  }
};