Resolve "WIP: UTXO Selection and Storage"

This commit is contained in:
Jim Hamill
2025-01-11 08:39:24 +00:00
parent f934216d68
commit 33fd9bac4d
156 changed files with 13304 additions and 4595 deletions
+93 -42
View File
@@ -28,6 +28,25 @@ export class Address {
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.
*
@@ -40,13 +59,19 @@ export class Address {
* @example
* const address = Address.fromCashAddr('bitcoincash:qpeqv9k9j3l7fepp9du24f9pr5jdu7c2mvn4wwldav')
*/
public static fromCashAddr(cashAddr: string): Address {
// CashAddr
const decodeResult = decodeCashAddress(cashAddr);
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 a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
if (!decodeResult) {
throw new Error('Invalid address format');
}
// Define the encode type in advance.
@@ -55,9 +80,11 @@ export class Address {
// 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:
@@ -106,25 +133,6 @@ export class Address {
return new Address({ payload: decodeResult.payload, type: encodeType });
}
/**
* 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 fromCashAddrOrLegacy(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);
}
}
/**
* Instantiate an address from Contract Bytecode.
*
@@ -136,7 +144,7 @@ export class Address {
*/
public static fromRedeemScript(
contractBytes: Uint8Array,
type: 'P2SH20' | 'P2SH32' = 'P2SH32'
type: 'P2SH20' | 'P2SH32' = 'P2SH32',
): Address {
// Do an initial SHA256 on the Contract Bytes.
let payload = sha256.hash(contractBytes);
@@ -164,7 +172,7 @@ export class Address {
*/
public static fromRedeemScriptHex(
hex: string,
type: 'P2SH20' | 'P2SH32' = 'P2SH32'
type: 'P2SH20' | 'P2SH32' = 'P2SH32',
): Address {
if (!isHex(hex)) {
throw new Error('Invalid hex');
@@ -227,12 +235,12 @@ export class Address {
*/
public static fromHash160(
hash160: Uint8Array,
type: AddressType = 'P2PKH'
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})`
`Invalid hash160 length: Expected 20 (Received ${hash160.length})`,
);
}
@@ -251,7 +259,7 @@ export class Address {
*/
public static fromHash160Hex(
hash160Hex: string,
type: AddressType = 'P2PKH'
type: AddressType = 'P2PKH',
): Address {
if (!isHex(hash160Hex)) {
throw new Error('Invalid hex');
@@ -262,10 +270,14 @@ export class Address {
return Address.fromHash160(hash160Bytes, type);
}
public static fromRaw(addressContents: KnownAddressTypeContents) {
return new this(addressContents);
}
public static isValid(address: string): boolean {
try {
Address.fromCashAddrOrLegacy(address);
} catch (error) {
Address.from(address);
} catch (_error) {
return false;
}
@@ -275,7 +287,7 @@ export class Address {
public static isValidLegacy(address: string): boolean {
try {
Address.fromLegacy(address);
} catch (error) {
} catch (_error) {
return false;
}
@@ -285,7 +297,7 @@ export class Address {
public static isValidCashAddr(address: string): boolean {
try {
Address.fromCashAddr(address);
} catch (error) {
} catch (_error) {
return false;
}
@@ -330,25 +342,60 @@ export class Address {
*/
public toCashAddr(
networkPrefix: 'bitcoincash' | 'bchtest' | 'bchreg' = 'bitcoincash',
supportTokens = false
): string {
// Define the encode type in advance.
let encodeType: 'p2pkh' | 'p2sh' | 'p2pkhWithTokens' | 'p2shWithTokens';
let encodeType: 'p2pkh' | 'p2sh';
// Determine the encoding to use based on the Address Content's Type.
switch (this.addressContents.type) {
case 'P2PKH':
encodeType = supportTokens ? 'p2pkhWithTokens' : 'p2pkh';
encodeType = 'p2pkh';
break;
case 'P2SH20':
encodeType = supportTokens ? 'p2shWithTokens' : 'p2sh';
break;
case 'P2SH32':
encodeType = supportTokens ? 'p2shWithTokens' : 'p2sh';
encodeType = 'p2sh';
break;
default:
throw new Error(
`Unsupported address type: ${this.addressContents.type}`
`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}`,
);
}
@@ -381,7 +428,7 @@ export class Address {
break;
default:
throw new Error(
`Unsupported address type: ${this.addressContents.type}`
`Unsupported address type: ${this.addressContents.type}`,
);
}
@@ -419,4 +466,8 @@ export class Address {
return lockscriptAsm;
}
public toRaw() {
return { ...this.addressContents };
}
}
+182
View File
@@ -0,0 +1,182 @@
import { binToHex, hexToBin, isHex, hash256 } from '@bitauth/libauth';
export interface BlockHeaderRaw {
version: number;
previousBlockHash: Uint8Array;
merkleRoot: Uint8Array;
timestamp: number;
bits: number;
nonce: number;
}
/**
* Represents a Bitcoin Cash block header.
* This class handles parsing and serialization of BCH block headers between different formats
* (raw object, bytes, hex) and provides access to individual header fields.
*/
export class BlockHeader {
/**
* Creates a new BlockHeader instance.
* @param version - The block version
* @param previousBlockHash - Hash of the previous block (32 bytes)
* @param merkleRoot - Merkle root of the block's transactions (32 bytes)
* @param timestamp - Block timestamp in seconds since Unix epoch
* @param bits - Compact representation of the target threshold
* @param nonce - Value modified during mining to make the block hash valid
*/
constructor(
private readonly version: number,
private readonly previousBlockHash: Uint8Array,
private readonly merkleRoot: Uint8Array,
private readonly timestamp: number,
private readonly bits: number,
private readonly nonce: number,
) {}
/**
* Creates a BlockHeader instance from a byte array.
* @param bytes - 80-byte array containing the serialized block header
* @returns A new BlockHeader instance
* @throws Error if the byte array length is not 80
*/
static fromBytes(bytes: Uint8Array): BlockHeader {
if (bytes.length !== 80) {
throw new Error('Invalid block header length');
}
const view = new DataView(bytes.buffer);
return new BlockHeader(
view.getInt32(0, true),
bytes.slice(4, 36),
bytes.slice(36, 68),
view.getUint32(68, true),
view.getUint32(72, true),
view.getUint32(76, true),
);
}
/**
* Creates a BlockHeader instance from a hex string.
* @param hex - 160-character hex string representing the block header
* @returns A new BlockHeader instance
* @throws Error if the hex string length is not 160
*/
static fromHex(hex: string): BlockHeader {
// Ensure that the given string is hex.
if (!isHex(hex)) {
throw new Error('The given string is not valid hexadecimal.');
}
return BlockHeader.fromBytes(hexToBin(hex));
}
/**
* Creates a BlockHeader instance from a raw object representation.
* @param obj - Object containing the block header fields
* @returns A new BlockHeader instance
*/
static fromRaw(obj: BlockHeaderRaw): BlockHeader {
return new BlockHeader(
obj.version,
obj.previousBlockHash,
obj.merkleRoot,
obj.timestamp,
obj.bits,
obj.nonce,
);
}
/**
* Serializes the block header to a byte array.
* @returns 80-byte array containing the serialized block header
*/
toBytes(): Uint8Array {
const buffer = new ArrayBuffer(80);
const view = new DataView(buffer);
const bytes = new Uint8Array(buffer);
view.setInt32(0, this.version, true);
bytes.set(this.previousBlockHash, 4);
bytes.set(this.merkleRoot, 36);
view.setUint32(68, this.timestamp, true);
view.setUint32(72, this.bits, true);
view.setUint32(76, this.nonce, true);
return bytes;
}
/**
* Converts the block header to a raw object representation.
* @returns Object containing the block header fields
*/
toRaw(): BlockHeaderRaw {
return {
version: this.version,
previousBlockHash: this.previousBlockHash,
merkleRoot: this.merkleRoot,
timestamp: this.timestamp,
bits: this.bits,
nonce: this.nonce,
};
}
/**
* Converts the block header to a hex string.
* @returns 160-character hex string representing the block header
*/
toHex(): string {
return binToHex(this.toBytes());
}
// Getters
getVersion(): number {
return this.version;
}
getPreviousBlockHash(): Uint8Array {
return this.previousBlockHash;
}
getMerkleRoot(): Uint8Array {
return this.merkleRoot;
}
getTimestamp(): number {
return this.timestamp;
}
getBits(): number {
return this.bits;
}
getNonce(): number {
return this.nonce;
}
/**
* Gets the previous block hash as a hex string.
* @returns 64-character hex string
*/
getPreviousBlockHashHex(): string {
return binToHex(this.previousBlockHash);
}
/**
* Gets the merkle root as a hex string.
* @returns 64-character hex string
*/
getMerkleRootHex(): string {
return binToHex(this.merkleRoot);
}
/**
* Calculates the hash of this block header.
* @returns 32-byte array containing the block hash
*/
getHash(): Uint8Array {
return hash256(this.toBytes());
}
/**
* Gets the block hash as a hex string.
* @returns 64-character hex string
*/
getHashHex(): string {
return binToHex(this.getHash());
}
}
+31 -18
View File
@@ -27,6 +27,19 @@ export class Bytes {
this.bytes = bytes;
}
/**
* Instantiates bytes from an Uint8Array.
*
* @param bytes The bytes as a Uint8Array.
*
* @throws {Error} If Base64 is invalid.
*
* @returns Instance of Bytes.
*/
public static fromBytes(bytes: Uint8Array): Bytes {
return new this(bytes);
}
/**
* Instantiates bytes from a Base64 string.
*
@@ -34,7 +47,7 @@ export class Bytes {
*
* @throws {Error} If Base64 is invalid.
*
* @returns {Bytes} Instance of Bytes.
* @returns Instance of Bytes.
*/
public static fromBase64(base64: string): Bytes {
// If the Base64 is invalid, throw an error.
@@ -42,43 +55,43 @@ export class Bytes {
throw new Error('Invalid base64 string');
}
return new Bytes(base64ToBin(base64));
return new this(base64ToBin(base64));
}
/**
* Instantiates bytes from a BigInt (LE encoded).
*
* @param number {number} The bigint number.
* @param number The bigint number.
*
* @throws {Error} If number is invalid.
*
* @returns {Bytes} Instance of Bytes.
* @returns Instance of Bytes.
*/
public static fromBigInt(number: bigint): Bytes {
return new Bytes(bigIntToBinUintLE(number));
return new this(bigIntToBinUintLE(number));
}
/**
* Instantiates bytes from a BigInt (64 Bit LE encoded).
*
* @param number {number} The bigint number.
* @param number The bigint number.
*
* @throws {Error} If number is invalid.
*
* @returns {Bytes} Instance of Bytes.
* @returns Instance of Bytes.
*/
public static fromBigInt64(number: bigint): Bytes {
return new Bytes(bigIntToBinUint64LE(number));
return new this(bigIntToBinUint64LE(number));
}
/**
* Instantiates bytes from a hex string.
*
* @param hex {string} The hex string.
* @param hex The hex string.
*
* @throws {Error} If hex is invalid.
*
* @returns {Bytes} Instance of Bytes.
* @returns Instance of Bytes.
*/
public static fromHex(hex: string): Bytes {
// If the hex is invalid, throw an error.
@@ -86,24 +99,24 @@ export class Bytes {
throw new Error('Invalid hexidecimal string');
}
return new Bytes(hexToBin(hex));
return new this(hexToBin(hex));
}
/**
* Instantiates bytes from a UTF8 string.
*
* @param utf8 {string} The UTF8 string.
* @param utf8 The UTF8 string.
*
* @returns {Bytes} Instance of Bytes.
* @returns Instance of Bytes.
*/
public static fromUtf8(utf8: string): Bytes {
return new Bytes(utf8ToBin(utf8));
return new this(utf8ToBin(utf8));
}
/**
* Return bytes as a Base64 string.
*
* @returns {string} Bytes as a Base64 string.
* @returns Bytes as a Base64 string.
*/
public toBase64(): string {
return binToBase64(this.bytes);
@@ -116,7 +129,7 @@ export class Bytes {
/**
* Return bytes as a hex string.
*
* @returns {string} Bytes as a hex string.
* @returns Bytes as a hex string.
*/
public toHex(): string {
return binToHex(this.bytes);
@@ -125,7 +138,7 @@ export class Bytes {
/**
* Return bytes as a UTF8 string
*
* @returns {string} Bytes as a UTF8 string.
* @returns Bytes as a UTF8 string.
*/
public toUtf8(): string {
return binToUtf8(this.bytes);
@@ -134,7 +147,7 @@ export class Bytes {
/**
* Return bytes as a Uint8Array
*
* @returns {Uint8Array} Bytes as a Uint8Array.
* @returns Bytes as a Uint8Array.
*/
public toBytes(): Uint8Array {
return this.bytes;
-58
View File
@@ -1,58 +0,0 @@
import {
ripemd160,
sha256,
} from '@bitauth/libauth';
/**
* Crypto Module.
*/
export class Crypto
{
/**
* SHA256 hash the given input
*
* @param input {Uint8Array} The input to hash.
*
* @returns {Uint8Array} The SHA256 Hash of the given input.
*/
public static sha256(input: Uint8Array): Uint8Array
{
return sha256.hash(input);
}
/**
* RIPEMD160 hash the given input
*
* @param input {Uint8Array} The input to hash.
*
* @returns {Uint8Array} The RIPEMD160 Hash of the given input.
*/
public static ripemd160(input: Uint8Array): Uint8Array
{
return ripemd160.hash(input);
}
/**
* Double SHA256 hash the given input
*
* @param input {Uint8Array} The input to hash.
*
* @returns {Uint8Array} The Double SHA256 Hash of the given input.
*/
public static hash256(input: Uint8Array): Uint8Array
{
return sha256.hash(sha256.hash(input));
}
/**
* RIPEMD160 and then SHA256 hash the given input
*
* @param input {Uint16Array} The input to hash.
*
* @returns {Uint8Array} The RIPEMD160+SHA256 Hash of the given input.
*/
public static hash160(input: Uint8Array): Uint8Array
{
return ripemd160.hash(sha256.hash(input));
}
}
+122
View File
@@ -0,0 +1,122 @@
// TODO: These are intended as temporary stand-ins until this functionality has been implemented directly in LibAuth.
// We are doing this so that we may better standardize with the rest of the BCH eco-system in future.
// See: https://github.com/bitauth/libauth/pull/108
import { binToHex, hexToBin } from '@bitauth/libauth';
export const extendedJsonReplacer = function (value: any): any {
if (typeof value === 'bigint') {
return `<bigint: ${value.toString()}n>`;
} else if (value instanceof Uint8Array) {
return `<Uint8Array: ${binToHex(value)}>`;
}
return value;
};
export const extendedJsonReviver = function (value: any): any {
// Define RegEx that matches our Extended JSON fields.
const bigIntRegex = /^<bigint: (?<bigint>[+-]?[0-9]*)n>$/;
const uint8ArrayRegex = /^<Uint8Array: (?<hex>[a-f0-9]*)>$/;
// Only perform a check if the value is a string.
// NOTE: We can skip all other values as all Extended JSON encoded fields WILL be a string.
if (typeof value === 'string') {
// Check if this value matches an Extended JSON encoded bigint.
const bigintMatch = value.match(bigIntRegex);
if (bigintMatch) {
// Access the named group directly instead of using array indices
const { bigint } = bigintMatch.groups!;
// Return the value casted to bigint.
return BigInt(bigint);
}
const uint8ArrayMatch = value.match(uint8ArrayRegex);
if (uint8ArrayMatch) {
// Access the named group directly instead of using array indices
const { hex } = uint8ArrayMatch.groups!;
// Return the value casted to bigint.
return hexToBin(hex);
}
}
// Return the original value.
return value;
};
export const encodeExtendedJsonObject = function (value: any): any {
// If this is an object type (and it is not null - which is technically an "object")...
// ... and it is not an ArrayBuffer (e.g. Uint8Array) which is also technically an "object...
if (
typeof value === 'object' &&
value !== null &&
!ArrayBuffer.isView(value)
) {
// If this is an array, recursively call this function on each value.
if (Array.isArray(value)) {
return value.map(encodeExtendedJsonObject);
}
// Declare object to store extended JSON entries.
const encodedObject: any = {};
// Iterate through each entry and encode it to extended JSON.
for (const [key, valueToEncode] of Object.entries(value)) {
encodedObject[key] = encodeExtendedJsonObject(valueToEncode);
}
// Return the extended JSON encoded object.
return encodedObject;
}
// Return the replaced value.
return extendedJsonReplacer(value);
};
export const decodeExtendedJsonObject = function (value: any): any {
// If this is an object type (and it is not null - which is technically an "object")...
// ... and it is not an ArrayBuffer (e.g. Uint8Array) which is also technically an "object...
if (
typeof value === 'object' &&
value !== null &&
!ArrayBuffer.isView(value)
) {
// If this is an array, recursively call this function on each value.
if (Array.isArray(value)) {
return value.map(decodeExtendedJsonObject);
}
// Declare object to store decoded JSON entries.
const decodedObject: any = {};
// Iterate through each entry and decode it from extended JSON.
for (const [key, valueToEncode] of Object.entries(value)) {
decodedObject[key] = decodeExtendedJsonObject(valueToEncode);
}
// Return the extended JSON encoded object.
return decodedObject;
}
// Return the revived value.
return extendedJsonReviver(value);
};
export const encodeExtendedJson = function (
value: any,
space: number | undefined = undefined,
): string {
const replacedObject = encodeExtendedJsonObject(value);
const stringifiedObject = JSON.stringify(replacedObject, null, space);
return stringifiedObject;
};
export const decodeExtendedJson = function (json: string): any {
const parsedObject = JSON.parse(json);
const revivedObject = decodeExtendedJsonObject(parsedObject);
return revivedObject;
};
+137
View File
@@ -0,0 +1,137 @@
import {
isHex,
isBase64,
binToHex,
base64ToBin,
hexToBin,
utf8ToBin,
ripemd160,
sha256,
} from '@bitauth/libauth';
/**
* Represents a hash primitive that provides various cryptographic hashing capabilities.
* This class handles conversions between different formats (bytes, hex, UTF-8) and
* supports multiple hashing algorithms (SHA256, RIPEMD160, Hash160, Hash256).
*/
export class Hasher {
/**
* Creates a new Hash instance.
* @param bytes - The byte array representation of the data to be hashed
*/
constructor(public bytes: Uint8Array) {}
/**
* Creates a Hash instance from a byte array.
* @param bytes - The byte array to create the hash from
* @returns A new Hash instance containing the provided bytes
*/
public static fromBytes(bytes: Uint8Array) {
return new this(bytes);
}
/**
* Creates a Hash instance from a Base64 string.
* @param base64 - The Base64 string to create the hash from
* @returns A new Hash instance containing the bytes converted from the Base64 string
* @throws {Error} If the provided string is not valid Base64
*/
public static fromBase64(base64: string) {
// Ensure that the given string is base64.
if (!isBase64(base64)) {
throw new Error('The given string is not valid Base64.');
}
return new this(base64ToBin(base64));
}
/**
* Creates a Hash instance from a hexadecimal string.
* @param hex - The hexadecimal string to create the hash from
* @returns A new Hash instance containing the bytes converted from the hex string
* @throws {Error} If the provided string is not valid hexadecimal
*/
public static fromHex(hex: string) {
// Ensure that the given string is hex.
if (!isHex(hex)) {
throw new Error('The given string is not valid hexadecimal.');
}
return new this(hexToBin(hex));
}
/**
* Creates a Hash instance from a UTF-8 string.
* @param utf8 - The UTF-8 string to create the hash from
* @returns A new Hash instance containing the bytes converted from the UTF-8 string
*/
public static fromUTF8(utf8: string) {
return new this(utf8ToBin(utf8));
}
/**
* Computes the SHA256 hash of the stored bytes.
* @returns A Uint8Array containing the SHA256 hash
*/
public toSHA256Bytes(): Uint8Array {
return sha256.hash(this.bytes);
}
/**
* Computes the SHA256 hash of the stored bytes and returns it as a hexadecimal string.
* @returns The SHA256 hash as a hexadecimal string
*/
public toSHA256Hex(): string {
return binToHex(this.toSHA256Bytes());
}
/**
* Computes the RIPEMD160 hash of the stored bytes.
* @returns A Uint8Array containing the RIPEMD160 hash
*/
public toRIPEMD160Bytes(): Uint8Array {
return ripemd160.hash(this.bytes);
}
/**
* Computes the RIPEMD160 hash of the stored bytes and returns it as a hexadecimal string.
* @returns The RIPEMD160 hash as a hexadecimal string
*/
public toRIPEMD160Hex(): string {
return binToHex(this.toRIPEMD160Bytes());
}
/**
* Computes the Hash160 (RIPEMD160(SHA256(x))) of the stored bytes.
* This is commonly used in cryptocurrency addresses.
* @returns A Uint8Array containing the Hash160 value
*/
public toHash160Bytes(): Uint8Array {
return ripemd160.hash(this.toSHA256Bytes());
}
/**
* Computes the Hash160 (RIPEMD160(SHA256(x))) of the stored bytes and returns it as a hexadecimal string.
* @returns The Hash160 value as a hexadecimal string
*/
public toHash160Hex(): string {
return binToHex(this.toHash160Bytes());
}
/**
* Computes the Hash256 (SHA256(SHA256(x))) of the stored bytes.
* This is commonly used for computing block hashes in Bitcoin.
* @returns A Uint8Array containing the Hash256 value
*/
public toHash256Bytes(): Uint8Array {
return sha256.hash(this.toSHA256Bytes());
}
/**
* Computes the Hash256 (SHA256(SHA256(x))) of the stored bytes and returns it as a hexadecimal string.
* @returns The Hash256 value as a hexadecimal string
*/
public toHash256Hex(): string {
return binToHex(sha256.hash(this.toSHA256Bytes()));
}
}
+100 -17
View File
@@ -5,6 +5,7 @@ import {
encodeHdPrivateKey,
decodeHdPrivateKey,
deriveHdPath,
deriveHdPathRelative,
deriveHdPublicNode,
deriveHdPrivateNodeFromSeed,
deriveSeedFromBip39Mnemonic,
@@ -40,7 +41,21 @@ export class HDPrivateNode {
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
* @returns The created HD Private Node.
*/
public static fromBytes(seed: Uint8Array): HDPrivateNode {
// Return a new HD Private Node from the given seed.
return this.fromSeed(seed);
}
/**
* Creates a HD Private Node from a raw seed.
*
* @param seed The raw seed bytes.
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns The created HD Private Node.
*/
public static fromSeed(seed: Uint8Array): HDPrivateNode {
// If the seed is not 32 bytes, throw an error.
@@ -59,7 +74,7 @@ export class HDPrivateNode {
}
// Return a new HD Private Node from the given seed.
return new HDPrivateNode(deriveResult);
return new this(deriveResult);
}
/**
@@ -69,14 +84,14 @@ export class HDPrivateNode {
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
* @returns The created HD Private Node.
*/
public static fromMnemonic(mnemonic: string): HDPrivateNode {
// Derive the seed from the mnemonic.
const seed = deriveSeedFromBip39Mnemonic(mnemonic);
// Return a new HDPrivateNode from the given seed.
return HDPrivateNode.fromSeed(seed);
return this.fromSeed(seed);
}
/**
@@ -86,7 +101,7 @@ export class HDPrivateNode {
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
* @returns The created HD Private Node.
*/
public static fromXPriv(xpriv: string): HDPrivateNode {
// Attempt to decode the XPriv Key.
@@ -98,7 +113,18 @@ export class HDPrivateNode {
}
// Return a new HD Private Node from the given XPriv.
return new HDPrivateNode(decodeResult.node);
return new this(decodeResult.node);
}
/**
* Creates a HD Private Node from raw node data.
*
* @param node The raw node data.
*
* @returns The created HD Private Node.
*/
public static fromRaw(node: HdPrivateNodeValid) {
return new this(node);
}
/**
@@ -106,7 +132,7 @@ export class HDPrivateNode {
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
* @returns The created HD Private Node.
*/
public static generateRandom(): HDPrivateNode {
// Attempt to derive HD Private Node from randomly generated bytes.
@@ -118,23 +144,29 @@ export class HDPrivateNode {
}
// Return the new HD Private Node from the random bytes.
return new HDPrivateNode(deriveResult);
return new this(deriveResult);
}
/**
* Returns the XPriv string for this node.
* Checks if the given XPriv string is a valid HD Private Key.
*
* @returns {string} The XPriv string for this node.
* @param xpriv The XPriv string to check.
*
* @returns True if the XPriv string is valid, false otherwise.
*/
public toString(): string {
// Return the Mainnet XPriv.
return this.toXPriv();
public static isXPriv(xpriv: string): boolean {
try {
this.fromXPriv(xpriv);
return true;
} catch (_error) {
return false;
}
}
/**
* Gets the Private Key entity for this node.
*
* @returns {PrivateKey} The Private Key Entity for this node.
* @returns The Private Key Entity for this node.
*/
public privateKey(): PrivateKey {
// Return a Private Key Entity from the private key of our node.
@@ -148,7 +180,7 @@ export class HDPrivateNode {
*
* @throws {Error} If HD Private Node cannot be derived.
*
* @returns {HDPrivateNode} The derived HD Private Node
* @returns The derived HD Private Node
*/
public derivePath(path: string): HDPrivateNode {
// Attempt to derive the given path.
@@ -163,10 +195,32 @@ export class HDPrivateNode {
return new HDPrivateNode(derivePathResult);
}
/**
* Derives a HD Private Node from the given relative BIP32 path.
*
* @param path {string} The BIP32 Path (e.g. 0/0).
*
* @throws {Error} If HD Private Node cannot be derived.
*
* @returns The derived HD Private Node
*/
public deriveRelativePath(path: string): HDPrivateNode {
// Attempt to derive the given path.
const derivePathResult = deriveHdPathRelative(this.node, path);
// If a string is returned, this indicates an error...
if (typeof derivePathResult === 'string') {
throw new Error(derivePathResult);
}
// Return a new HD Private Node Entity derived from the given path.
return new HDPrivateNode(derivePathResult);
}
/**
* Derives a HD Public Node from this Private Node.
*
* @returns {HDPublicNode} The derived HD Public Node
* @returns The derived HD Public Node
*/
public deriveHDPublicNode(): HDPublicNode {
// Derive a HD Public Node from this HD Private Node.
@@ -176,12 +230,22 @@ export class HDPrivateNode {
return new HDPublicNode(publicNode);
}
/**
* Returns the Private Key of this node (as a Uint8Array).
*
* @returns The Node's Private Key (as a Uint8Array).
*/
public toBytes(): Uint8Array {
// Encode the XPriv from our node info.
return this.node.privateKey;
}
/**
* Converts this node to an XPriv Key for export.
*
* @param network {HdKeyNetwork} The network to encode for.
*
* @returns {string} The XPriv Key.
* @returns The XPriv Key.
*/
public toXPriv(network: HdKeyNetwork = 'mainnet'): string {
// Create our node info structure.
@@ -193,4 +257,23 @@ export class HDPrivateNode {
// Encode the XPriv from our node info.
return encodeHdPrivateKey(nodeInfo).hdPrivateKey;
}
/**
* Returns the XPriv string for this node.
*
* @returns The XPriv string for this node.
*/
public toString(): string {
// Return the Mainnet XPriv.
return this.toXPriv();
}
/**
* Returns the raw node data.
*
* @returns The raw node data.
*/
public toRaw(): HdPrivateNodeValid {
return this.node;
}
}
+30 -13
View File
@@ -50,20 +50,23 @@ export class HDPublicNode {
return new HDPublicNode(decodeResult.node);
}
/**
* Returns the XPub string for this node.
*
* @returns {string} The XPub string for this node.
*/
public toString(): string {
// Return the Mainnet XPub.
return this.toXPub();
public static fromRaw(node: HdPublicNodeValid) {
return new this(node);
}
public static isXPub(xpub: string): boolean {
try {
this.fromXPub(xpub);
return true;
} catch (_error) {
return false;
}
}
/**
* Gets the Public Key Entity for this node.
*
* @returns {PublicKey} The Public Key Entity for this node.
* @returns The Public Key Entity for this node.
*/
public publicKey(): PublicKey {
// Return a Public Key Entity from the public key of our node.
@@ -73,11 +76,11 @@ export class HDPublicNode {
/**
* Derives a HD Public Node from the given BIP32 path.
*
* @param path {string} The BIP32 Path (e.g. m/44'/145'/0'/0/1).
* @param path The BIP32 Path (e.g. m/44'/145'/0'/0/1).
*
* @throws {Error} If HD Public Node cannot be derived.
*
* @returns {HDPrivateNode} The derived HD Public Node
* @returns The derived HD Public Node
*/
public derivePath(path: string): HDPublicNode {
// Attempt to derive the given path.
@@ -95,9 +98,9 @@ export class HDPublicNode {
/**
* Converts this node to an XPub Key for export.
*
* @param network {HdKeyNetwork} The network to encode for.
* @param network The network to encode for.
*
* @returns {string} The XPub Key.
* @returns The XPub Key.
*/
public toXPub(network: HdKeyNetwork = 'mainnet'): string {
// Create our node info structure.
@@ -109,4 +112,18 @@ export class HDPublicNode {
// Encode the XPub from our node info.
return encodeHdPublicKey(nodeInfo).hdPublicKey;
}
public toRaw(): HdPublicNodeValid {
return this.node;
}
/**
* Returns the XPub string for this node.
*
* @returns {string} The XPub string for this node.
*/
public toString(): string {
// Return the Mainnet XPub.
return this.toXPub();
}
}
+6 -1
View File
@@ -1,11 +1,16 @@
export * from './address.js';
export * from './block-header.js';
export * from './bytes.js';
export * from './crypto.js';
export * from './ext-json.js';
export * from './hasher.js';
export * from './hd-private-node.js';
export * from './hd-public-node.js';
export * from './mnemonic.js';
export * from './private-key.js';
export * from './public-key.js';
export * from './satoshis.js';
export * from './script.js';
export * from './transaction.js';
export * from './transaction-hash.js';
export * from './utils.js';
export * from './utxo.js';
+246
View File
@@ -0,0 +1,246 @@
import {
generateBip39MnemonicNonStandard,
bip39WordListEnglish,
} from '@bitauth/libauth';
export type MnemonicRaw = {
words: string[];
passphrase?: string;
}
/**
* Class representing a BIP39 Mnemonic phrase.
* Supports 12, 15, 18, 21, and 24 word phrases with optional passphrase.
*/
export class Mnemonic {
public static VALID_WORD_COUNTS = new Set([12, 15, 18, 21, 24]);
/**
* Instantiate Mnemonic from a phrase string.
*
* @param phrase The complete phrase (mnemonic words + optional passphrase).
* @param wordList Optional wordlist (defaults to English BIP-39 wordlist).
* @throws {Error} If the mnemonic is invalid.
*/
constructor(private mnemonic: MnemonicRaw) {}
/**
* Instantiates a mnemonic from a string or array of words
*
* @param phraseOrWords The mnemonic phrase as a string or array of words
* @param dictionary Optional wordlist (defaults to English BIP-39 wordlist).
* @throws {Error} If mnemonic could not be generated.
* @returns Instance of Mnemonic.
*/
public static from(phraseOrWords: string | Array<string>, wordList: Array<string> = bip39WordListEnglish) {
// If this is a list of words...
if (Array.isArray(phraseOrWords)) {
return this.fromWords(phraseOrWords, wordList);
}
return this.fromString(phraseOrWords, wordList);
}
/**
* Instantiates a mnemonic from a string
*
* @param phrase The mnemonic phrase as a string
* @param dictionary Optional wordlist (defaults to English BIP-39 wordlist).
* @throws {Error} If mnemonic could not be generated.
* @returns Instance of Mnemonic.
*/
public static fromString(phrase: string, wordList: Array<string> = bip39WordListEnglish) {
// Split into words and remove any whitespace.
const allWords = phrase
.trim()
.split(/\s+/)
.map((word) => word.trim());
return this.fromWords(allWords, wordList);
}
/**
* Instantiates a mnemonic from an array of words.
*
* @param words Array of words (strings) that represent the mnemonic
* @param dictionary Optional wordlist (defaults to English BIP-39 wordlist).
* @throws {Error} If mnemonic could not be generated.
* @returns Instance of Mnemonic.
*/
public static fromWords(words: Array<string>, wordList: Array<string> = bip39WordListEnglish) {
// Check if last word is a passphrase by looking at total count
const hasPassphrase =
words.length === 13 ||
words.length === 16 ||
words.length === 19 ||
words.length === 22 ||
words.length === 25;
// Parse words and optional passphrase
const mnemonicRaw: MnemonicRaw = {
words: hasPassphrase ? words.slice(0, -1) : words,
}
if(hasPassphrase) {
mnemonicRaw.passphrase = words[words.length - 1]
}
// Validate word count
if (!Mnemonic.VALID_WORD_COUNTS.has(mnemonicRaw.words.length)) {
throw new Error(
`Invalid word count ${mnemonicRaw.words.length}. BIP-39 requires 12, 15, 18, 21, or 24 words` +
`${hasPassphrase ? ' (excluding passphrase)' : ''}`,
);
}
// Normalize and validate words against dictionary
const wordSet = new Set(wordList);
// Convert all words to lower-case.
mnemonicRaw.words = mnemonicRaw.words.map((word) => word.toLowerCase());
// Find any words that are not present in our dictionary (i.e. invalid words).
const invalidWords = mnemonicRaw.words.reduce((invalidWords, word) => {
if (!wordSet.has(word)) {
invalidWords.push(word);
}
return invalidWords;
}, [] as Array<string>);
// If there are invalid words that are not present in the provided dictionary, throw an error.
if (invalidWords.length) {
throw new Error(
`The following words are not valid with the provided dictionary: ${invalidWords.join(' ')}`,
);
}
return new this(mnemonicRaw);
}
/**
* Instantiates a mnemonic from its raw (serialized) mnemonic representation.
*
* @remarks this method is unsafe and should only use trusted data.
*
* @param mnemonicRaw The raw menmonic data.
* @throws {Error} If mnemonic could not be generated.
* @returns Instance of Mnemonic.
*/
public static fromRaw(mnemonicRaw: MnemonicRaw) {
return new this(mnemonicRaw);
}
/**
* Generates a random mnemonic phrase.
*
* @param dictionary Optional wordlist (defaults to English BIP-39 wordlist).
* @throws {Error} If mnemonic could not be generated.
* @returns Instance of Mnemonic.
*/
public static generateRandom(
passphrase?: string,
dictionary: string[] = bip39WordListEnglish,
): Mnemonic {
// Attempt to generate mnemonic.
const mnemonicResult = generateBip39MnemonicNonStandard(dictionary);
if (typeof mnemonicResult === 'string') {
throw new Error(mnemonicResult);
}
// Turn phrase parts (phrase) into an array so that we can append the passphrase later (if present).
const words = mnemonicResult.phrase.split(/\s+/);
// If there is a passphrase, append it.
if (passphrase) {
words.push(passphrase);
}
// Return the generated phrase (with passphrase appended - if present).
return this.fromWords(words, dictionary);
}
/**
* Validates a mnemonic phrase.
*
* @param phraseOrWords The mnemonic phrase or words to validate.
* @param wordList Optional wordlist (defaults to English BIP-39 wordlist).
* @returns boolean indicating if the phrase is valid.
*/
public static isValid(
phraseOrWords: string | Array<string>,
wordList: string[] = bip39WordListEnglish,
): boolean {
try {
this.from(phraseOrWords, wordList);
return true;
} catch {
return false;
}
}
/**
* Checks if a word exists in the provided dictionary.
*
* @param word Word to check.
* @param dictionary Optional wordlist (defaults to English BIP-39 wordlist).
* @returns boolean indicating if the word is valid.
*/
public static isValidWord(
word: string,
dictionary: string[] = bip39WordListEnglish,
): boolean {
return dictionary.includes(word.toLowerCase());
}
/**
* Gets the passphrase if one exists.
*
* @returns The passphrase or undefined if none exists.
*/
public getPassphrase(): string | undefined {
return this.mnemonic.passphrase;
}
/**
* Returns the complete phrase as a string.
*
* @param includePassphrase Whether to include the passphrase in the output.
* @returns Complete phrase as a string.
*/
public toString(includePassphrase: boolean = true): string {
// Join the menmonic words, separating them using a space character.
const mnemonicWords = this.mnemonic.words.join(' ');
// If we should include the passphrase - and a passphrase is present...
if (includePassphrase && this.mnemonic.passphrase) {
return `${mnemonicWords} ${this.mnemonic.passphrase}`;
}
// Return the words.
return mnemonicWords;
}
/**
* Gets the number of words in the mnemonic (excluding passphrase).
*
* @returns Number of words.
*/
public toWords(includePassphrase: boolean = true): Array<string> {
const words = [ ...this.mnemonic.words ];
if(this.mnemonic.passphrase && includePassphrase) {
words.push(this.mnemonic.passphrase);
}
return words;
}
/**
* Returns a serializable represntation of the class
*
* @returns A serializable reprentation of the class.
*/
public toRaw(): MnemonicRaw {
return { ...this.mnemonic };
}
}
+194 -66
View File
@@ -7,7 +7,6 @@ import {
generatePrivateKey,
hexToBin,
isHex,
binToUtf8,
utf8ToBin,
bigIntToCompactUint,
secp256k1,
@@ -37,7 +36,7 @@ export class PrivateKey {
// Ensure that the Private Key is valid.
if (!secp256k1.validatePrivateKey(bytes)) {
throw new Error(
`Private Key is invalid: Must be >= 0x01 and <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140.`
`Private Key is invalid: Must be >= 0x01 and <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140.`,
);
}
@@ -70,7 +69,7 @@ export class PrivateKey {
// Ensure that the given string is hex.
if (!isHex(privateKeyHex)) {
throw new Error(
`The given string (${privateKeyHex}) is not valid hexadecimal.`
`The given string (${privateKeyHex}) is not valid hexadecimal.`,
);
}
@@ -90,7 +89,7 @@ export class PrivateKey {
*/
public static fromWIF(
wif: string,
removePrefixes = ['bitcoincash:', 'wif:']
removePrefixes = ['bitcoincash:', 'wif:'],
): PrivateKey {
// Remove the prefixes from the WIF (if they exist).
const wifWithoutPrefixes = removePrefixes.reduce((result, prefix) => {
@@ -105,6 +104,22 @@ export class PrivateKey {
throw new Error(decodeResult);
}
// Ensure the Private Key is valid by checking the length (32 bytes = uncompressed private key, 33 bytes = compressed private key).
// NOTE: This is to work around a LibAuth bug where decodePrivateKeyWif does not verify the version of the Base58 string.
// This means that we will get a false positive on Base58/Legacy addresses as being valid WIFs.
// Once the below PR is merged into LibAuth (and we've updated to the new version), we can remove the following code.
// https://github.com/bitauth/libauth/pull/147
const isValidPrivateKey =
decodeResult.privateKey.length === 32 ||
decodeResult.privateKey.length === 33;
// If it is not a valid private key, then it is not a WIF,
if (!isValidPrivateKey) {
throw new Error(
`Expected 32 or 33 bytes WIF, received ${decodeResult.privateKey.length}`,
);
}
// Return a new Private Key entity.
return new PrivateKey(decodeResult.privateKey);
}
@@ -120,6 +135,15 @@ export class PrivateKey {
return new PrivateKey(bytes);
}
public static isWIF(wif: string, removePrefixes = ['bitcoincash:', 'wif:']) {
try {
this.fromWIF(wif, removePrefixes);
return true;
} catch (_error) {
return false;
}
}
/**
* Derive the public key from this private key.
*
@@ -141,101 +165,83 @@ export class PrivateKey {
}
/**
* Sign a message hash with this Private Key using a Recoverable Compact Signature.
* Sign a message using a recoverable compact signature format.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param message The message to sign, either as a Uint8Array or string.
* If a string is provided, it will be converted to UTF-8 bytes.
*
* @throws {Error} If message hash cannot be signed.
* @throws {Error} If message cannot be signed.
*
* @returns {Uint8Array} The generated signature.
* @returns The generated recoverable compact signature.
*/
public signMessageHashRecoverableCompact(
messageHash: Uint8Array
): RecoverableSignature {
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashRecoverableCompact(
this.bytes,
messageHash
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
public signMessageRecoverableCompact(message: Uint8Array | string) {
if (typeof message === 'string') {
message = utf8ToBin(message);
}
// Return the signature.
return signResult;
const messageHash = sha256.hash(message);
return this.signMessageHashRecoverableCompact(messageHash);
}
/**
* Sign a message hash with this Private Key using a Compact Signature.
* Sign a message using a compact signature format.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param message The message to sign, either as a Uint8Array or string.
* If a string is provided, it will be converted to UTF-8 bytes.
*
* @throws {Error} If message hash cannot be signed.
* @throws {Error} If message cannot be signed.
*
* @returns {Uint8Array} The generated signature.
* @returns The generated compact signature.
*/
public signMessageHashCompact(messageHash: Uint8Array): Uint8Array {
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashCompact(
this.bytes,
messageHash
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
public signMessageCompact(message: Uint8Array | string) {
if (typeof message === 'string') {
message = utf8ToBin(message);
}
// Return the signature.
return signResult;
const messageHash = sha256.hash(message);
return this.signMessageHashCompact(messageHash);
}
/**
* Sign a message hash with this Private Key using a Schnorr Signature.
* Sign a message using the Schnorr signature scheme.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param message The message to sign, either as a Uint8Array or string.
* If a string is provided, it will be converted to UTF-8 bytes.
*
* @throws {Error} If message hash cannot be signed.
* @throws {Error} If message cannot be signed.
*
* @returns {Uint8Array} The generated signature.
* @returns The generated Schnorr signature.
*/
public signMessageHashSchnorr(messageHash: Uint8Array): Uint8Array {
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashSchnorr(
this.bytes,
messageHash
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
public signMessageSchnorr(message: Uint8Array | string) {
if (typeof message === 'string') {
message = utf8ToBin(message);
}
// Return the signature.
return signResult;
const messageHash = sha256.hash(message);
return this.signMessageHashSchnorr(messageHash);
}
/**
* Sign a message hash with this Private Key using a DER Signature.
* Sign a message using DER (Distinguished Encoding Rules) format.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param message The message to sign, either as a Uint8Array or string.
* If a string is provided, it will be converted to UTF-8 bytes.
*
* @throws {Error} If message hash cannot be signed.
* @throws {Error} If message cannot be signed.
*
* @returns {Uint8Array} The generated signature.
* @returns The generated DER signature.
*/
public signMessageHashDER(messageHash: Uint8Array): Uint8Array {
const signResult = secp256k1.signMessageHashDER(this.bytes, messageHash);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
public signMessageDER(message: Uint8Array | string) {
if (typeof message === 'string') {
message = utf8ToBin(message);
}
// Return the signature.
return signResult;
const messageHash = sha256.hash(message);
return this.signMessageHashDER(messageHash);
}
/**
@@ -279,6 +285,128 @@ export class PrivateKey {
return electronEncoding;
}
/**
* Sign a message hash with this Private Key using a Recoverable Compact Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashRecoverableCompact(
messageHash: Uint8Array | string,
): RecoverableSignature {
// If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
}
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashRecoverableCompact(
this.bytes,
messageHash,
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using a Compact Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashCompact(messageHash: Uint8Array | string): Uint8Array {
// If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
}
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashCompact(
this.bytes,
messageHash,
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using a Schnorr Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashSchnorr(messageHash: Uint8Array | string): Uint8Array {
// If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
}
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashSchnorr(
this.bytes,
messageHash,
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using a DER Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashDER(messageHash: Uint8Array | string): Uint8Array {
// If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
}
const signResult = secp256k1.signMessageHashDER(this.bytes, messageHash);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Convert the raw bytes of this Private Key to hexadecimal encoding.
*
+83 -27
View File
@@ -7,6 +7,7 @@ import {
secp256k1,
sha256,
ripemd160,
utf8ToBin,
} from '@bitauth/libauth';
/**
@@ -19,7 +20,7 @@ export class PublicKey {
/**
* Construct a new Public Key.
*
* @param bytes {Uint8Array} The raw bytes (33) for the Public Key.
* @param bytes The raw bytes (33) for the Public Key.
*/
constructor(bytes: Uint8Array) {
// Ensure that exactly 33 Bytes are given.
@@ -33,17 +34,30 @@ export class PublicKey {
/**
* Create a Public Key entity from the given hexadecimal string.
*
* @param publicKeyHex {string} Hexadecimal encoding of the Public Key bytes.
* @param publicKeyBytes Uint8Array containing the Public Key bytes.
*
* @throws {Error} If Public Key cannot be created.
*
* @returns {PublicKey} The created Public Key entity.
* @returns The created Public Key entity.
*/
public static fromBytes(publicKeyBytes: Uint8Array): PublicKey {
return new PublicKey(publicKeyBytes);
}
/**
* Create a Public Key entity from the given hexadecimal string.
*
* @param publicKeyHex Hexadecimal encoding of the Public Key bytes.
*
* @throws {Error} If Public Key cannot be created.
*
* @returns The created Public Key entity.
*/
public static fromHex(publicKeyHex: string): PublicKey {
// Ensure that the given string is hex.
if (!isHex(publicKeyHex)) {
throw new Error(
`The given string (${publicKeyHex}) is not valid hexadecimal.`
`The given string (${publicKeyHex}) is not valid hexadecimal.`,
);
}
@@ -57,7 +71,7 @@ export class PublicKey {
/**
* Derive the Hash160 for this Public Key.
*
* @returns {Uint8Array} The Hash160 of the Public Key.
* @returns The Hash160 of the Public Key.
*/
public deriveHash160(): Uint8Array {
// SHA256 Hash the public key.
@@ -73,7 +87,7 @@ export class PublicKey {
/**
* Derive the Hash160 as Hex for this Public Key.
*
* @returns {string} The Hash160 of the Public Key as hex.
* @returns The Hash160 of the Public Key as hex.
*/
public deriveHash160Hex(): string {
const hash160Bytes = this.deriveHash160();
@@ -84,7 +98,7 @@ export class PublicKey {
/**
* Derive the Address for this Public Key.
*
* @returns {Address} The Address for this Public Key.
* @returns The Address for this Public Key.
*/
public deriveAddress(): Address {
// Get the RIPEMD160 Hash of the Public Key.
@@ -94,22 +108,48 @@ export class PublicKey {
return Address.fromHash160(ripemd160Hash, 'P2PKH');
}
public verifyMessageSchnorr(
message: Uint8Array | string,
signature: Uint8Array | string,
) {
if (typeof message === 'string') {
message = utf8ToBin(message);
}
const messageHash = sha256.hash(message);
return this.verifyMessageHashSchnorr(messageHash, signature);
}
public verifyMessageDER(
message: Uint8Array | string,
signature: Uint8Array | string,
) {
if (typeof message === 'string') {
message = utf8ToBin(message);
}
const messageHash = sha256.hash(message);
return this.verifyMessageHashDER(messageHash, signature);
}
/**
* Verify that a compact message hash was signed with the corresponding Private Key.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param signature {Uint8Array) The signature of the message.
* @param messageHash The hash of the message.
* @param signature The signature of the message.
*
* @returns {boolean} True if signature and message hash verifies, false otherwise..
* @returns True if signature and message hash verifies, false otherwise..
*/
public verifyMessageHashCompact(
messageHash: Uint8Array,
signature: Uint8Array
signature: Uint8Array,
): boolean {
const validity = secp256k1.verifySignatureCompact(
signature,
this.bytes,
messageHash
messageHash,
);
return validity;
@@ -118,19 +158,27 @@ export class PublicKey {
/**
* Verify that a DER message hash was signed with the corresponding Private Key.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param signature {Uint8Array) The signature of the message.
* @param messageHash The hash of the message.
* @param signature The signature of the message.
*
* @returns {boolean} True if signature and message hash verifies, false otherwise..
* @returns True if signature and message hash verifies, false otherwise..
*/
public verifyMessageHashDER(
messageHash: Uint8Array,
signature: Uint8Array
messageHash: Uint8Array | string,
signature: Uint8Array | string,
): boolean {
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
}
if (typeof signature === 'string') {
signature = hexToBin(signature);
}
const validity = secp256k1.verifySignatureDER(
signature,
this.bytes,
messageHash
messageHash,
);
return validity;
@@ -139,19 +187,27 @@ export class PublicKey {
/**
* Verify that a Schnorr message hash was signed with the corresponding Private Key.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param signature {Uint8Array) The signature of the message.
* @param messageHash The hash of the message.
* @param signature The signature of the message.
*
* @returns {boolean} True if signature and message hash verifies, false otherwise..
* @returns True if signature and message hash verifies, false otherwise..
*/
public verifyMessageHashSchnorr(
messageHash: Uint8Array,
signature: Uint8Array
messageHash: Uint8Array | string,
signature: Uint8Array | string,
): boolean {
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
}
if (typeof signature === 'string') {
signature = hexToBin(signature);
}
const validity = secp256k1.verifySignatureSchnorr(
signature,
this.bytes,
messageHash
messageHash,
);
return validity;
@@ -160,7 +216,7 @@ export class PublicKey {
/**
* Encode the raw bytes of this Public Key to a hexadecimal representation.
*
* @returns {string} The raw bytes Public Key encoded as Hexadecimal.
* @returns The raw bytes Public Key encoded as Hexadecimal.
*/
public toHex(): string {
return binToHex(this.bytes);
@@ -169,7 +225,7 @@ export class PublicKey {
/**
* Get the raw bytes of this Public Key.
*
* @returns {Uint8Array} The raw bytes of the Public Key.
* @returns The raw bytes of the Public Key.
*/
public toBytes(): Uint8Array {
// Return the raw bytes for this Public Key.
@@ -179,7 +235,7 @@ export class PublicKey {
/**
* Return the hexadecimal representation of the raw bytes of the Public Key.
*
* @returns {string} The raw bytes of the Public Key as hexadecimal.
* @returns The raw bytes of the Public Key as hexadecimal.
*/
public toString(): string {
// Return the raw Public Key byes encoded as a hexadecimal string.
+14
View File
@@ -6,6 +6,7 @@ import {
binToHex,
hexToBin,
isHex,
parseScript,
} from '@bitauth/libauth';
/**
@@ -23,6 +24,19 @@ export class Script {
this.bytes = bytes;
}
/**
* Create a Script Entity from the given Assembly String.
*
* @param asm {string} String of assembly code.
*
* @throws {Error} If script cannot be decoded.
*
* @returns {Script} The created Script Entity.
*/
public static fromBytes(bytes: Uint8Array): Script {
return new Script(bytes);
}
/**
* Create a Script Entity from the given Assembly String.
*
+78 -89
View File
@@ -1,106 +1,95 @@
import {
binToHex,
hexToBin,
hashTransaction,
isHex,
} from '@bitauth/libauth';
import { binToHex, hexToBin, hashTransaction, isHex } from '@bitauth/libauth';
/**
* Transaction Hash Primitive.
*/
export class TransactionHash
{
public readonly bytes: Uint8Array;
export class TransactionHash {
public readonly bytes: Uint8Array;
/**
* Instantiates TransactionHash from a Transaction Hash's bytes.
*
* @param bytes {Uint8Array} The Transaction Hash's bytes.
*/
constructor(bytes: Uint8Array)
{
// If bytes is not exactly 32, then this is an invalid transaction hash...
if(bytes.length !== 32)
{
throw(new Error(`Invalid Transaction Hash: Expected 32 bytes but received ${bytes.length}`));
}
/**
* Instantiates TransactionHash from a Transaction Hash's bytes.
*
* @param bytes {Uint8Array} The Transaction Hash's bytes.
*/
constructor(bytes: Uint8Array) {
// If bytes is not exactly 32, then this is an invalid transaction hash...
if (bytes.length !== 32) {
throw new Error(
`Invalid Transaction Hash: Expected 32 bytes but received ${bytes.length}`,
);
}
this.bytes = bytes;
}
this.bytes = bytes;
}
/**
* Instantiates TransactionHash from a hex string.
*
* @param hex {string} The hex string.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromHex(hex: string): TransactionHash
{
if(!isHex(hex))
{
throw(new Error('Invalid hex provided'));
}
/**
* Instantiates TransactionHash from a hex string.
*
* @param hex {string} The hex string.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromHex(hex: string): TransactionHash {
if (!isHex(hex)) {
throw new Error('Invalid hex provided');
}
const bytes = hexToBin(hex);
const bytes = hexToBin(hex);
return new TransactionHash(bytes);
}
return new TransactionHash(bytes);
}
/**
* Instantiates TransactionHash from a Transaction's bytes.
*
* @param transactionBytes {Uint8Array} The Transaction's bytes.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionBytes(transactionBytes: Uint8Array): TransactionHash
{
const transactionHash = hashTransaction(transactionBytes);
/**
* Instantiates TransactionHash from a Transaction's bytes.
*
* @param transactionBytes {Uint8Array} The Transaction's bytes.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionBytes(
transactionBytes: Uint8Array,
): TransactionHash {
const transactionHash = hashTransaction(transactionBytes);
return new TransactionHash(hexToBin(transactionHash));
}
return new TransactionHash(hexToBin(transactionHash));
}
/**
* Instantiates TransactionHash from a Transaction's bytes in hex.
*
* @param transactionHex {Uint8Array} The Transaction's bytes in hex.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionHex(transactionHex: string): TransactionHash
{
if(!isHex(transactionHex))
{
throw(new Error('Invalid hex provided'));
}
/**
* Instantiates TransactionHash from a Transaction's bytes in hex.
*
* @param transactionHex {Uint8Array} The Transaction's bytes in hex.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionHex(transactionHex: string): TransactionHash {
if (!isHex(transactionHex)) {
throw new Error('Invalid hex provided');
}
const bytes = hexToBin(transactionHex);
const bytes = hexToBin(transactionHex);
return TransactionHash.fromTransactionBytes(bytes);
}
return TransactionHash.fromTransactionBytes(bytes);
}
/**
* Returns transaction hash as a hex string.
*
* @returns {string} Hex string of the Transaction Hash.
*/
public toHex(): string
{
return binToHex(this.bytes);
}
/**
* Returns transaction hash as a hex string.
*
* @returns {string} Hex string of the Transaction Hash.
*/
public toHex(): string {
return binToHex(this.bytes);
}
/**
* Returns transaction hash as a Uint8Array.
*
* @returns {Uint8Array} Uint8Array of the Transaction Hash.
*/
public toBytes(): Uint8Array
{
return this.bytes;
}
/**
* Returns transaction hash as a Uint8Array.
*
* @returns {Uint8Array} Uint8Array of the Transaction Hash.
*/
public toBytes(): Uint8Array {
return this.bytes;
}
}
+71 -34
View File
@@ -10,19 +10,47 @@ import {
import type { TransactionBCH } from '@bitauth/libauth';
/**
* Transaction Entity.
* Transaction Primitive.
*/
export class Transaction {
constructor(public transaction: TransactionBCH) {}
/**
* Create a Transaction Entity from the given byte data.
* Instantiates a transaction from either:
*
* @param bytes {Uint8Array} The raw byte data of the transaction.
* 1. Transaction bytecode
* 2. Transaction bytecode (in hexadecimal format)
*
* @param transaction The transaction in one of the above formats.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns {Transaction} The created Transaction Entity.
* @returns The created Transaction Primitive.
*/
public static from(transaction: Uint8Array | string) {
// Handle string (hex) case
if (typeof transaction === 'string') {
return Transaction.fromHex(transaction);
}
// Handle Uint8Array case
if (transaction instanceof Uint8Array) {
return Transaction.fromBytes(transaction);
}
throw new TypeError(
'Invalid transaction input type. Expected Uint8Array, or hex string',
);
}
/**
* Create a Transaction Primitive from the given byte data.
*
* @param bytes The raw byte data of the transaction.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns {Transaction} The created Transaction Primitive.
*/
public static fromBytes(bytes: Uint8Array): Transaction {
// Attempt to decode the raw transaction bytes.
@@ -33,18 +61,18 @@ export class Transaction {
throw new Error(decodeResult);
}
// Return a new instance of the Transaction Entity created from the decode result.
// Return a new instance of the Transaction Primitive created from the decode result.
return new Transaction(decodeResult);
}
/**
* Create a Transaction Entity from the given byte data encoded as hexadecimal string.
* Create a Transaction Primitive from the given byte data encoded as hexadecimal string.
*
* @param hex {string} The raw byte data (encoded as hexadecimal string) of the transaction.
* @param hex The raw byte data (encoded as hexadecimal string) of the transaction.
*
* @throws {Error} If transaction cannot be decoded.
*
* @returns {Transaction} The created Transaction Entity.
* @returns {Transaction} The created Transaction Primitive.
*/
public static fromHex(hex: string): Transaction {
// Ensure that the given string is hex.
@@ -59,12 +87,17 @@ export class Transaction {
return Transaction.fromBytes(bytes);
}
public static fromAny(bytesOrHex: Uint8Array | string) {
if (typeof bytesOrHex === 'string') {
return Transaction.fromHex(bytesOrHex);
}
return Transaction.fromBytes(bytesOrHex);
/**
* Create a Transaction Primitive from LibAuth's TransactionBCH type.
*
* @remarks This method is UNSAFE and should only be used where input is trusted.
*
* @param transaction The transaction object (as per LibAuth).
*
* @returns The created Transaction Primitive.
*/
public static fromRaw(transaction: TransactionBCH) {
return new this(transaction);
}
public toBytes(): Uint8Array {
@@ -91,6 +124,30 @@ export class Transaction {
return this.toHash().toBytes();
}
public toRaw(): TransactionBCH {
return this.transaction;
}
public isCoinbase(): boolean {
// A coinbase transaction MUST only have one input
if (this.getInputs().length !== 1) {
return false;
}
const input = this.getInputs()[0];
// Check if the outpoint transaction hash is all zeros (32 bytes of 0x00)
const isOutpointZero = input.outpointTransactionHash.every(
(byte) => byte === 0x00,
);
// Check if the outpoint index is 0xFFFFFFFF (or -1 in signed 32-bit integer representation)
const isOutpointIndexMax = input.outpointIndex === 0xffffffff;
// Both conditions must be true for a coinbase transaction
return isOutpointZero && isOutpointIndexMax;
}
public getVersion() {
return this.transaction.version;
}
@@ -106,24 +163,4 @@ export class Transaction {
public getLocktime() {
return this.transaction.locktime;
}
public isCoinbase(): boolean {
// A coinbase transaction MUST only have one input
if (this.getInputs().length !== 1) {
return false;
}
const input = this.getInputs()[0];
// Check if the outpoint transaction hash is all zeros (32 bytes of 0x00)
const isOutpointZero = input.outpointTransactionHash.every(
(byte) => byte === 0x00
);
// Check if the outpoint index is 0xFFFFFFFF (or -1 in signed 32-bit integer representation)
const isOutpointIndexMax = input.outpointIndex === 0xffffffff;
// Both conditions must be true for a coinbase transaction
return isOutpointZero && isOutpointIndexMax;
}
}
+168
View File
@@ -0,0 +1,168 @@
import { Transaction } from './transaction.js';
import { binToHex } from '@bitauth/libauth';
/**
* Sort transactions by the order in which they should be broadcast.
*/
export function sortTransactions(
transactions: Array<Transaction>,
): Array<Transaction> {
const transactionMap = transactions.reduce(
(map, transaction) => {
const txHash = transaction.toHash().toHex();
map[txHash] = transaction;
return map;
},
{} as Record<string, Transaction>,
);
const visited = new Set<string>();
const order: Transaction[] = [];
const visit = (txid: string) => {
if (visited.has(txid)) return;
const tx = transactionMap[txid];
if (!tx) return;
visited.add(txid);
// First, visit all transactions that this transaction depends on
for (const input of tx.getInputs()) {
const outpointTransactionHashHex = binToHex(
input.outpointTransactionHash,
);
visit(outpointTransactionHashHex);
}
order.push(tx);
};
// Start the topological sort from each transaction
for (const tx of Object.values(transactionMap)) {
visit(tx.toHash().toHex());
}
return order;
}
export class ExtMap<T> extends Map<PropertyKey, T> {
/**
* Creates a new ExtMap from an array using a key selector
*/
static fromArray<T, K extends PropertyKey>(
array: readonly T[],
keySelector: (item: T) => K,
): ExtMap<T> {
const collection = new ExtMap<T>();
array.forEach((item) => {
const key = keySelector(item);
collection.set(key, item);
});
return collection;
}
static fromObject<T>(object: { [key: PropertyKey]: T }): ExtMap<T> {
const collection = new ExtMap<T>();
for (let [key, value] of Object.entries(object)) {
collection.set(key, value);
}
return collection;
}
/**
* Flattens an array of ExtMaps into a single ExtMap
* Last collection's values take precedence for duplicate keys
*/
static flatten<T>(collections: ExtMap<T>[]): ExtMap<T> {
const flattened = new ExtMap<T>();
collections.forEach((collection) => {
for (const [key, value] of collection.entries()) {
flattened.set(key, value);
}
});
return flattened;
}
getOrFail(key: string): T {
const value = this.get(key);
if (!value) {
throw new Error(`Failed to find key "${key}"`);
}
return value;
}
/**
* Returns a new ExtMap with filtered entries
*/
filter(predicate: (value: T, key: PropertyKey) => boolean): ExtMap<T> {
const filtered = new ExtMap<T>();
for (const [key, value] of this.entries()) {
if (predicate(value, key)) {
filtered.set(key, value);
}
}
return filtered;
}
/**
* Returns a new ExtMap with mapped values
*/
map<U>(mapper: (value: T, key: PropertyKey) => U): ExtMap<U> {
const mapped = new ExtMap<U>();
for (const [key, value] of this.entries()) {
mapped.set(key, mapper(value, key));
}
return mapped;
}
/**
* Returns a new ExtMap with entries sorted by the given comparator
* @param comparator - Function to determine the sort order
* @returns A new ExtMap with sorted entries
*/
sort(comparator: (a: T, b: T) => number): ExtMap<T> {
const entries = Array.from(this.entries());
const sorted = new ExtMap<T>();
entries
.sort(([, valueA], [, valueB]) => comparator(valueA, valueB))
.forEach(([key, value]) => {
sorted.set(key, value);
});
return sorted;
}
/**
* Converts the collection to a plain object
*/
toObject(): Record<PropertyKey, T> {
return Object.fromEntries(this.entries());
}
/**
* Converts the collection to an array of values
*/
toArray(): T[] {
return Array.from(this.values());
}
/**
* Converts the collection to a Set
* @param callback - Optional function to transform keys before adding to Set
* @returns A Set containing either the keys or transformed values
*/
toSet<U = PropertyKey>(callback?: (key: PropertyKey, value: T) => U): Set<U> {
const set = new Set<U>();
for (const [key, value] of this.entries()) {
set.add(callback ? callback(key, value) : (key as U));
}
return set;
}
}
+78
View File
@@ -0,0 +1,78 @@
import { type Input, type Output, binToHex } from '@bitauth/libauth';
import { ExtMap } from './utils.js';
// TODO: I want to make these a class.
// But it's really painful to do in TS.
// TS cannot extend an interface, it can only implement them.
// This means that we would have to redefine each property explicitly.
// But a getId function on UTXOs would be ergonomic.
// TODO: Maybe just compose it and have an "output" object, ya' dingus.
export class Outpoint {
constructor(
public outpointTransactionHash: Uint8Array,
public outpointIndex: number,
) {}
static fromInput(
input: Pick<Input, 'outpointTransactionHash' | 'outpointIndex'>,
) {
return new this(input.outpointTransactionHash, input.outpointIndex);
}
toString() {
const outpointHashHex = binToHex(this.outpointTransactionHash);
return `${outpointHashHex}:${this.outpointIndex}`;
}
}
export class UTXO {
constructor(
public outpoint: Outpoint,
public output: Output,
) {}
static from(outpoint: Outpoint, output: Output) {
return new this(outpoint, output);
}
flatten() {
return {
...this.outpoint,
...this.output,
};
}
}
export type UTXOSet = ExtMap<UTXO>;
// TODO: Sort me better. If you work out a better name for your UTXO Set above, you might be able to put me in that class.
// Or maybe I do just work best as a stand-alone function?
// “Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.” — John Carmack
export function calculateBalanceSats(unspents: Array<UTXO>) {
// Iterate over each unspent and return the total satoshi balance.
return unspents.reduce((totalSats, unspent) => {
return (totalSats = totalSats + unspent.output.valueSatoshis);
}, 0n);
}
export type TokenBalances = { [categoryId: string]: bigint };
export function calculateBalanceTokens(unspents: Array<UTXO>) {
// Declare an object to store token category IDs to their balances.
const tokenBalances: TokenBalances = {};
// Iterate through each unspent and, if it has a token, add it to our balances.
unspents.forEach((unspent) => {
if (unspent.output.token) {
const tokenCategoryHex = binToHex(unspent.output.token.category);
const currentTokenTotal = tokenBalances[tokenCategoryHex] || 0n;
const newTotal = currentTokenTotal + unspent.output.token.amount;
tokenBalances[tokenCategoryHex] = newTotal;
}
});
// Return the token balances map.
return tokenBalances;
}