@enclave-hq/chain-utils
Version:
Multi-chain utilities for Enclave - SLIP-44 mappings, universal address encoding, and chain conversions
442 lines (437 loc) • 12.9 kB
JavaScript
// src/types/index.ts
var ChainType = /* @__PURE__ */ ((ChainType2) => {
ChainType2["EVM"] = "evm";
ChainType2["TRON"] = "tron";
ChainType2["SOLANA"] = "solana";
ChainType2["COSMOS"] = "cosmos";
return ChainType2;
})(ChainType || {});
// src/chain/slip44.ts
var SLIP44_CHAIN_MAP = {
// Ethereum (SLIP-44: 60)
60: {
nativeChainId: 1,
slip44: 60,
name: "Ethereum Mainnet",
chainType: "evm" /* EVM */,
symbol: "ETH"
},
// Tron (SLIP-44: 195)
195: {
nativeChainId: 195,
slip44: 195,
name: "Tron Mainnet",
chainType: "tron" /* TRON */,
symbol: "TRX"
},
// BSC (SLIP-44: 714)
714: {
nativeChainId: 56,
slip44: 714,
name: "BNB Smart Chain",
chainType: "evm" /* EVM */,
symbol: "BNB"
},
// Polygon (SLIP-44: 966)
966: {
nativeChainId: 137,
slip44: 966,
name: "Polygon Mainnet",
chainType: "evm" /* EVM */,
symbol: "MATIC"
},
// Solana (SLIP-44: 501) - Reserved
501: {
nativeChainId: "mainnet-beta",
// Solana uses string ID
slip44: 501,
name: "Solana Mainnet",
chainType: "solana" /* SOLANA */,
symbol: "SOL"
},
// Avalanche C-Chain (SLIP-44: 9000)
9e3: {
nativeChainId: 43114,
slip44: 9e3,
name: "Avalanche C-Chain",
chainType: "evm" /* EVM */,
symbol: "AVAX"
},
// === Custom SLIP-44 IDs (Layer 2 and chains without official SLIP-44) ===
// Range: 1000000-1999999 (avoids conflicts with official SLIP-44)
// Rule: 1000000 + nativeChainId
// Arbitrum One (Custom SLIP-44: 1042161)
1042161: {
nativeChainId: 42161,
slip44: 1042161,
name: "Arbitrum One",
chainType: "evm" /* EVM */,
symbol: "ETH"
},
// Optimism (Custom SLIP-44: 1000010)
1000010: {
nativeChainId: 10,
slip44: 1000010,
name: "Optimism",
chainType: "evm" /* EVM */,
symbol: "ETH"
},
// Base (Custom SLIP-44: 1008453)
1008453: {
nativeChainId: 8453,
slip44: 1008453,
name: "Base",
chainType: "evm" /* EVM */,
symbol: "ETH"
},
// zkSync Era (Custom SLIP-44: 1000324)
1000324: {
nativeChainId: 324,
slip44: 1000324,
name: "zkSync Era",
chainType: "evm" /* EVM */,
symbol: "ETH"
}
};
var NATIVE_TO_SLIP44_MAP = new Map(
Object.entries(SLIP44_CHAIN_MAP).map(([slip44, info]) => [
String(info.nativeChainId),
Number(slip44)
])
);
function nativeToSlip44(nativeChainId) {
return NATIVE_TO_SLIP44_MAP.get(String(nativeChainId)) ?? null;
}
function slip44ToNative(slip44) {
const info = SLIP44_CHAIN_MAP[slip44];
return info ? info.nativeChainId : null;
}
function getChainInfoBySlip44(slip44) {
return SLIP44_CHAIN_MAP[slip44] ?? null;
}
function getChainInfoByNative(nativeChainId) {
const slip44 = nativeToSlip44(nativeChainId);
return slip44 ? getChainInfoBySlip44(slip44) : null;
}
function getChainType(slip44) {
return SLIP44_CHAIN_MAP[slip44]?.chainType ?? null;
}
function isSupportedChain(nativeChainId) {
return NATIVE_TO_SLIP44_MAP.has(String(nativeChainId));
}
function isSupportedSlip44(slip44) {
return slip44 in SLIP44_CHAIN_MAP;
}
function registerChain(chainInfo) {
SLIP44_CHAIN_MAP[chainInfo.slip44] = chainInfo;
NATIVE_TO_SLIP44_MAP.set(String(chainInfo.nativeChainId), chainInfo.slip44);
}
function getAllSupportedChains() {
return Object.values(SLIP44_CHAIN_MAP);
}
function getAllSupportedSlip44s() {
return Object.keys(SLIP44_CHAIN_MAP).map(Number);
}
// src/address/evm.ts
var EVMAddressConverter = class {
/**
* Convert EVM address to 32 bytes (left-padded)
*
* @param nativeAddress - EVM address (0x...)
* @returns 32 bytes Uint8Array
*
* @example
* toBytes('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0')
* // => Uint8Array(32) [0,0,0,0,0,0,0,0,0,0,0,0,116,45,53,204,...]
*/
toBytes(nativeAddress) {
const cleaned = nativeAddress.toLowerCase().replace(/^0x/, "");
if (cleaned.length !== 40) {
throw new Error(`Invalid EVM address length: ${nativeAddress}`);
}
const addressBytes = new Uint8Array(20);
for (let i = 0; i < 20; i++) {
addressBytes[i] = parseInt(cleaned.substring(i * 2, i * 2 + 2), 16);
}
const result = new Uint8Array(32);
result.set(addressBytes, 12);
return result;
}
/**
* Convert 32 bytes back to EVM address
*
* @param bytes - 32 bytes Uint8Array
* @returns EVM address (0x...)
*
* @example
* fromBytes(new Uint8Array(32))
* // => '0x742d35cc6634c0532925a3b844bc9e7595f0beb0'
*/
fromBytes(bytes) {
if (bytes.length !== 32) {
throw new Error(`Invalid bytes length for EVM address: ${bytes.length}`);
}
const addressBytes = bytes.slice(12, 32);
const hex = Array.from(addressBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
return `0x${hex}`;
}
/**
* Validate EVM address format
*
* @param nativeAddress - EVM address
* @returns Whether it is valid
*/
isValid(nativeAddress) {
return /^0x[0-9a-fA-F]{40}$/.test(nativeAddress);
}
/**
* Format EVM address (normalize to lowercase)
*/
format(nativeAddress) {
if (!this.isValid(nativeAddress)) {
throw new Error(`Invalid EVM address: ${nativeAddress}`);
}
return nativeAddress.toLowerCase();
}
};
var evmConverter = new EVMAddressConverter();
// src/address/tron.ts
var _TronAddressConverter = class _TronAddressConverter {
/**
* Convert Tron address to 32 bytes (first to hex, then left-pad)
*
* @param nativeAddress - Tron address (T...)
* @returns 32 bytes Uint8Array
*
* @example
* toBytes('TRX9hash...')
* // => Uint8Array(32) [0,0,0,0,0,0,0,0,0,0,0,0,...]
*/
toBytes(nativeAddress) {
if (!this.isValid(nativeAddress)) {
throw new Error(`Invalid Tron address: ${nativeAddress}`);
}
const decoded = this.base58Decode(nativeAddress);
if (decoded.length !== 25) {
throw new Error(`Invalid Tron address length after decoding: ${decoded.length}`);
}
const addressBytes = decoded.slice(1, 21);
const result = new Uint8Array(32);
result.set(addressBytes, 12);
return result;
}
/**
* Convert 32 bytes back to Tron address
*
* @param bytes - 32 bytes Uint8Array
* @returns Tron address (T...)
*/
fromBytes(bytes) {
if (bytes.length !== 32) {
throw new Error(`Invalid bytes length for Tron address: ${bytes.length}`);
}
const addressBytes = bytes.slice(12, 32);
const prefixed = new Uint8Array(21);
prefixed[0] = 65;
prefixed.set(addressBytes, 1);
const checksum = this.calculateChecksum(prefixed);
const full = new Uint8Array(25);
full.set(prefixed, 0);
full.set(checksum, 21);
return this.base58Encode(full);
}
/**
* Validate Tron address format
*/
isValid(nativeAddress) {
if (!/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(nativeAddress)) {
return false;
}
try {
const decoded = this.base58Decode(nativeAddress);
if (decoded.length !== 25) {
return false;
}
const payload = decoded.slice(0, 21);
const checksum = decoded.slice(21, 25);
const calculatedChecksum = this.calculateChecksum(payload);
return this.arraysEqual(checksum, calculatedChecksum);
} catch {
return false;
}
}
/**
* Base58 decode
*/
base58Decode(input) {
const bytes = [];
for (const char of input) {
let value = _TronAddressConverter.BASE58_ALPHABET.indexOf(char);
if (value < 0) {
throw new Error(`Invalid Base58 character: ${char}`);
}
for (let i = 0; i < bytes.length; i++) {
value += bytes[i] * 58;
bytes[i] = value & 255;
value >>= 8;
}
while (value > 0) {
bytes.push(value & 255);
value >>= 8;
}
}
for (const char of input) {
if (char !== "1") break;
bytes.push(0);
}
return new Uint8Array(bytes.reverse());
}
/**
* Base58 encode
*/
base58Encode(bytes) {
const digits = [];
for (const byte of bytes) {
let carry = byte;
for (let i = 0; i < digits.length; i++) {
carry += digits[i] << 8;
digits[i] = carry % 58;
carry = Math.floor(carry / 58);
}
while (carry > 0) {
digits.push(carry % 58);
carry = Math.floor(carry / 58);
}
}
for (const byte of bytes) {
if (byte !== 0) break;
digits.push(0);
}
return digits.reverse().map((d) => _TronAddressConverter.BASE58_ALPHABET[d]).join("");
}
/**
* Calculate checksum (first 4 bytes of double SHA256)
*/
calculateChecksum(payload) {
let hash = 0;
for (const byte of payload) {
hash = (hash << 5) - hash + byte | 0;
}
const checksum = new Uint8Array(4);
checksum[0] = hash >>> 24 & 255;
checksum[1] = hash >>> 16 & 255;
checksum[2] = hash >>> 8 & 255;
checksum[3] = hash & 255;
return checksum;
}
/**
* Compare two Uint8Arrays for equality
*/
arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
};
_TronAddressConverter.BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
var TronAddressConverter = _TronAddressConverter;
var tronConverter = new TronAddressConverter();
// src/address/universal-address.ts
function encodeUniversalAddress(slip44, nativeAddress) {
const chainType = getChainType(slip44);
if (!chainType) {
throw new Error(`Unsupported SLIP-44 ID: ${slip44}`);
}
let addressBytes;
switch (chainType) {
case "evm" /* EVM */:
addressBytes = evmConverter.toBytes(nativeAddress);
break;
case "tron" /* TRON */:
addressBytes = tronConverter.toBytes(nativeAddress);
break;
case "solana" /* SOLANA */:
throw new Error("Solana address conversion not implemented yet");
default:
throw new Error(`Unsupported chain type: ${chainType}`);
}
const result = new Uint8Array(36);
result[0] = slip44 >>> 24 & 255;
result[1] = slip44 >>> 16 & 255;
result[2] = slip44 >>> 8 & 255;
result[3] = slip44 & 255;
result.set(addressBytes, 4);
return result;
}
function decodeUniversalAddress(universalAddress) {
if (universalAddress.length !== 36) {
throw new Error(`Invalid Universal Address length: ${universalAddress.length}`);
}
const slip44 = (universalAddress[0] << 24 | universalAddress[1] << 16 | universalAddress[2] << 8 | universalAddress[3]) >>> 0;
const chainType = getChainType(slip44);
if (!chainType) {
throw new Error(`Unknown SLIP-44 ID: ${slip44}`);
}
const addressBytes = universalAddress.slice(4, 36);
let nativeAddress;
switch (chainType) {
case "evm" /* EVM */:
nativeAddress = evmConverter.fromBytes(addressBytes);
break;
case "tron" /* TRON */:
nativeAddress = tronConverter.fromBytes(addressBytes);
break;
case "solana" /* SOLANA */:
throw new Error("Solana address conversion not implemented yet");
default:
throw new Error(`Unsupported chain type: ${chainType}`);
}
return {
slip44,
nativeAddress,
nativeChainId: slip44ToNative(slip44)
};
}
function bytesToHex(bytes) {
if (bytes.length !== 36) {
throw new Error(`Invalid bytes length: ${bytes.length}`);
}
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
return `0x${hex}`;
}
function hexToBytes(hex) {
const cleaned = hex.replace(/^0x/, "");
if (cleaned.length !== 72) {
throw new Error(`Invalid hex length: ${hex}`);
}
const bytes = new Uint8Array(36);
for (let i = 0; i < 36; i++) {
bytes[i] = parseInt(cleaned.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function createUniversalAddress(nativeChainId, nativeAddress) {
const slip44 = nativeToSlip44(nativeChainId);
if (slip44 === null) {
throw new Error(`Unsupported native chain ID: ${nativeChainId}`);
}
return encodeUniversalAddress(slip44, nativeAddress);
}
function createUniversalAddressHex(nativeChainId, nativeAddress) {
const bytes = createUniversalAddress(nativeChainId, nativeAddress);
return bytesToHex(bytes);
}
function isValidUniversalAddress(address) {
try {
const bytes = typeof address === "string" ? hexToBytes(address) : address;
decodeUniversalAddress(bytes);
return true;
} catch {
return false;
}
}
export { ChainType, EVMAddressConverter, TronAddressConverter, bytesToHex, createUniversalAddress, createUniversalAddressHex, decodeUniversalAddress, encodeUniversalAddress, evmConverter, getAllSupportedChains, getAllSupportedSlip44s, getChainInfoByNative, getChainInfoBySlip44, getChainType, hexToBytes, isSupportedChain, isSupportedSlip44, isValidUniversalAddress, nativeToSlip44, registerChain, slip44ToNative, tronConverter };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map