Skip to content

Instantly share code, notes, and snippets.

@heyitsarpit
Created April 8, 2025 14:33
Show Gist options
  • Save heyitsarpit/5411925fb10bb410edcf23ceb73c4d70 to your computer and use it in GitHub Desktop.
Save heyitsarpit/5411925fb10bb410edcf23ceb73c4d70 to your computer and use it in GitHub Desktop.
Address utility with cross conversion fns
import bs58 from 'bs58';
export class Address {
private buffer: Buffer;
private constructor(buffer: Buffer) {
if (buffer.length !== 32) {
throw new Error('Internal representation must be 32 bytes.');
}
this.buffer = buffer;
}
static fromEvm20(address: string): Address {
if (!Address.isValidEvm20(address)) {
throw new Error('Invalid EVM 20-byte address.');
}
address = address.startsWith('0x') ? address.slice(2) : address;
const evm20 = Buffer.from(address, 'hex');
// Create a 12-byte zero buffer for padding
const padding = Buffer.alloc(12, 0);
const fullBuffer = Buffer.concat([padding, evm20]);
return new Address(fullBuffer);
}
static fromEvm32(address: string): Address {
if (!Address.isValidEvm32(address)) {
throw new Error('Invalid EVM 32-byte address.');
}
address = address.startsWith('0x') ? address.slice(2) : address;
const buffer = Buffer.from(address, 'hex');
return new Address(buffer);
}
static fromSolana(address: string): Address {
if (!Address.isValidSolana(address)) {
throw new Error('Invalid Solana address.');
}
const decoded = bs58.decode(address);
const buffer = Buffer.from(decoded);
return new Address(buffer);
}
toEvm32(): string {
return '0x' + this.buffer.toString('hex');
}
toEvm20(): string {
// Slice the lower 20 bytes.
return '0x' + this.buffer.slice(12).toString('hex');
}
toSolana(): string {
return bs58.encode(this.buffer);
}
static isValidEvm20(address: string): boolean {
if (address.startsWith('0x')) {
address = address.slice(2);
}
return /^[0-9a-fA-F]{40}$/.test(address);
}
static isValidEvm32(address: string): boolean {
if (address.startsWith('0x')) {
address = address.slice(2);
}
return /^[0-9a-fA-F]{64}$/.test(address);
}
static isValidSolana(address: string): boolean {
try {
const decoded = bs58.decode(address);
return decoded.length === 32;
} catch {
return false;
}
}
}
@heyitsarpit
Copy link
Author

updated implementation with more generic naming

import bs58 from 'bs58';

export class Address {
  private buffer: Buffer;

  // Private constructor that accepts a 32-byte Buffer.
  private constructor(buffer: Buffer) {
    if (buffer.length !== 32) {
      throw new Error('Internal representation must be 32 bytes.');
    }
    this.buffer = buffer;
  }

  /**
   * Creates an Address instance from a 20-byte hexadecimal string.
   * The hex20 input is padded on the left with zeros to form a 32-byte value.
   */
  static fromHex20(input: string): Address {
    if (!Address.isValidHex20(input)) {
      throw new Error('Invalid hex20 address.');
    }
    // Remove "0x" prefix if present.
    const cleaned = input.startsWith('0x') ? input.slice(2) : input;
    const hex20Buffer = Buffer.from(cleaned, 'hex');
    // Pad left with zeros to form a 32-byte buffer.
    const padding = Buffer.alloc(12, 0);
    const fullBuffer = Buffer.concat([padding, hex20Buffer]);
    return new Address(fullBuffer);
  }

  /**
   * Creates an Address instance from a 32-byte hexadecimal string.
   */
  static fromHex32(input: string): Address {
    if (!Address.isValidHex32(input)) {
      throw new Error('Invalid hex32 address.');
    }
    const cleaned = input.startsWith('0x') ? input.slice(2) : input;
    const buffer = Buffer.from(cleaned, 'hex');
    return new Address(buffer);
  }

  /**
   * Creates an Address instance from a base58-encoded string.
   */
  static fromBs58(input: string): Address {
    if (!Address.isValidBs58(input)) {
      throw new Error('Invalid bs58 address.');
    }
    const decoded = bs58.decode(input);
    const buffer = Buffer.from(decoded);
    return new Address(buffer);
  }

  /**
   * Returns the address as a hex32 string (32-byte hex representation with "0x" prefix).
   */
  toHex32(): string {
    return '0x' + this.buffer.toString('hex');
  }

  /**
   * Returns the address as a hex20 string (lower 20 bytes of the internal 32-byte representation).
   */
  toHex20(): string {
    // Slice the lower 20 bytes (skip the first 12 bytes).
    return '0x' + this.buffer.slice(12).toString('hex');
  }

  /**
   * Returns the address as a bs58-encoded string.
   */
  toBs58(): string {
    return bs58.encode(this.buffer);
  }

  /**
   * Validates a hex20 address (should be exactly 40 hex characters, ignoring "0x" prefix).
   */
  static isValidHex20(input: string): boolean {
    const cleaned = input.startsWith('0x') ? input.slice(2) : input;
    return /^[0-9a-fA-F]{40}$/.test(cleaned);
  }

  /**
   * Validates a hex32 address (should be exactly 64 hex characters, ignoring "0x" prefix).
   */
  static isValidHex32(input: string): boolean {
    const cleaned = input.startsWith('0x') ? input.slice(2) : input;
    return /^[0-9a-fA-F]{64}$/.test(cleaned);
  }

  /**
   * Validates a bs58 address (base58-decoded value must be 32 bytes).
   */
  static isValidBs58(input: string): boolean {
    try {
      const decoded = bs58.decode(input);
      return decoded.length === 32;
    } catch {
      return false;
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment