474 lines
13 KiB
TypeScript
474 lines
13 KiB
TypeScript
import {
|
|
Base58AddressFormatVersion,
|
|
addressContentsToLockingBytecode,
|
|
lockingBytecodeToAddressContents,
|
|
isHex,
|
|
binToHex,
|
|
hexToBin,
|
|
decodeBase58Address,
|
|
decodeCashAddress,
|
|
disassembleBytecodeBCH,
|
|
encodeBase58AddressFormat,
|
|
encodeCashAddress,
|
|
sha256,
|
|
ripemd160,
|
|
} from '@bitauth/libauth';
|
|
|
|
import type { KnownAddressTypeContents } from '@bitauth/libauth';
|
|
|
|
export type AddressType = 'P2PK' | 'P2PKH' | 'P2SH20' | 'P2SH32';
|
|
|
|
/**
|
|
* Address Entity.
|
|
*/
|
|
export class Address {
|
|
public readonly addressContents: KnownAddressTypeContents;
|
|
|
|
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): Address {
|
|
// Attempt to decode as a Cash Address.
|
|
try {
|
|
return Address.fromCashAddr(address);
|
|
} catch (_error) {
|
|
// And if that fails, attempt to decode as a Base58 Address.
|
|
return Address.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> = ['', 'bitcoincash', 'bchtest', 'bchreg'],
|
|
): 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('Invalid address format');
|
|
}
|
|
|
|
// 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(`Unsupported address type: ${decodeResult.type}`);
|
|
}
|
|
|
|
// Return new instance of address.
|
|
return new Address({ 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(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(`Unsupported address type: ${decodeResult.version}`);
|
|
}
|
|
|
|
return new Address({ payload: decodeResult.payload, type: encodeType });
|
|
}
|
|
|
|
/**
|
|
* Instantiate an address from Contract Bytecode.
|
|
*
|
|
* @param contractBytes The contract bytecode
|
|
*
|
|
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
|
*
|
|
* @returns {Address} Instance of Address.
|
|
*/
|
|
public static fromRedeemScript(
|
|
contractBytes: Uint8Array,
|
|
type: 'P2SH20' | 'P2SH32' = 'P2SH32',
|
|
): Address {
|
|
// Do an initial SHA256 on the Contract Bytes.
|
|
let payload = sha256.hash(contractBytes);
|
|
|
|
if (type === 'P2SH20') {
|
|
// RIPEMD160 the SHA256 hash.
|
|
payload = ripemd160.hash(payload);
|
|
} else if (type === 'P2SH32') {
|
|
// SHA256 Hash the SHA256 hash a seconr time.
|
|
payload = sha256.hash(payload);
|
|
}
|
|
|
|
// Return a new instance of address.
|
|
return new Address({ 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 {
|
|
if (!isHex(hex)) {
|
|
throw new Error('Invalid hex');
|
|
}
|
|
|
|
const contractBytecode = hexToBin(hex);
|
|
|
|
// Return a new instance of address.
|
|
return Address.fromRedeemScript(contractBytecode, type);
|
|
}
|
|
|
|
/**
|
|
* 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 new Address(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 {
|
|
if (!isHex(hex)) {
|
|
throw new Error('Invalid hex');
|
|
}
|
|
|
|
const lockScriptBytes = hexToBin(hex);
|
|
|
|
return Address.fromLockscriptBytes(lockScriptBytes);
|
|
}
|
|
|
|
/**
|
|
* 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 fromHash160(
|
|
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 new Address({ 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 {
|
|
if (!isHex(hash160Hex)) {
|
|
throw new Error('Invalid hex');
|
|
}
|
|
|
|
const hash160Bytes = hexToBin(hash160Hex);
|
|
|
|
return Address.fromHash160(hash160Bytes, type);
|
|
}
|
|
|
|
public static fromRaw(addressContents: KnownAddressTypeContents) {
|
|
return new this(addressContents);
|
|
}
|
|
|
|
public static isValid(address: string): boolean {
|
|
try {
|
|
Address.from(address);
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static isValidLegacy(address: string): boolean {
|
|
try {
|
|
Address.fromLegacy(address);
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static isValidCashAddr(address: string): boolean {
|
|
try {
|
|
Address.fromCashAddr(address);
|
|
} 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 binToHex(this.addressContents.payload);
|
|
}
|
|
|
|
/**
|
|
* 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 binToHex(this.toLockscriptBytes());
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
public toRaw() {
|
|
return { ...this.addressContents };
|
|
}
|
|
}
|