UNPKG

@hyperlane-xyz/utils

Version:

General utilities and types for the Hyperlane network

308 lines 11 kB
import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; import { Wallet, utils as ethersUtils } from 'ethers'; 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 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 EVM_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/; const SEALEVEL_ZEROISH_ADDRESS_REGEX = /^1+$/; const COSMOS_ZEROISH_ADDRESS_REGEX = /^[a-z]{1,10}?1[0]+$/; 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 getAddressProtocolType(address) { if (!address) return undefined; if (isAddressEvm(address)) { return ProtocolType.Ethereum; } else if (isAddressCosmos(address)) { return ProtocolType.Cosmos; } else if (isAddressSealevel(address)) { return ProtocolType.Sealevel; } 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 isValidAddress(address, protocol) { return routeAddressUtil({ [ProtocolType.Ethereum]: isValidAddressEvm, [ProtocolType.Sealevel]: isValidAddressSealevel, [ProtocolType.Cosmos]: isValidAddressCosmos, }, 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 normalizeAddress(address, protocol) { return routeAddressUtil({ [ProtocolType.Ethereum]: normalizeAddressEvm, [ProtocolType.Sealevel]: normalizeAddressSealevel, [ProtocolType.Cosmos]: normalizeAddressCosmos, }, 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 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), }, 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 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 { 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)); } 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 addressToBytes(address, protocol) { const bytes = routeAddressUtil({ [ProtocolType.Ethereum]: addressToBytesEvm, [ProtocolType.Sealevel]: addressToBytesSol, [ProtocolType.Cosmos]: addressToBytesCosmos, }, 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 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 { 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!'); } } //# sourceMappingURL=addresses.js.map