@eco-foundation/routes-ts
Version:
The Eco Routes Protocol is a decentralized intent based system that allows users to submit their intent to the network and have it fulfilled by a solver on the destination rollup of their choise.
315 lines (314 loc) • 12.5 kB
JavaScript
;
/**
* @file utils.ts
*
* Utility functions for working with Solidity ABI structures in TypeScript.
* Provides tools to extract, parse, and manipulate ABI definitions for type-safe
* interaction with smart contracts.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.hexToBase58 = exports.base58ToHex = exports.extractAbiStruct = void 0;
const tronweb_1 = require("tronweb");
/**
* Base58 alphabet used by Bitcoin, Solana, and TRON
*/
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
/**
* Generic Base58 decoder that works for both Solana and TRON addresses
* @param base58String - The Base58 encoded string
* @returns Buffer containing the decoded bytes
*/
function decodeBase58(base58String) {
const alphabet = BASE58_ALPHABET;
const alphabetMap = {};
// Create reverse mapping
for (let i = 0; i < alphabet.length; i++) {
alphabetMap[alphabet[i]] = i;
}
let result = BigInt(0);
let base = BigInt(1);
// Process from right to left
for (let i = base58String.length - 1; i >= 0; i--) {
const char = base58String[i];
if (!(char in alphabetMap)) {
throw new Error(`Invalid character '${char}' in Base58 string`);
}
result += BigInt(alphabetMap[char]) * base;
base *= BigInt(58);
}
// Convert to hex string
let hex = result.toString(16);
if (hex.length % 2) {
hex = '0' + hex;
}
// Count leading zeros in original string
let leadingZeros = 0;
for (const char of base58String) {
if (char === '1')
leadingZeros++;
else
break;
}
// Add leading zero bytes
const leadingZeroBytes = '00'.repeat(leadingZeros);
hex = leadingZeroBytes + hex;
return Buffer.from(hex, 'hex');
}
/**
* Generic Base58 encoder that works for both Solana and TRON addresses
* @param buffer - The buffer to encode
* @returns Base58 encoded string
*/
function encodeBase58(buffer) {
const alphabet = BASE58_ALPHABET;
let result = '';
let num = BigInt('0x' + buffer.toString('hex'));
while (num > 0) {
const remainder = num % BigInt(58);
result = alphabet[Number(remainder)] + result;
num = num / BigInt(58);
}
// Handle leading zeros
for (let i = 0; i < buffer.length && buffer[i] === 0; i++) {
result = '1' + result;
}
return result;
}
/**
* Detects whether a Base58 address is likely a TRON or Solana address
* @param base58Address - The Base58 address to analyze
* @returns 'tron' | 'solana' | 'unknown'
*/
function detectAddressType(base58Address) {
try {
// Try TRON first - TRON addresses typically start with 'T' and can be validated by TronWeb
if (base58Address.startsWith('T')) {
tronweb_1.TronWeb.address.toHex(base58Address);
return 'tron';
}
// For other addresses, check the decoded byte length
const decoded = decodeBase58(base58Address);
// Solana addresses are 32 bytes when decoded
if (decoded.length === 32) {
return 'solana';
}
// TRON addresses are 21 bytes when decoded (20 bytes + 1 network byte)
if (decoded.length === 21 && decoded[0] === 0x41) {
return 'tron';
}
return 'unknown';
}
catch {
return 'unknown';
}
}
/**
* Extracts the ABI struct definition with the given name from a contract ABI
*
* This function enables type-safe extraction of Solidity struct definitions from
* contract ABIs, which is essential for encoding and decoding complex data structures.
*
* @param abi - The contract ABI containing the struct definition
* @param structName - The name of the struct to extract
* @returns The struct component definition with proper typing
* @throws Error if the struct is not found in the ABI
*/
function extractAbiStruct(abi, structName) {
const obj = extractAbiStructRecursive(abi, structName);
if (!obj) {
throw ExtractAbiStructFailed(structName);
}
// @ts-expect-error components is always present for structs
return obj.components;
}
exports.extractAbiStruct = extractAbiStruct;
/**
* Recursively searches through an ABI definition to find a struct with the specified name.
* This helper function powers the extractAbiStruct function by traversing the nested ABI structure,
* looking through inputs and components fields to find matching struct definitions.
*
* @param params - The ABI or ABI fragment to search through
* @param structName - The name of the struct to find in the ABI
* @returns The found struct definition or undefined if not found
*
* @internal This is an internal helper function used by extractAbiStruct
*/
function extractAbiStructRecursive(abi, structName) {
for (const item of abi) {
const obj = item;
if (obj.name === structName) {
return obj;
}
if (obj.inputs) {
const result = extractAbiStructRecursive(obj.inputs, structName);
if (result) {
return result;
}
}
if (obj.components) {
const result = extractAbiStructRecursive(obj.components, structName);
if (result) {
return result;
}
}
}
}
/**
* Creates a standardized error object when a struct extraction fails.
* This function provides consistent error messaging when a requested struct
* cannot be found in the provided ABI, making debugging easier.
*
* @param structName - The name of the struct that could not be found
* @returns Error object with descriptive message about the extraction failure
*
* @internal This is an internal helper function used by extractAbiStruct
*/
function ExtractAbiStructFailed(structName) {
return new Error(`Could not extract the structure from abi: ${structName}`);
}
/**
* Converts a Base58 address string to its hex representation.
* This function supports both TRON and Solana addresses, automatically detecting
* the address type and handling the conversion appropriately.
*
* @param base58Address - The Base58 encoded address string
* @returns The hex representation with 0x prefix, padded to 32 bytes (64 hex characters)
* @throws Error if the Base58 address is invalid or conversion fails
*
* @example
* ```typescript
* // TRON address
* const tronHex = base58ToHex("TQh8ig6rmuMqb5u8efU5LDvoott1oLzoqu")
* console.log(tronHex) // "0x000000000000000000000000a17fa8126b6a12feb2fe9c19f618fe04d7329074"
*
* // Solana address
* const solanaHex = base58ToHex("C34z78p3WtkDZoxtBqiKgeuC71rbnv2H7koqHmb5Eo3M")
* console.log(solanaHex) // "0xa3f83922f3081c229a9f7ff240f29f34a3548e8c7f05b6202d0d7df3de781788"
* ```
*/
function base58ToHex(base58Address) {
try {
const addressType = detectAddressType(base58Address);
if (addressType === 'tron') {
// Use TronWeb for TRON addresses
const hexAddress = tronweb_1.TronWeb.address.toHex(base58Address);
let cleanHex = hexAddress.startsWith('0x') ? hexAddress.slice(2) : hexAddress;
// Remove the TRON network byte (0x41 for mainnet) to get the 20-byte address
if (cleanHex.length === 42 && cleanHex.startsWith('41')) {
cleanHex = cleanHex.slice(2);
}
// Pad to 64 characters (32 bytes)
const paddedHex = cleanHex.padStart(64, '0');
return `0x${paddedHex}`;
}
else {
// Use generic Base58 decoding for Solana and other addresses
const decoded = decodeBase58(base58Address);
let hex = decoded.toString('hex');
// Ensure we have 64 characters (32 bytes)
if (hex.length < 64) {
hex = hex.padStart(64, '0');
}
else if (hex.length > 64) {
// If longer than 32 bytes, take the last 32 bytes
hex = hex.slice(-64);
}
return `0x${hex}`;
}
}
catch (error) {
throw new Error(`Failed to convert Base58 address to hex: ${base58Address}. ${error.message}`);
}
}
exports.base58ToHex = base58ToHex;
/**
* Converts a hex address string to its Base58 representation.
* This function supports both TRON and Solana address formats, with automatic detection
* based on the hex input length and content.
*
* @param hexAddress - The hex encoded address string with or without 0x prefix
* @param targetFormat - Optional format specification ('tron' | 'solana' | 'auto')
* @returns The Base58 representation
* @throws Error if the hex address is invalid or conversion fails
*
* @example
* ```typescript
* // TRON address (20 bytes padded to 32 bytes)
* const tronAddress = hexToBase58("0x000000000000000000000000a17fa8126b6a12feb2fe9c19f618fe04d7329074")
* console.log(tronAddress) // "TQh8ig6rmuMqb5u8efU5LDvoott1oLzoqu"
*
* // Solana address (32 bytes)
* const solanaAddress = hexToBase58("0xa3f83922f3081c229a9f7ff240f29f34a3548e8c7f05b6202d0d7df3de781788")
* console.log(solanaAddress) // "C34z78p3WtkDZoxtBqiKgeuC71rbnv2H7koqHmb5Eo3M"
* ```
*/
function hexToBase58(hexAddress, targetFormat = 'auto') {
try {
// Clean the hex string - remove 0x prefix if present
let cleanHex = hexAddress.startsWith('0x') ? hexAddress.slice(2) : hexAddress;
// Determine the target format if auto-detection is requested
let format = targetFormat;
if (format === 'auto') {
// Detect format based on hex characteristics
if (cleanHex.length === 64) {
// Check if this looks like a padded TRON address (leading zeros + 40 char address)
const leadingZeros = cleanHex.match(/^0+/)?.[0]?.length || 0;
const remainingHex = cleanHex.slice(leadingZeros);
if (leadingZeros >= 24 && remainingHex.length === 40) {
// Likely a padded TRON address (12+ leading zero bytes + 20-byte address)
format = 'tron';
}
else if (cleanHex.match(/^[0-9a-fA-F]{64}$/) && !cleanHex.startsWith('000000000000000000000000')) {
// Full 32-byte hex without excessive leading zeros - likely Solana
format = 'solana';
}
else {
// Default to Solana for 32-byte addresses
format = 'solana';
}
}
else if (cleanHex.length === 40) {
// 20-byte address, assume TRON
format = 'tron';
}
else {
throw new Error(`Unsupported hex address length: ${cleanHex.length} characters`);
}
}
if (format === 'tron') {
// TRON address handling
let addressHex = cleanHex;
// For 64-character hex, extract the last 40 characters (20 bytes)
if (addressHex.length === 64) {
addressHex = addressHex.slice(-40);
}
// Ensure we have a valid 40-character hex string for a 20-byte address
if (addressHex.length !== 40) {
throw new Error(`Invalid hex address length for TRON: expected 40 characters, got ${addressHex.length}`);
}
// Add TRON network byte (0x41 for mainnet) to the address
const tronHexWithNetworkByte = `41${addressHex}`;
// Convert hex address to Base58 using TronWeb
return tronweb_1.TronWeb.address.fromHex(tronHexWithNetworkByte);
}
else {
// Solana address handling
let addressHex = cleanHex;
// Ensure we have exactly 64 characters (32 bytes)
if (addressHex.length < 64) {
addressHex = addressHex.padStart(64, '0');
}
else if (addressHex.length > 64) {
// If longer than 32 bytes, take the last 32 bytes
addressHex = addressHex.slice(-64);
}
// Convert hex to buffer and encode as Base58
const buffer = Buffer.from(addressHex, 'hex');
return encodeBase58(buffer);
}
}
catch (error) {
throw new Error(`Failed to convert hex address to Base58: ${hexAddress}. ${error.message}`);
}
}
exports.hexToBase58 = hexToBase58;