Copy
Ask AI
Clicker Swap Comment:
Chain: ${chain}
Token Address: ${contractAddress}
Txn Hash: ${txnHash}
Comment: ${commentText}
signature-verification.ts that we use in our own backend services and test suite.
Copy
Ask AI
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;
}
};