Resolve "WIP: UTXO Selection and Storage"
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user