@hyperlane-xyz/utils
Version:
General utilities and types for the Hyperlane network
624 lines • 23.3 kB
JavaScript
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