Files
xo-stack/packages/primitives/src/address.ts
T

548 lines
15 KiB
TypeScript

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<string> = 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).toUint8Array(), 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).toUint8Array());
}
/**
* 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).toUint8Array(),
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 };
}
}