UNPKG

@hyperlane-xyz/utils

Version:

General utilities and types for the Hyperlane network

624 lines 23.3 kB
import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; import { bech32m } from 'bech32'; import bs58 from 'bs58'; import { Wallet, utils as ethersUtils } from 'ethers'; import { addAddressPadding, encode, num, validateAndParseAddress, } from 'starknet'; import { isNullish } from './typeof.js'; import { ProtocolType } from './types.js'; import { assert } from './validation.js'; const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; const SEALEVEL_ADDRESS_REGEX = /^[a-zA-Z0-9]{36,44}$/; const COSMOS_NATIVE_ADDRESS_REGEX = /^(0x)?[0-9a-fA-F]{64}$/; const STARKNET_ADDRESS_REGEX = /^(0x)?[0-9a-fA-F]{63,64}$/; const RADIX_ADDRESS_REGEX = /^(account|component)_(rdx|loc|sim|tdx_[\d]_)[a-z0-9]{55}$/; const ALEO_ADDRESS_REGEX = /^([a-z0-9_]+\.aleo\/aleo1[a-z0-9]{58}|aleo1[a-z0-9]{58})$/; const TRON_ADDRESS_REGEX = /^T[1-9A-HJ-NP-Za-km-z]{33}$/; const HEX_BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/; // https://github.com/cosmos/cosmos-sdk/blob/84c33215658131d87daf3c629e909e12ed9370fa/types/coin.go#L601C17-L601C44 const COSMOS_DENOM_PATTERN = `[a-zA-Z][a-zA-Z0-9]{2,127}`; // https://en.bitcoin.it/wiki/BIP_0173 const BECH32_ADDRESS_PATTERN = `[a-zA-Z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38,58}`; const COSMOS_ADDRESS_REGEX = new RegExp(`^${BECH32_ADDRESS_PATTERN}$`); const IBC_DENOM_REGEX = new RegExp(`^ibc/([A-Fa-f0-9]{64})$`); const COSMOS_FACTORY_TOKEN_REGEX = new RegExp(`^factory/(${BECH32_ADDRESS_PATTERN})/${COSMOS_DENOM_PATTERN}$`); const EVM_TX_HASH_REGEX = /^0x([A-Fa-f0-9]{64})$/; const SEALEVEL_TX_HASH_REGEX = /^[a-zA-Z1-9]{88}$/; const COSMOS_TX_HASH_REGEX = /^(0x)?[A-Fa-f0-9]{64}$/; const STARKNET_TX_HASH_REGEX = /^(0x)?[0-9a-fA-F]{64}$/; const RADIX_TX_HASH_REGEX = /^txid_(rdx|sim|tdx_[\d]_)[a-z0-9]{59}$/; const ALEO_TX_HASH_REGEX = /^at1[a-z0-9]{58}$/; const TRON_TX_HASH_REGEX = /^0x([A-Fa-f0-9]{64})$/; const EVM_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/; const SEALEVEL_ZEROISH_ADDRESS_REGEX = /^1+$/; const COSMOS_ZEROISH_ADDRESS_REGEX = /^[a-z]{1,10}?1[0]+$/; const COSMOS_NATIVE_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/; const STARKNET_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/; const RADIX_ZEROISH_ADDRESS_REGEX = /^0*$/; const ALEO_ZEROISH_ADDRESS_REGEX = /^(?:[a-z0-9_]+\.aleo\/)?aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc$/; const TRON_ZEROISH_ADDRESS_REGEX = /^T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb$/; export const ZERO_ADDRESS_HEX_32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; export function isAddressEvm(address) { return EVM_ADDRESS_REGEX.test(address); } export function isAddressSealevel(address) { return SEALEVEL_ADDRESS_REGEX.test(address); } export function isAddressCosmos(address) { return (COSMOS_ADDRESS_REGEX.test(address) || IBC_DENOM_REGEX.test(address) || COSMOS_FACTORY_TOKEN_REGEX.test(address)); } export function isAddressCosmosNative(address) { return COSMOS_NATIVE_ADDRESS_REGEX.test(address); } export function isCosmosIbcDenomAddress(address) { return IBC_DENOM_REGEX.test(address); } export function isAddressStarknet(address) { try { return (STARKNET_ADDRESS_REGEX.test(address) && !!validateAndParseAddress(address)); } catch { return false; } } export function isAddressRadix(address) { return RADIX_ADDRESS_REGEX.test(address); } export function isAddressAleo(address) { return ALEO_ADDRESS_REGEX.test(address); } export function isAddressTron(address) { return TRON_ADDRESS_REGEX.test(address); } export function getAddressProtocolType(address) { if (!address) return undefined; if (isAddressEvm(address)) { return ProtocolType.Ethereum; } else if (isAddressAleo(address)) { return ProtocolType.Aleo; } else if (isAddressTron(address)) { return ProtocolType.Tron; } else if (isAddressCosmos(address)) { return ProtocolType.Cosmos; } else if (isAddressCosmosNative(address)) { return ProtocolType.CosmosNative; } else if (isAddressSealevel(address)) { return ProtocolType.Sealevel; } else if (isAddressStarknet(address)) { return ProtocolType.Starknet; } else if (isAddressRadix(address)) { return ProtocolType.Radix; } else { return undefined; } } export function isAddress(address) { return !!getAddressProtocolType(address); } function routeAddressUtil(fns, param, fallback, protocol) { protocol ||= getAddressProtocolType(param); if (protocol && fns[protocol]) return fns[protocol](param); else if (!isNullish(fallback)) return fallback; else throw new Error(`Unsupported protocol ${protocol}`); } // Slower than isAddressEvm above but actually validates content and checksum export function isValidAddressEvm(address) { // Need to catch because ethers' isAddress throws in some cases (bad checksum) try { const isValid = address && ethersUtils.isAddress(address); return !!isValid; } catch { return false; } } // Slower than isAddressSealevel above but actually validates content and checksum export function isValidAddressSealevel(address) { try { const isValid = address && new PublicKey(address).toBase58(); return !!isValid; } catch { return false; } } // Slower than isAddressCosmos above but actually validates content and checksum export function isValidAddressCosmos(address) { try { const isValid = address && (IBC_DENOM_REGEX.test(address) || COSMOS_FACTORY_TOKEN_REGEX.test(address) || fromBech32(address)); return !!isValid; } catch { return false; } } export function isValidAddressStarknet(address) { try { return (!!address && STARKNET_ADDRESS_REGEX.test(address) && !!validateAndParseAddress(address)); } catch { return false; } } export function isValidAddressRadix(address) { try { const isValid = address && RADIX_ADDRESS_REGEX.test(address); return !!isValid; } catch { return false; } } export function isValidAddressAleo(address) { try { const isValid = address && ALEO_ADDRESS_REGEX.test(address); return !!isValid; } catch { return false; } } export function isValidAddressTron(address) { try { // Tron is EVM-compatible, so accept both Tron base58 (T...) and EVM hex (0x...) formats const isValid = address && (TRON_ADDRESS_REGEX.test(address) || isValidAddressEvm(address)); return !!isValid; } catch { return false; } } export function isValidAddress(address, protocol) { return routeAddressUtil({ [ProtocolType.Ethereum]: isValidAddressEvm, [ProtocolType.Sealevel]: isValidAddressSealevel, [ProtocolType.Cosmos]: isValidAddressCosmos, [ProtocolType.CosmosNative]: isValidAddressCosmos, [ProtocolType.Starknet]: isValidAddressStarknet, [ProtocolType.Radix]: isValidAddressRadix, [ProtocolType.Aleo]: isValidAddressAleo, [ProtocolType.Tron]: isValidAddressTron, }, address, false, protocol); } export function normalizeAddressEvm(address) { if (isZeroishAddress(address)) return address; try { return ethersUtils.getAddress(address); } catch { return address; } } export function normalizeAddressSealevel(address) { if (isZeroishAddress(address)) return address; try { return new PublicKey(address).toBase58(); } catch { return address; } } export function normalizeAddressCosmos(address) { if (isZeroishAddress(address)) return address; try { return normalizeBech32(address); } catch { return address; } } export function normalizeAddressStarknet(address) { if (isZeroishAddress(address)) return address; try { return validateAndParseAddress(address); } catch { return address; } } export function normalizeAddressRadix(address) { return address; } export function normalizeAddressAleo(address) { return address; } export function normalizeAddressTron(address) { return address; } export function normalizeAddress(address, protocol) { return routeAddressUtil({ [ProtocolType.Ethereum]: normalizeAddressEvm, [ProtocolType.Sealevel]: normalizeAddressSealevel, [ProtocolType.Cosmos]: normalizeAddressCosmos, [ProtocolType.CosmosNative]: normalizeAddressCosmos, [ProtocolType.Starknet]: normalizeAddressStarknet, [ProtocolType.Radix]: normalizeAddressRadix, [ProtocolType.Aleo]: normalizeAddressAleo, [ProtocolType.Tron]: normalizeAddressTron, }, address, address, protocol); } export function eqAddressEvm(a1, a2) { return normalizeAddressEvm(a1) === normalizeAddressEvm(a2); } export function eqAddressSol(a1, a2) { return normalizeAddressSealevel(a1) === normalizeAddressSealevel(a2); } export function eqAddressCosmos(a1, a2) { return normalizeAddressCosmos(a1) === normalizeAddressCosmos(a2); } export function eqAddressStarknet(a1, a2) { return normalizeAddressStarknet(a1) === normalizeAddressStarknet(a2); } export function eqAddressRadix(a1, a2) { return normalizeAddressRadix(a1) === normalizeAddressRadix(a2); } export function eqAddressAleo(a1, a2) { return normalizeAddressAleo(a1) === normalizeAddressAleo(a2); } export function eqAddressTron(a1, a2) { return normalizeAddressTron(a1) === normalizeAddressTron(a2); } export function eqAddress(a1, a2) { const p1 = getAddressProtocolType(a1); const p2 = getAddressProtocolType(a2); if (p1 !== p2) return false; return routeAddressUtil({ [ProtocolType.Ethereum]: (_a1) => eqAddressEvm(_a1, a2), [ProtocolType.Sealevel]: (_a1) => eqAddressSol(_a1, a2), [ProtocolType.Cosmos]: (_a1) => eqAddressCosmos(_a1, a2), [ProtocolType.CosmosNative]: (_a1) => eqAddressCosmos(_a1, a2), [ProtocolType.Starknet]: (_a1) => eqAddressStarknet(_a1, a2), [ProtocolType.Radix]: (_a1) => eqAddressRadix(_a1, a2), [ProtocolType.Aleo]: (_a1) => eqAddressAleo(_a1, a2), [ProtocolType.Tron]: (_a1) => eqAddressTron(_a1, a2), }, a1, false, p1); } export function isValidTransactionHashEvm(input) { return EVM_TX_HASH_REGEX.test(input); } export function isValidTransactionHashSealevel(input) { return SEALEVEL_TX_HASH_REGEX.test(input); } export function isValidTransactionHashCosmos(input) { return COSMOS_TX_HASH_REGEX.test(input); } export function isValidTransactionHashStarknet(input) { return STARKNET_TX_HASH_REGEX.test(input); } export function isValidTransactionHashRadix(input) { return RADIX_TX_HASH_REGEX.test(input); } export function isValidTransactionHashAleo(input) { return ALEO_TX_HASH_REGEX.test(input); } export function isValidTransactionHashTron(input) { return TRON_TX_HASH_REGEX.test(input); } export function isValidTransactionHash(input, protocol) { if (protocol === ProtocolType.Ethereum) { return isValidTransactionHashEvm(input); } else if (protocol === ProtocolType.Sealevel) { return isValidTransactionHashSealevel(input); } else if (protocol === ProtocolType.Cosmos) { return isValidTransactionHashCosmos(input); } else if (protocol === ProtocolType.CosmosNative) { return isValidTransactionHashCosmos(input); } else if (protocol === ProtocolType.Starknet) { return isValidTransactionHashStarknet(input); } else if (protocol === ProtocolType.Radix) { return isValidTransactionHashRadix(input); } else if (protocol === ProtocolType.Aleo) { return isValidTransactionHashAleo(input); } else if (protocol === ProtocolType.Tron) { return isValidTransactionHashTron(input); } else { return false; } } export function isZeroishAddress(address) { return (EVM_ZEROISH_ADDRESS_REGEX.test(address) || SEALEVEL_ZEROISH_ADDRESS_REGEX.test(address) || COSMOS_ZEROISH_ADDRESS_REGEX.test(address) || COSMOS_NATIVE_ZEROISH_ADDRESS_REGEX.test(address) || STARKNET_ZEROISH_ADDRESS_REGEX.test(address) || RADIX_ZEROISH_ADDRESS_REGEX.test(address) || ALEO_ZEROISH_ADDRESS_REGEX.test(address) || TRON_ZEROISH_ADDRESS_REGEX.test(address)); } /** * Compares two optional addresses, treating undefined and zeroish addresses as equivalent. * Useful for comparing optional contract addresses (ISM, hooks, etc.) where both * undefined and zero address mean "not set" or "use default". * * @param a1 First address (can be undefined) * @param a2 Second address (can be undefined) * @param eqFn Protocol-specific address equality function (e.g., eqAddressEvm, eqAddressRadix) * @returns true if addresses are equivalent (both unset/zeroish or both set to same address) */ export function eqOptionalAddress(a1, a2, eqFn) { const formatAddress = (addr) => !addr || isZeroishAddress(addr) ? undefined : addr; const normalized1 = formatAddress(a1); const normalized2 = formatAddress(a2); // Both undefined/zeroish or same string if (normalized1 === normalized2) return true; // One is undefined/zeroish, other is not if (!normalized1 || !normalized2) return false; // Both are real addresses, use protocol-specific comparison return eqFn(normalized1, normalized2); } export function shortenAddress(address, capitalize) { if (!address) return ''; if (address.length < 8) return address; const normalized = normalizeAddress(address); const shortened = normalized.substring(0, 5) + '...' + normalized.substring(normalized.length - 4); return capitalize ? capitalizeAddress(shortened) : shortened; } export function capitalizeAddress(address) { if (address.startsWith('0x')) return '0x' + address.substring(2).toUpperCase(); else return address.toUpperCase(); } export function addressToBytes32Evm(address) { return ethersUtils .hexZeroPad(ethersUtils.hexStripZeros(address), 32) .toLowerCase(); } // For EVM addresses only, kept for backwards compatibility and convenience export function bytes32ToAddress(bytes32) { return ethersUtils.getAddress(bytes32.slice(-40)); } export function addressToBytesEvm(address) { const addrBytes32 = addressToBytes32Evm(address); return Buffer.from(strip0x(addrBytes32), 'hex'); } export function addressToBytesSol(address) { return new PublicKey(address).toBytes(); } export function addressToBytesCosmos(address) { return fromBech32(address).data; } export function addressToBytesCosmosNative(address) { return Buffer.from(strip0x(address), 'hex'); } export function addressToBytesStarknet(address) { const normalizedAddress = normalizeAddressStarknet(address); return num.hexToBytes(normalizedAddress); } export function addressToBytesRadix(address) { let byteArray = new Uint8Array(bech32m.fromWords(bech32m.decode(address).words)); // Ensure the byte array is 32 bytes long, padding from the left if necessary if (byteArray.length < 32) { const paddedArray = new Uint8Array(32); paddedArray.set(byteArray, 32 - byteArray.length); byteArray = paddedArray; } return byteArray; } export function addressToBytesAleo(address) { let aleoAddress = address; if (address.includes('/')) { aleoAddress = address.split('/')[1]; } return new Uint8Array(bech32m.fromWords(bech32m.decode(aleoAddress).words)); } export function addressToBytesTron(address) { const decoded = bs58.decode(address); const payload = decoded.slice(0, -4); const checksum = decoded.slice(-4); const hash1 = ethersUtils.arrayify(ethersUtils.sha256(payload)); const hash2 = ethersUtils.arrayify(ethersUtils.sha256(hash1)); assert(Buffer.from(checksum).equals(new Uint8Array(hash2.slice(0, 4))), 'Invalid Tron address checksum'); return new Uint8Array(payload.slice(1)); // strip 0x41 prefix } export function addressToBytes(address, protocol) { const bytes = routeAddressUtil({ [ProtocolType.Ethereum]: addressToBytesEvm, [ProtocolType.Sealevel]: addressToBytesSol, [ProtocolType.Cosmos]: addressToBytesCosmos, [ProtocolType.CosmosNative]: addressToBytesCosmosNative, [ProtocolType.Starknet]: addressToBytesStarknet, [ProtocolType.Radix]: addressToBytesRadix, [ProtocolType.Aleo]: addressToBytesAleo, [ProtocolType.Tron]: addressToBytesTron, }, address, new Uint8Array(), protocol); assert(bytes.length && !bytes.every((b) => b == 0), 'address bytes must not be empty'); return bytes; } export function addressToByteHexString(address, protocol) { return ensure0x(Buffer.from(addressToBytes(address, protocol)).toString('hex')); } export function addressToBytes32(address, protocol) { // If the address is already bytes32, just return, avoiding a regression // where an already bytes32 address cannot be categorized as a protocol address. if (HEX_BYTES32_REGEX.test(ensure0x(address))) return ensure0x(address); const bytes = addressToBytes(address, protocol); return bytesToBytes32(bytes); } export function bytesToBytes32(bytes) { if (bytes.length > 32) { throw new Error('bytes must be 32 bytes or less'); } // This 0x-prefixes the hex string return ethersUtils.hexZeroPad(ensure0x(Buffer.from(bytes).toString('hex')), 32); } // Pad bytes to a certain length, padding with 0s at the start export function padBytesToLength(bytes, length) { if (bytes.length > length) { throw new Error(`bytes must be ${length} bytes or less`); } return Buffer.concat([Buffer.alloc(length - bytes.length), bytes]); } export function bytesToAddressEvm(bytes) { return bytes32ToAddress(Buffer.from(bytes).toString('hex')); } export function bytesToAddressSol(bytes) { return new PublicKey(bytes).toBase58(); } export function bytesToAddressCosmos(bytes, prefix) { if (!prefix) throw new Error('Prefix required for Cosmos address'); return toBech32(prefix, bytes); } export function bytesToAddressCosmosNative(bytes, prefix) { if (!prefix) throw new Error('Prefix required for Cosmos Native address'); // if the bytes are of length 32 we have to check if the bytes are a cosmos // native account address or an ID from the hyperlane cosmos module. A cosmos // native account address is padded with 12 bytes in front. if (bytes.length === 32) { if (bytes.slice(0, 12).every((b) => !b)) { // since the first 12 bytes are empty we know it is an account address return toBech32(prefix, bytes.slice(12)); } // else it is an ID from the hyperlane cosmos module and we just need // to represent the bytes in hex return ensure0x(Buffer.from(bytes).toString('hex')); } return toBech32(prefix, bytes); } export function bytesToAddressStarknet(bytes) { const hexString = encode.buf2hex(bytes); return addAddressPadding(hexString); } export function bytesToAddressRadix(bytes, prefix) { if (!prefix) throw new Error('Prefix required for Radix address'); // If the bytes array is larger than or equal to 30 bytes, take the last 30 bytes // Otherwise, pad with zeros from the left up to 30 bytes if (bytes.length >= 30) { bytes = bytes.slice(bytes.length - 30); } else { const paddedBytes = new Uint8Array(30); paddedBytes.set(bytes, 30 - bytes.length); bytes = paddedBytes; } return bech32m.encode(prefix, bech32m.toWords(bytes)); } export function bytesToAddressAleo(bytes) { return bech32m.encode('aleo', bech32m.toWords(bytes)); } export function bytesToAddressTron(bytes) { let payload20; if (bytes.length === 32) payload20 = bytes.slice(12); else if (bytes.length === 21 && bytes[0] === 0x41) payload20 = bytes.slice(1); else if (bytes.length === 20) payload20 = bytes; else throw new Error(`Invalid Tron address byte length: ${bytes.length}`); const addressBytes = new Uint8Array([0x41, ...payload20]); const hash1 = ethersUtils.arrayify(ethersUtils.sha256(addressBytes)); const hash2 = ethersUtils.arrayify(ethersUtils.sha256(hash1)); const checksum = hash2.slice(0, 4); const finalBytes = new Uint8Array(addressBytes.length + 4); finalBytes.set(addressBytes); finalBytes.set(checksum, addressBytes.length); return bs58.encode(finalBytes); } export function bytesToProtocolAddress(bytes, toProtocol, prefix) { assert(bytes.length && !bytes.every((b) => b == 0), 'address bytes must not be empty'); if (toProtocol === ProtocolType.Ethereum) { return bytesToAddressEvm(bytes); } else if (toProtocol === ProtocolType.Sealevel) { return bytesToAddressSol(bytes); } else if (toProtocol === ProtocolType.Cosmos) { return bytesToAddressCosmos(bytes, prefix); } else if (toProtocol === ProtocolType.CosmosNative) { return bytesToAddressCosmosNative(bytes, prefix); } else if (toProtocol === ProtocolType.Starknet) { return bytesToAddressStarknet(bytes); } else if (toProtocol === ProtocolType.Radix) { return bytesToAddressRadix(bytes, prefix); } else if (toProtocol === ProtocolType.Aleo) { return bytesToAddressAleo(bytes); } else if (toProtocol === ProtocolType.Tron) { return bytesToAddressTron(bytes); } else { throw new Error(`Unsupported protocol for address ${toProtocol}`); } } export function convertToProtocolAddress(address, protocol, prefix) { const currentProtocol = getAddressProtocolType(address); if (!currentProtocol) throw new Error(`Unknown address protocol for ${address}`); if (currentProtocol === protocol) return address; const addressBytes = addressToBytes(address, currentProtocol); return bytesToProtocolAddress(addressBytes, protocol, prefix); } export function ensure0x(hexstr) { return hexstr.startsWith('0x') ? hexstr : `0x${hexstr}`; } export function strip0x(hexstr) { return hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr; } export function isPrivateKeyEvm(privateKey) { try { return new Wallet(privateKey).privateKey === privateKey; } catch { throw new Error('Provided Private Key is not EVM compatible!'); } } export function hexToBech32mPrefix(hex, prefix, length = 32) { let bytes = addressToBytes(hex); bytes = bytes.slice(bytes.length - length); return bech32m.encode(prefix, bech32m.toWords(bytes)); } export function hexToRadixCustomPrefix(hex, module, prefix, length = 32) { prefix = prefix || 'account_rdx'; prefix = prefix.replace('account', module); return hexToBech32mPrefix(hex, prefix, length); } //# sourceMappingURL=addresses.js.map