import { Bytes } from './bytes.js'; import { Base58AddressFormatVersion, addressContentsToLockingBytecode, lockingBytecodeToAddressContents, decodeBase58Address, decodeCashAddress, disassembleBytecodeBCH, encodeBase58AddressFormat, encodeCashAddress, sha256, ripemd160, } from '@bitauth/libauth'; import type { KnownAddressTypeContents } from '@bitauth/libauth'; export type AddressType = 'P2PK' | 'P2PKH' | 'P2SH20' | 'P2SH32'; export const DEFAULT_ADDRESS_PREFIXES = [ '', 'bitcoincash', 'bchtest', 'bchreg', ]; /** * Address Entity. * * @example * * ```ts * // Import our primitives. * import { Address } from '@xocash/primitives'; * * // Instantiate from a CashAddr (Legacy Addresses also supported). * const address = Address.from('bitcoincash:qpmlpygrtzznznhgsl4c245xgmglpfh0vgcqns0q0r'); * * // Convert to Locking Bytecode. * console.log(address.toLockscriptBytes()); * * // Convert to Legacy Address. * console.log(address.toLegacy()); * * // Convert to Token Address. * console.log(address.toTokenAddress()); * ``` */ export class Address { protected readonly addressContents: KnownAddressTypeContents; protected constructor(addressContents: KnownAddressTypeContents) { this.addressContents = addressContents; } /** * Decode a Cash Address or Legacy Address. * * @param address {string} The address to attempt to decode. * * @throws {Error} If address cannot be decoded. * * @returns {Address} Instance of Address. */ public static from( address: string, tryPrefixes = DEFAULT_ADDRESS_PREFIXES, ): Address { // Attempt to decode as a Cash Address. try { return this.fromCashAddr(address, tryPrefixes); } catch (_error) { // And if that fails, attempt to decode as a Base58 Address. return this.fromLegacy(address); } } /** * Decode a CashAddr into an Address Entity. * * @param cashAddr The CashAddr to decode. * * @throws {Error} If address cannot be decoded. * * @returns {Address} Instance of Address. * * @example * const address = Address.fromCashAddr('bitcoincash:qpeqv9k9j3l7fepp9du24f9pr5jdu7c2mvn4wwldav') */ public static fromCashAddr( cashAddr: string, tryPrefixes: Array = DEFAULT_ADDRESS_PREFIXES, ): Address { // Try all prefixes (including empty string) until we find a valid decode const decodeResult = tryPrefixes .map((prefix) => decodeCashAddress(prefix ? `${prefix}:${cashAddr}` : cashAddr), ) .find((result) => typeof result !== 'string'); if (!decodeResult) { throw new Error(`Cannot decode address '${cashAddr}': ${decodeResult}`); } // Define the encode type in advance. let encodeType: AddressType; // Determine the encoding to use based on the Decoding Result's Type. switch (decodeResult.type) { case 'p2pkh': case 'p2pkhWithTokens': encodeType = 'P2PKH'; break; case 'p2sh': case 'p2shWithTokens': encodeType = decodeResult.payload.length === 32 ? 'P2SH32' : 'P2SH20'; break; default: throw new Error( `Cannot decode address '${cashAddr}': Unsupported address type '${decodeResult.type}'`, ); } // Return new instance of address. return new this({ payload: decodeResult.payload, type: encodeType }); } /** * Decode a Legacy Address into an Address Entity. * * @param legacyAddress The Legacy (Base58) Address to decode. * * @throws {Error} If address cannot be decoded. * * @returns {Address} Instance of Address. */ public static fromLegacy(legacyAddress: string): Address { // Decode the Legacy (Base58) Address. const decodeResult = decodeBase58Address(legacyAddress); // If a string is returned, this indicates an error... if (typeof decodeResult === 'string') { throw new Error( `Cannot decode address '${legacyAddress}': ${decodeResult}`, ); } // Define the encode type in advance. let encodeType: AddressType; // Determine the encoding to use based on the Decoding Result's Type. switch (decodeResult.version) { case Base58AddressFormatVersion.p2pkh: case Base58AddressFormatVersion.p2pkhTestnet: encodeType = 'P2PKH'; break; case Base58AddressFormatVersion.p2sh20: case Base58AddressFormatVersion.p2sh20Testnet: encodeType = 'P2SH20'; break; default: throw new Error( `Cannot decode address '${legacyAddress}': Unsupported address type '${decodeResult.version}'`, ); } return new this({ payload: decodeResult.payload, type: encodeType }); } /** * Instantiate an address from Contract Bytecode. * * @param bytes The contract bytecode * * @throws {Error} If address cannot be instantiated from Contract Bytecode. * * @returns {Address} Instance of Address. */ public static fromRedeemScript( bytes: Uint8Array, type: 'P2SH20' | 'P2SH32' = 'P2SH32', ): Address { // Do an initial SHA256 on the Contract Bytes. let payload = sha256.hash(bytes); if (type === 'P2SH20') { // RIPEMD160 the SHA256 hash. payload = ripemd160.hash(payload); } else if (type === 'P2SH32') { // SHA256 Hash the SHA256 hash a second time. payload = sha256.hash(payload); } // Return a new instance of address. return this.fromRaw({ payload, type }); } /** * Instantiate an address from Contract Bytecode as a hex string. * * @param hex The contract bytecode as hex * * @throws {Error} If address cannot be instantiated from Contract Bytecode. * * @returns {Address} Instance of Address. */ public static fromRedeemScriptHex( hex: string, type: 'P2SH20' | 'P2SH32' = 'P2SH32', ): Address { // Return a new instance of address. return this.fromRedeemScript(Bytes.fromHex(hex).toBytes(), type); } /** * Attempt to instantiate Address from Locking Bytecode. * * @param lockscript The lockscript as bytes or hex. * * @throws {Error} If lock script cannot be decoded into an address. * * @returns Instance of Address. */ public static fromLockscript(lockscript: string | Uint8Array) { return lockscript instanceof Uint8Array ? this.fromLockscriptBytes(lockscript) : this.fromLockscriptHex(lockscript); } /** * Attempt to instantiate Address from Locking Bytecode. * * @param lockScript {Uint8Array} The lockscript bytes. * * @throws {Error} If lock script cannot be decoded into an address. * * @returns {Address} Instance of Address. */ public static fromLockscriptBytes(lockScript: Uint8Array): Address { const addressContents = lockingBytecodeToAddressContents(lockScript); // If the type is not recognized, throw an error... if (addressContents.type === 'unknown') { throw new Error(`Invalid type (${addressContents.type})`); } return this.fromRaw(addressContents); } /** * Attempt to instantiate Address from Locking Bytecode as a hex string. * * @param hex {Uint8Array} The lockscript bytes as hex. * * @throws {Error} If lock script cannot be decoded into an address. * * @returns {Address} Instance of Address. */ public static fromLockscriptHex(hex: string): Address { return this.fromLockscriptBytes(Bytes.fromHex(hex).toBytes()); } /** * Instantiate an address from a Ripemd160 hash. * * @param hash160 {Uint8Array} The hash to use. * @param type {AddressType} The type of the address to instantiate (e.g. P2PKH). * * @throws {Error} If hash160 is not 20 bytes. * * @returns {Address} Instance of Address. */ public static fromHash160Bytes( hash160: Uint8Array, type: AddressType = 'P2PKH', ): Address { // If the Hash160 bytes is not 20 in length, it is invalid... if (hash160.length !== 20) { throw new Error( `Invalid hash160 length: Expected 20 (Received ${hash160.length})`, ); } return this.fromRaw({ payload: hash160, type }); } /** * Instantiate an address from a Ripemd160 hash (as hex). * * @param hash160Hex {string} The hash to use (as hex). * @param type {AddressType} The type of the address to instantiate (e.g. P2PKH). * * @throws {Error} If hash160 is not 20 bytes. * * @returns {Address} Instance of Address. */ public static fromHash160Hex( hash160Hex: string, type: AddressType = 'P2PKH', ): Address { return this.fromHash160Bytes(Bytes.fromHex(hash160Hex).toBytes(), type); } /** * Creates an Address instance from a raw object representation (LibAuth's KnownAddressTypeContents). * * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. * * @param obj - LibAuth Object of type KnownAddressTypeContents * @returns A new Address instance */ public static fromRaw(addressContents: KnownAddressTypeContents) { return new this(addressContents); } /** * Checks whether the given address is a valid Legacy or CashAddr. * * @param address The address. * * @returns boolean indicating whether the address is a valid Legacy Address or CashAddr. */ public static isValid( address: string, tryPrefixes = DEFAULT_ADDRESS_PREFIXES, ): boolean { try { Address.from(address, tryPrefixes); } catch (_error) { return false; } return true; } /** * Checks whether the given address is a valid Legacy Address. * * @param address The address. * * @returns boolean indicating whether the address is a valid Legacy Address. */ public static isValidLegacy(address: string): boolean { try { Address.fromLegacy(address); } catch (_error) { return false; } return true; } /** * Checks whether the given address is a valid CashAddr. * * @param address The address. * * @returns boolean indicating whether the address is a valid CashAddr. */ public static isValidCashAddr( address: string, tryPrefixes = DEFAULT_ADDRESS_PREFIXES, ): boolean { try { Address.fromCashAddr(address, tryPrefixes); } catch (_error) { return false; } return true; } /** * Gets the type of address (P2PKH, P2SH). * * @returns {AddressType} The type of Address. */ public type(): AddressType { return this.addressContents.type; } /** * Gets this address' raw Hash160 (Ripemd160) hash as a Uint8Array. * * @returns {Uint8Array} The Hash160 of this address. */ public toHash160Bytes(): Uint8Array { return this.addressContents.payload; } /** * Gets this address' raw Hash160 (Ripemd16) hash as a hex string. * * @returns {Uint8Array} The Hash160 of this address. */ public toHash160Hex(): string { return Bytes.fromBytes(this.addressContents.payload).toHex(); } /** * Converts this address to CashAddr format. * * @param networkPrefix {'bitcoincash' | 'bchtest' | 'bchreg'} The Cash Address Network prefix. * * @throws {Error} If address cannot be encoded as a CashAddr. * * @returns {string} The address in CashAddr format. */ public toCashAddr( networkPrefix: 'bitcoincash' | 'bchtest' | 'bchreg' = 'bitcoincash', ): string { // Define the encode type in advance. let encodeType: 'p2pkh' | 'p2sh'; // Determine the encoding to use based on the Address Content's Type. switch (this.addressContents.type) { case 'P2PKH': encodeType = 'p2pkh'; break; case 'P2SH20': case 'P2SH32': encodeType = 'p2sh'; break; default: throw new Error( `Unsupported address type: ${this.addressContents.type}`, ); } // NOTE: This function will throw if the payload is invalid. return encodeCashAddress({ prefix: networkPrefix, type: encodeType, payload: this.addressContents.payload, }).address; } /** * Converts this address to a Tokens Address format. * * @param networkPrefix {'bitcoincash' | 'bchtest' | 'bchreg'} The Cash Address Network prefix. * * @throws {Error} If address cannot be encoded as a CashAddr. * * @returns {string} The address in Token Address format. */ public toTokenAddress( networkPrefix: 'bitcoincash' | 'bchtest' | 'bchreg' = 'bitcoincash', ): string { // Define the encode type in advance. let encodeType: 'p2pkhWithTokens' | 'p2shWithTokens'; // Determine the encoding to use based on the Address Content's Type. switch (this.addressContents.type) { case 'P2PKH': encodeType = 'p2pkhWithTokens'; break; case 'P2SH20': case 'P2SH32': encodeType = 'p2shWithTokens'; break; default: throw new Error( `Unsupported address type: ${this.addressContents.type}`, ); } // NOTE: This function will throw if the payload is invalid. return encodeCashAddress({ prefix: networkPrefix, type: encodeType, payload: this.addressContents.payload, }).address; } /** * Converts this address to Legacy (Base58) Address format. * * @throws {Error} If address cannot be encoded as a Legacy Address (Base58). * * @returns {string} The address in Legacy Address format. */ public toLegacy(): string { // Define the encode type in advance. let encodeType: Base58AddressFormatVersion; // Determine the encoding to use based on the Address Content's Type. switch (this.addressContents.type) { case 'P2PKH': encodeType = Base58AddressFormatVersion.p2pkh; break; case 'P2SH20': encodeType = Base58AddressFormatVersion.p2sh20; break; default: throw new Error( `Unsupported address type: ${this.addressContents.type}`, ); } // Encode the address in Base58 format. return encodeBase58AddressFormat(encodeType, this.addressContents.payload); } /** * Get the corresponding lockscript for this address as a Uint8Array. * * @returns {Uint8Array} The lockscript as a Uint8Array. */ public toLockscriptBytes(): Uint8Array { return addressContentsToLockingBytecode(this.addressContents); } /** * Get the corresponding lockscript for this address as a hex string. * * @returns {Uint8Array} The lockscript as a hex string. */ public toLockscriptHex(): string { return Bytes.fromBytes(this.toLockscriptBytes()).toHex(); } /** * Get the corresponding lockscript for this address as ASM. * * @returns {Uint8Array} The lockscript as ASN. */ public toLockscriptAsm(): string { const lockscriptBytes = this.toLockscriptBytes(); const lockscriptAsm = disassembleBytecodeBCH(lockscriptBytes); return lockscriptAsm; } /** * Converts the address to its raw LibAuth representation (KnownAddressTypeContents). * * @returns A LibAuth KnownAddressTypeContents object. */ public toRaw() { return { ...this.addressContents }; } }