Address validation
Validate and format Stacks addresses and principals
Overview
Stacks addresses follow specific formats that differ between mainnet and testnet. Proper validation ensures your application handles addresses correctly, preventing loss of funds and improving user experience. This guide covers address validation, formatting, and conversion utilities.
Basic address validation
Validate Stacks addresses:
import {validateStacksAddress,validateContractName} from '@stacks/transactions';// Validate standard addressesconst isValidMainnet = validateStacksAddress('SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY');console.log('Valid mainnet:', isValidMainnet); // trueconst isValidTestnet = validateStacksAddress('ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC');console.log('Valid testnet:', isValidTestnet); // trueconst isInvalid = validateStacksAddress('invalid-address');console.log('Valid:', isInvalid); // false// Validate contract namesconst validContract = validateContractName('my-contract');console.log('Valid contract name:', validContract); // trueconst invalidContract = validateContractName('My Contract!');console.log('Valid contract name:', invalidContract); // false
Address types and detection
Identify address types and networks:
import {getAddressFromPrivateKey,getAddressFromPublicKey,TransactionVersion} from '@stacks/transactions';// Detect address type from prefixfunction getAddressInfo(address: string): {type: 'standard' | 'contract' | 'multisig' | 'invalid';network: 'mainnet' | 'testnet' | 'unknown';} {if (!validateStacksAddress(address)) {return { type: 'invalid', network: 'unknown' };}// Mainnet prefixesif (address.startsWith('SP')) {return { type: 'standard', network: 'mainnet' };} else if (address.startsWith('SM')) {return { type: 'multisig', network: 'mainnet' };}// Testnet prefixeselse if (address.startsWith('ST')) {return { type: 'standard', network: 'testnet' };} else if (address.startsWith('SN')) {return { type: 'multisig', network: 'testnet' };}// Contract address (contains .)if (address.includes('.')) {const [principal] = address.split('.');const info = getAddressInfo(principal);return { ...info, type: 'contract' };}return { type: 'invalid', network: 'unknown' };}// Usageconst info = getAddressInfo('SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY.my-contract');console.log(info); // { type: 'contract', network: 'mainnet' }
Address generation
Generate addresses from keys:
import {makeRandomPrivKey,getPublicKey,getAddressFromPrivateKey,getAddressFromPublicKey,TransactionVersion,AddressHashMode} from '@stacks/transactions';// Generate new random addressfunction generateNewAddress(network: 'mainnet' | 'testnet') {const privateKey = makeRandomPrivKey();const publicKey = getPublicKey(privateKey);const version = network === 'mainnet'? TransactionVersion.Mainnet: TransactionVersion.Testnet;const address = getAddressFromPrivateKey(privateKey, version);return {privateKey,publicKey,address,};}// Generate address from existing private keyfunction getAddressFromKey(privateKey: string, network: 'mainnet' | 'testnet') {const version = network === 'mainnet'? TransactionVersion.Mainnet: TransactionVersion.Testnet;return getAddressFromPrivateKey(privateKey, version);}// Generate multisig addressfunction generateMultisigAddress(publicKeys: string[],signaturesRequired: number,network: 'mainnet' | 'testnet') {const version = network === 'mainnet'? TransactionVersion.Mainnet: TransactionVersion.Testnet;const hashMode = AddressHashMode.SerializeP2SH;// Implementation depends on multisig setup// This is a simplified examplereturn getAddressFromPublicKey(publicKeys[0], // Simplified - real implementation needs all keysversion,hashMode);}
Contract address handling
Work with contract principals:
// Parse contract address componentsfunction parseContractAddress(contractAddress: string): {principal: string;contractName: string;isValid: boolean;} {const parts = contractAddress.split('.');if (parts.length !== 2) {return { principal: '', contractName: '', isValid: false };}const [principal, contractName] = parts;const isValid = validateStacksAddress(principal) &&validateContractName(contractName);return { principal, contractName, isValid };}// Build contract addressfunction buildContractAddress(principal: string, contractName: string): string {if (!validateStacksAddress(principal)) {throw new Error('Invalid principal address');}if (!validateContractName(contractName)) {throw new Error('Invalid contract name');}return `${principal}.${contractName}`;}// Validate full contract identifierfunction validateContractAddress(address: string): boolean {const { isValid } = parseContractAddress(address);return isValid;}// Usageconst parsed = parseContractAddress('SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY.my-token');console.log(parsed);// { principal: 'SP2J6...', contractName: 'my-token', isValid: true }
Address conversion utilities
Convert between formats and networks:
import { c32addressDecode, c32address } from 'c32check';// Convert between testnet and mainnet addressesfunction convertAddressNetwork(address: string,toNetwork: 'mainnet' | 'testnet'): string {try {// Decode the addressconst decoded = c32addressDecode(address);// Determine new versionlet newVersion: number;if (toNetwork === 'mainnet') {newVersion = decoded[0] === 26 ? 22 : 20; // Multi-sig or standard} else {newVersion = decoded[0] === 22 ? 26 : 21; // Multi-sig or standard}// Re-encode with new versionconst newAddress = c32address(newVersion, decoded[1]);return newAddress;} catch (error) {throw new Error('Invalid address format');}}// Extract address hashfunction getAddressHash(address: string): string {const decoded = c32addressDecode(address);return Buffer.from(decoded[1]).toString('hex');}// Check if addresses are same (ignoring network)function isSameAddress(addr1: string, addr2: string): boolean {try {const hash1 = getAddressHash(addr1);const hash2 = getAddressHash(addr2);return hash1 === hash2;} catch {return false;}}
Advanced validation patterns
Comprehensive address validator
Create a robust validation system:
class AddressValidator {private cache = new Map<string, boolean>();validate(address: string, options?: {network?: 'mainnet' | 'testnet';allowContracts?: boolean;allowMultisig?: boolean;}): { valid: boolean; reason?: string } {// Check cacheconst cacheKey = `${address}-${JSON.stringify(options)}`;if (this.cache.has(cacheKey)) {return { valid: this.cache.get(cacheKey)! };}// Basic validationif (!validateStacksAddress(address)) {return { valid: false, reason: 'Invalid address format' };}const info = getAddressInfo(address);// Check network if specifiedif (options?.network && info.network !== options.network) {return {valid: false,reason: `Address is for ${info.network}, expected ${options.network}`};}// Check contract addressesif (info.type === 'contract' && !options?.allowContracts) {return { valid: false, reason: 'Contract addresses not allowed' };}// Check multisigif (info.type === 'multisig' && !options?.allowMultisig) {return { valid: false, reason: 'Multisig addresses not allowed' };}// Cache resultthis.cache.set(cacheKey, true);return { valid: true };}validateBatch(addresses: string[], options?: any): Map<string, boolean> {const results = new Map<string, boolean>();for (const address of addresses) {const { valid } = this.validate(address, options);results.set(address, valid);}return results;}}
Address formatting
Format addresses for display:
function formatAddress(address: string,options?: {truncate?: boolean;length?: number;separator?: string;}): string {if (!validateStacksAddress(address)) {return 'Invalid Address';}if (options?.truncate) {const length = options.length || 8;const start = address.slice(0, length);const end = address.slice(-length);const separator = options.separator || '...';return `${start}${separator}${end}`;}return address;}// Format for display with copy functionalityfunction AddressDisplay({ address }: { address: string }) {const [copied, setCopied] = useState(false);const formatted = formatAddress(address, {truncate: true,length: 6});const copyToClipboard = () => {navigator.clipboard.writeText(address);setCopied(true);setTimeout(() => setCopied(false), 2000);};return (<div className="address-display" onClick={copyToClipboard}><code>{formatted}</code>{copied && <span>✓ Copied</span>}</div>);}
Input validation hooks
React hooks for address inputs:
import { useState, useCallback } from 'react';function useAddressInput(options?: {network?: 'mainnet' | 'testnet';allowContracts?: boolean;}) {const [value, setValue] = useState('');const [error, setError] = useState<string | null>(null);const [isValid, setIsValid] = useState(false);const validate = useCallback((address: string) => {if (!address) {setError(null);setIsValid(false);return;}const validator = new AddressValidator();const result = validator.validate(address, options);setError(result.reason || null);setIsValid(result.valid);}, [options]);const handleChange = useCallback((newValue: string) => {setValue(newValue);validate(newValue);}, [validate]);return {value,error,isValid,setValue: handleChange,validate,};}// Usage in componentfunction AddressInput() {const address = useAddressInput({network: 'mainnet',allowContracts: false});return (<div><inputvalue={address.value}onChange={(e) => address.setValue(e.target.value)}placeholder="Enter Stacks address"className={address.error ? 'error' : ''}/>{address.error && (<span className="error-message">{address.error}</span>)}</div>);}
Security considerations
Implement secure address handling:
// Sanitize user inputfunction sanitizeAddress(input: string): string {// Remove whitespace and common separatorsreturn input.trim().replace(/[\s\-_]/g, '');}// Verify address ownershipasync function verifyAddressOwnership(address: string,signature: string,message: string): Promise<boolean> {try {// Verify the signature matches the addressconst verified = verifyMessageSignature({message,signature,publicKey: await getPublicKeyFromAddress(address),});return verified;} catch {return false;}}// Validate address for specific use casefunction validateRecipientAddress(address: string,options: {blockList?: string[];allowList?: string[];requireMainnet?: boolean;}): { valid: boolean; reason?: string } {// Check blocklistif (options.blockList?.includes(address)) {return { valid: false, reason: 'Address is blocked' };}// Check allowlistif (options.allowList && !options.allowList.includes(address)) {return { valid: false, reason: 'Address not in allowlist' };}// Check networkconst info = getAddressInfo(address);if (options.requireMainnet && info.network !== 'mainnet') {return { valid: false, reason: 'Mainnet address required' };}return { valid: true };}
Testing utilities
Test address validation:
import { describe, it, expect } from 'vitest';describe('Address validation', () => {const validAddresses = ['SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY','ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC','SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY.my-contract',];const invalidAddresses = ['invalid','SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZ', // Too short'XP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY', // Wrong prefix'SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY.', // Missing contract];validAddresses.forEach(address => {it(`should validate ${address}`, () => {expect(validateStacksAddress(address)).toBe(true);});});invalidAddresses.forEach(address => {it(`should reject ${address}`, () => {expect(validateStacksAddress(address)).toBe(false);});});});
Best practices
- Always validate user input: Never trust addresses from users
- Check network compatibility: Ensure addresses match your network
- Handle edge cases: Contract addresses, multisig, etc.
- Cache validation results: Avoid redundant validation
- Provide clear error messages: Help users fix invalid inputs
Common mistakes
Not checking network type
// Bad: Accepting any valid addressconst isValid = validateStacksAddress(userInput);// Good: Checking network matchesconst info = getAddressInfo(userInput);if (info.network !== 'mainnet') {throw new Error('Please use a mainnet address');}
Assuming address format
// Bad: Assuming standard addressconst [principal, contract] = address.split('.');// Good: Proper validationconst parsed = parseContractAddress(address);if (!parsed.isValid) {throw new Error('Invalid contract address');}