From f53fc4b456057d320d3148d17cd4bb300f94f2b0 Mon Sep 17 00:00:00 2001 From: Jim Hamill Date: Mon, 13 Jan 2025 09:43:11 +0000 Subject: [PATCH] Amend Primitives/Tooling --- packages/docs/src/.vitepress/config.ts | 24 ++- packages/docs/src/.vitepress/theme/custom.css | 4 + .../docs/src/tools/primitive-functions.ts | 128 +++++++------- packages/docs/src/tools/primitives.md | 15 +- .../docs/src/tools/stack/utils-functions.ts | 56 +++++- packages/docs/src/tools/stack/utils.md | 25 ++- packages/primitives/README.md | 2 +- packages/primitives/src/address.ts | 80 +++++++-- packages/primitives/src/block-header.ts | 60 +++++-- packages/primitives/src/bytes.ts | 2 +- packages/primitives/src/ext-json.ts | 8 +- packages/primitives/src/hd-private-node.ts | 162 +++++++++--------- packages/primitives/src/hd-public-node.ts | 83 +++++---- packages/primitives/src/index.ts | 1 - packages/primitives/src/mnemonic.ts | 46 +++-- packages/primitives/src/private-key.ts | 131 +++++++++----- packages/primitives/src/public-key.ts | 107 ++++++++---- packages/primitives/src/satoshis.ts | 4 +- packages/primitives/src/script.ts | 62 ++----- packages/primitives/src/transaction-hash.ts | 71 +++++++- packages/primitives/src/transaction.ts | 84 +++++++-- packages/primitives/tests/mnemonic.spec.ts | 78 +++++---- packages/stack/src/storage/base-store.ts | 4 +- packages/stack/src/storage/store-indexeddb.ts | 6 +- .../src/utils/{encryption.ts => encrypter.ts} | 12 +- .../src => stack/src/utils}/hasher.ts | 0 packages/stack/src/utils/index.ts | 3 +- packages/stack/src/wallet/wallet-hd.ts | 2 +- packages/stack/tests/utils/encryption.spec.ts | 20 +-- 29 files changed, 817 insertions(+), 463 deletions(-) rename packages/stack/src/utils/{encryption.ts => encrypter.ts} (91%) rename packages/{primitives/src => stack/src/utils}/hasher.ts (100%) diff --git a/packages/docs/src/.vitepress/config.ts b/packages/docs/src/.vitepress/config.ts index ee86dd8..b090876 100644 --- a/packages/docs/src/.vitepress/config.ts +++ b/packages/docs/src/.vitepress/config.ts @@ -129,7 +129,6 @@ export default defineConfig({ link: '/tools/primitives#block-header', }, { text: 'Bytes', link: '/tools/primitives#bytes' }, - { text: 'Hasher', link: '/tools/primitives#hasher' }, { text: 'HD Private Node', link: '/tools/primitives#hd-private-node', @@ -164,18 +163,35 @@ export default defineConfig({ }, ], }, - /* { text: 'Stack', collapsed: false, items: [ + /* { text: 'Blockchain', - items: [{ text: 'Blockchain Electrum' }], + link: '/tools/stack/blockchain', + items: [ + { + text: 'Electrum', + link: '/tools/stack/blockchain#encryption', + }, + ], + }, + */ + { + text: 'Utils', + link: '/tools/stack/utils', + items: [ + { + text: 'Encrypter', + link: '/tools/stack/utils#encrypter', + }, + { text: 'Hasher', link: '/tools/stack/utils#hasher' }, + ], }, ], }, - */ ], }, ], diff --git a/packages/docs/src/.vitepress/theme/custom.css b/packages/docs/src/.vitepress/theme/custom.css index fe51e07..c659d16 100644 --- a/packages/docs/src/.vitepress/theme/custom.css +++ b/packages/docs/src/.vitepress/theme/custom.css @@ -30,6 +30,10 @@ body { .vp-doc h4 { font-size: 14px; opacity: 0.75; + color: var(--primary); + padding-top: 3em; + border-top: 1px solid var(--vp-c-text-1); + margin-top: 3em; } .VPSidebarItem .text { diff --git a/packages/docs/src/tools/primitive-functions.ts b/packages/docs/src/tools/primitive-functions.ts index 4941d39..5d5ee0b 100644 --- a/packages/docs/src/tools/primitive-functions.ts +++ b/packages/docs/src/tools/primitive-functions.ts @@ -4,7 +4,8 @@ import { Bytes, encodeExtendedJson, decodeExtendedJson, - Hasher, + // Encrypter, + // Hasher, HDPrivateNode, HDPublicNode, Mnemonic, @@ -116,10 +117,10 @@ export const address = { export const blockHeader = { from: [ { - label: 'From Hex', - inputs: ['UTF-8'], + label: 'From', + inputs: ['Hex'], models: [''], - callback: (args: [string]) => BlockHeader.fromHex(...args), + callback: (args: [string]) => BlockHeader.from(...args), }, ], to: [ @@ -193,67 +194,30 @@ export const bytes = { ], }; -export const hasher = { - from: [ - { - label: 'From Hex', - inputs: ['Hex'], - models: [''], - callback: (args: [string]) => Hasher.fromHex(...args), - }, - { - label: 'From Base64', - inputs: ['Base64'], - models: [''], - callback: (args: [string]) => Hasher.fromBase64(...args), - }, - { - label: 'From UTF-8', - inputs: ['UTF-8'], - models: [''], - callback: (args: [string]) => Hasher.fromUTF8(...args), - }, - ], - to: [ - { - label: 'To Sha256 Hex', - callback: (hasher: Hasher) => hasher.toSHA256Hex(), - }, - { - label: 'To RipeMD160 Hex', - callback: (hasher: Hasher) => hasher.toRIPEMD160Hex(), - }, - { - label: 'To Hash160 Hex', - callback: (hasher: Hasher) => hasher.toHash160Hex(), - }, - { - label: 'To Hash256 Hex', - callback: (hasher: Hasher) => hasher.toHash256Hex(), - }, - ], -}; - export const hdPrivateNode = { from: [ { label: 'Generate Random', - inputs: ['Derivation Path'], - models: ["m/44'/145'/0'/0/1"], callback: () => HDPrivateNode.generateRandom(), }, { - label: 'From Mnemonic', - inputs: ['Mnemonic', 'Derivation Path'], - models: ['', "m/44'/145'/0'/0/1"], + label: 'From Mnemonic Phrase', + inputs: ['Mnemonic'], + models: [''], callback: (args: [string]) => HDPrivateNode.fromMnemonic(...args), }, { label: 'From XPriv Key', - inputs: ['XPriv Key', 'Derivation Path'], - models: ['', "m/44'/145'/0'/0/1"], + inputs: ['XPriv Key'], + models: [''], callback: (args: [string]) => HDPrivateNode.fromXPriv(...args), }, + { + label: 'From Seed', + inputs: ['Seed (Hex)'], + models: [''], + callback: (args: [string]) => HDPrivateNode.fromSeed(...args), + }, { label: 'From Raw', inputs: ['Raw (LibAuth Format/ExtJSON Encoded)'], @@ -283,6 +247,18 @@ export const hdPrivateNode = { hdPrivateNode.deriveHDPublicNode().toXPub(), }, ], + methods: [ + { + label: 'Derive Path', + inputs: ['BIP32 Path'], + models: ["m/44'/145'/0'/0/0"], + callback: (hdPrivateNode: HDPrivateNode, args: [string]) => + hdPrivateNode + .derivePath(...args) + .toPrivateKey() + .toWif(), + }, + ], }; export const hdPublicNode = { @@ -317,30 +293,50 @@ export const hdPublicNode = { encodeExtendedJson(hdPublicNode.toRaw(), 2), }, ], + methods: [ + { + label: 'Derive Path', + inputs: ['BIP32 Public Key Path'], + models: ['M/0/0'], + callback: (hdPublicNode: HDPublicNode, args: [string]) => + hdPublicNode + .derivePath(...args) + .toPublicKey() + .deriveAddress() + .toCashAddr(), + }, + ], }; export const mnemonic = { from: [ { label: 'Generate Random', - callback: () => Mnemonic.generateRandom(), + inputs: ['Passphrase (Optional)'], + models: [undefined], + callback: (args: [string?]) => Mnemonic.generateRandom(...args), }, { - label: 'From String', - inputs: ['String'], + label: 'From Phrase', + inputs: ['Mnemonic Phrase'], models: [''], - callback: (args: [string]) => Mnemonic.fromString(...args), + callback: (args: [string]) => Mnemonic.fromPhrase(...args), extendedInput: true, }, ], to: [ { - label: 'To String (without passphrase)', - callback: (mnemonic: Mnemonic) => mnemonic.toString(false), + label: 'To Seed', + callback: (mnemonic: Mnemonic) => + Bytes.fromBytes(mnemonic.toSeed()).toHex(), }, { - label: 'To String (with passphrase)', - callback: (mnemonic: Mnemonic) => mnemonic.toString(true), + label: 'To Phrase (without passphrase)', + callback: (mnemonic: Mnemonic) => mnemonic.toPhrase(false), + }, + { + label: 'To Phrase (with passphrase)', + callback: (mnemonic: Mnemonic) => mnemonic.toPhrase(true), }, { label: 'Words', @@ -361,16 +357,10 @@ export const privateKey = { callback: () => PrivateKey.generateRandom(), }, { - label: 'From Hex', - inputs: ['Hex'], + label: 'From Hex or WIF', + inputs: ['Hex or WIF'], models: [''], - callback: (args: [string]) => PrivateKey.fromHex(...args), - }, - { - label: 'From WIF', - inputs: ['WIF'], - models: [''], - callback: (args: [string]) => PrivateKey.fromWIF(...args), + callback: (args: [string]) => PrivateKey.from(...args), }, ], to: [ diff --git a/packages/docs/src/tools/primitives.md b/packages/docs/src/tools/primitives.md index 23f27de..f7746f1 100644 --- a/packages/docs/src/tools/primitives.md +++ b/packages/docs/src/tools/primitives.md @@ -6,7 +6,6 @@ import { address, blockHeader, bytes, - hasher, hdPrivateNode, hdPublicNode, mnemonic, @@ -58,18 +57,6 @@ import { /> -## Hasher - -
-

A utility tool for computing various cryptographic hashes commonly used in Bitcoin Cash.

- - - -
- ## HD Private Node
@@ -78,6 +65,7 @@ import {
@@ -90,6 +78,7 @@ import { diff --git a/packages/docs/src/tools/stack/utils-functions.ts b/packages/docs/src/tools/stack/utils-functions.ts index d6f3a5f..94419a2 100644 --- a/packages/docs/src/tools/stack/utils-functions.ts +++ b/packages/docs/src/tools/stack/utils-functions.ts @@ -1,14 +1,13 @@ import { Bytes } from '@xocash/primitives'; +import { Encrypter, Hasher } from '@xocash/stack'; -import { Encryption } from '@xocash/stack'; - -export const encryption = { +export const encrypter = { from: [ { label: 'From Passphrase', inputs: ['Passphrase'], models: [''], - callback: (args: [string]) => Encryption.fromPassphrase(...args), + callback: (args: [string]) => Encrypter.fromPassphrase(...args), extendedInput: true, }, ], @@ -18,17 +17,58 @@ export const encryption = { label: 'Encrypt', inputs: ['UTF8'], models: ['Your text here'], - callback: async (encryption: Encryption, args: [string]) => - Bytes.fromBytes(await encryption.encrypt(...args)).toHex(), + callback: async (encrypter: Encrypter, args: [string]) => + Bytes.fromBytes(await encrypter.encrypt(...args)).toHex(), }, { label: 'Decrypt', inputs: ['Hex'], models: ['Encrypted Data (hex)'], - callback: async (encryption: Encryption, args: [string]) => + callback: async (encrypter: Encrypter, args: [string]) => Bytes.fromBytes( - await encryption.decrypt(Bytes.fromHex(...args).toBytes()), + await encrypter.decrypt(Bytes.fromHex(...args).toBytes()), ).toUtf8(), }, ], }; + +export const hasher = { + from: [ + { + label: 'From Hex', + inputs: ['Hex'], + models: [''], + callback: (args: [string]) => Hasher.fromHex(...args), + }, + { + label: 'From Base64', + inputs: ['Base64'], + models: [''], + callback: (args: [string]) => Hasher.fromBase64(...args), + }, + { + label: 'From UTF-8', + inputs: ['UTF-8'], + models: [''], + callback: (args: [string]) => Hasher.fromUTF8(...args), + }, + ], + to: [ + { + label: 'To Sha256 Hex', + callback: (hasher: Hasher) => hasher.toSHA256Hex(), + }, + { + label: 'To RipeMD160 Hex', + callback: (hasher: Hasher) => hasher.toRIPEMD160Hex(), + }, + { + label: 'To Hash160 Hex', + callback: (hasher: Hasher) => hasher.toHash160Hex(), + }, + { + label: 'To Hash256 Hex', + callback: (hasher: Hasher) => hasher.toHash256Hex(), + }, + ], +}; diff --git a/packages/docs/src/tools/stack/utils.md b/packages/docs/src/tools/stack/utils.md index d3777f7..71af8b7 100644 --- a/packages/docs/src/tools/stack/utils.md +++ b/packages/docs/src/tools/stack/utils.md @@ -3,7 +3,8 @@ import PrimitiveToolVue, { type FromFunction, type ToFunction } from '@theme/com // Import our tool functions import { - encryption + encrypter, + hasher, } from './utils-functions.ts' @@ -11,15 +12,27 @@ import { # Utils -## Encryption +## Encrypter
-

A Utility tool for converting between different address formats.

+

A Utility tool for encrypting data using AES-GCM.

+

Additional parameters (Salt, Key Length, IV, etc) are available via the API.

+
+ +## Hasher + +
+

A utility tool for computing various cryptographic hashes commonly used in Bitcoin Cash.

+ +
diff --git a/packages/primitives/README.md b/packages/primitives/README.md index f959a5e..c816fc5 100644 --- a/packages/primitives/README.md +++ b/packages/primitives/README.md @@ -2,4 +2,4 @@ **NOTE: This package is EXTREMELY UNSTABLE and WILL BREAK frequently. DO NOT USE.** -A collection of useful BCH Primitives. +A collection of immutable primitives for use with BCH. diff --git a/packages/primitives/src/address.ts b/packages/primitives/src/address.ts index 644e2df..7b573a2 100644 --- a/packages/primitives/src/address.ts +++ b/packages/primitives/src/address.ts @@ -18,13 +18,20 @@ import type { KnownAddressTypeContents } from '@bitauth/libauth'; export type AddressType = 'P2PK' | 'P2PKH' | 'P2SH20' | 'P2SH32'; +export const DEFAULT_ADDRESS_PREFIXES = [ + '', + 'bitcoincash', + 'bchtest', + 'bchreg', +]; + /** * Address Entity. */ export class Address { public readonly addressContents: KnownAddressTypeContents; - constructor(addressContents: KnownAddressTypeContents) { + protected constructor(addressContents: KnownAddressTypeContents) { this.addressContents = addressContents; } @@ -37,13 +44,16 @@ export class Address { * * @returns {Address} Instance of Address. */ - public static from(address: string): Address { + public static from( + address: string, + tryPrefixes = DEFAULT_ADDRESS_PREFIXES, + ): Address { // Attempt to decode as a Cash Address. try { - return Address.fromCashAddr(address); + return this.fromCashAddr(address, tryPrefixes); } catch (_error) { // And if that fails, attempt to decode as a Base58 Address. - return Address.fromLegacy(address); + return this.fromLegacy(address); } } @@ -61,7 +71,7 @@ export class Address { */ public static fromCashAddr( cashAddr: string, - tryPrefixes: Array = ['', 'bitcoincash', 'bchtest', 'bchreg'], + tryPrefixes: Array = DEFAULT_ADDRESS_PREFIXES, ): Address { // Try all prefixes (including empty string) until we find a valid decode const decodeResult = tryPrefixes @@ -158,7 +168,7 @@ export class Address { } // Return a new instance of address. - return new Address({ payload, type }); + return this.fromRaw({ payload, type }); } /** @@ -181,7 +191,7 @@ export class Address { const contractBytecode = hexToBin(hex); // Return a new instance of address. - return Address.fromRedeemScript(contractBytecode, type); + return this.fromRedeemScript(contractBytecode, type); } /** @@ -201,7 +211,7 @@ export class Address { throw new Error(`Invalid type (${addressContents.type})`); } - return new Address(addressContents); + return this.fromRaw(addressContents); } /** @@ -220,7 +230,7 @@ export class Address { const lockScriptBytes = hexToBin(hex); - return Address.fromLockscriptBytes(lockScriptBytes); + return this.fromLockscriptBytes(lockScriptBytes); } /** @@ -244,7 +254,7 @@ export class Address { ); } - return new Address({ payload: hash160, type }); + return this.fromRaw({ payload: hash160, type }); } /** @@ -267,16 +277,34 @@ export class Address { const hash160Bytes = hexToBin(hash160Hex); - return Address.fromHash160(hash160Bytes, type); + return this.fromHash160(hash160Bytes, type); } + /** + * Creates an Address instance from a raw object representation (LibAuth's KnownAddressTypeContents). + * + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. + * + * @param obj - LibAuth Object of type KnownAddressTypeContents + * @returns A new Address instance + */ public static fromRaw(addressContents: KnownAddressTypeContents) { return new this(addressContents); } - public static isValid(address: string): boolean { + /** + * Checks whether the given address is a valid Legacy or CashAddr. + * + * @param address The address. + * + * @returns boolean indicating whether the address is a valid Legacy Address or CashAddr. + */ + public static isValid( + address: string, + tryPrefixes = DEFAULT_ADDRESS_PREFIXES, + ): boolean { try { - Address.from(address); + Address.from(address, tryPrefixes); } catch (_error) { return false; } @@ -284,6 +312,13 @@ export class Address { return true; } + /** + * Checks whether the given address is a valid Legacy Address. + * + * @param address The address. + * + * @returns boolean indicating whether the address is a valid Legacy Address. + */ public static isValidLegacy(address: string): boolean { try { Address.fromLegacy(address); @@ -294,9 +329,19 @@ export class Address { return true; } - public static isValidCashAddr(address: string): boolean { + /** + * Checks whether the given address is a valid CashAddr. + * + * @param address The address. + * + * @returns boolean indicating whether the address is a valid CashAddr. + */ + public static isValidCashAddr( + address: string, + tryPrefixes = DEFAULT_ADDRESS_PREFIXES, + ): boolean { try { - Address.fromCashAddr(address); + Address.fromCashAddr(address, tryPrefixes); } catch (_error) { return false; } @@ -467,6 +512,11 @@ export class Address { return lockscriptAsm; } + /** + * Converts the address to its raw LibAuth representation (KnownAddressTypeContents). + * + * @returns A LibAuth KnownAddressTypeContents object. + */ public toRaw() { return { ...this.addressContents }; } diff --git a/packages/primitives/src/block-header.ts b/packages/primitives/src/block-header.ts index 7558995..582174a 100644 --- a/packages/primitives/src/block-header.ts +++ b/packages/primitives/src/block-header.ts @@ -24,7 +24,7 @@ export class BlockHeader { * @param bits - Compact representation of the target threshold * @param nonce - Value modified during mining to make the block hash valid */ - constructor( + protected constructor( private readonly version: number, private readonly previousBlockHash: Uint8Array, private readonly merkleRoot: Uint8Array, @@ -33,6 +33,24 @@ export class BlockHeader { private readonly nonce: number, ) {} + /** + * Instantiates a blockheader from either: + * + * 1. Transaction bytecode + * 2. Transaction bytecode (in hexadecimal format) + * + * @param blockHeader The block header in one of the above formats. + * + * @throws {Error} If the block header cannot be decoded. + * + * @returns The created BlockHeader Primitive. + */ + public static from(blockHeader: Uint8Array | string) { + return blockHeader instanceof Uint8Array + ? BlockHeader.fromBytes(blockHeader) + : BlockHeader.fromHex(blockHeader); + } + /** * Creates a BlockHeader instance from a byte array. * @param bytes - 80-byte array containing the serialized block header @@ -72,6 +90,9 @@ export class BlockHeader { /** * Creates a BlockHeader instance from a raw object representation. + * + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. + * * @param obj - Object containing the block header fields * @returns A new BlockHeader instance */ @@ -86,6 +107,23 @@ export class BlockHeader { ); } + /** + * Checks whether the given BlochHeader is of a valid structure. + * + * @param blockHeader The blockheader as hex or a Uint8Array. + * + * @returns boolean indicating whether the blockheader is of a valid structure. + */ + public static isValid(blockHeader: Uint8Array | string): boolean { + try { + BlockHeader.from(blockHeader); + } catch (_error) { + return false; + } + + return true; + } + /** * Serializes the block header to a byte array. * @returns 80-byte array containing the serialized block header @@ -106,7 +144,17 @@ export class BlockHeader { } /** - * Converts the block header to a raw object representation. + * Converts the block header to a hex string. + * + * @returns 160-character hex string representing the block header + */ + toHex(): string { + return binToHex(this.toBytes()); + } + + /** + * Converts the block header to a serializable raw object representation. + * * @returns Object containing the block header fields */ toRaw(): BlockHeaderRaw { @@ -120,14 +168,6 @@ export class BlockHeader { }; } - /** - * 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; diff --git a/packages/primitives/src/bytes.ts b/packages/primitives/src/bytes.ts index 3f859ac..2366a65 100644 --- a/packages/primitives/src/bytes.ts +++ b/packages/primitives/src/bytes.ts @@ -23,7 +23,7 @@ export class Bytes { * * @param bytes {Uint8Array} The bytes to use. */ - constructor(bytes: Uint8Array) { + protected constructor(bytes: Uint8Array) { this.bytes = bytes; } diff --git a/packages/primitives/src/ext-json.ts b/packages/primitives/src/ext-json.ts index 4775982..7170460 100644 --- a/packages/primitives/src/ext-json.ts +++ b/packages/primitives/src/ext-json.ts @@ -1,6 +1,8 @@ -// 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 +/** + * 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'; diff --git a/packages/primitives/src/hd-private-node.ts b/packages/primitives/src/hd-private-node.ts index 060eb19..e806ed3 100644 --- a/packages/primitives/src/hd-private-node.ts +++ b/packages/primitives/src/hd-private-node.ts @@ -1,4 +1,6 @@ +import { Bytes } from './bytes.js'; import { HDPublicNode } from './hd-public-node.js'; +import { Mnemonic } from './mnemonic.js'; import { PrivateKey } from './private-key.js'; import { @@ -8,7 +10,6 @@ import { deriveHdPathRelative, deriveHdPublicNode, deriveHdPrivateNodeFromSeed, - deriveSeedFromBip39Mnemonic, generateRandomBytes, } from '@bitauth/libauth'; @@ -30,7 +31,7 @@ export class HDPrivateNode { * * @param node The HD Private Node. */ - constructor(node: HdPrivateNodeValid) { + protected constructor(node: HdPrivateNodeValid) { this.node = node; } @@ -43,27 +44,18 @@ export class HDPrivateNode { * * @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); - } + public static fromSeed(seed: Uint8Array | string): HDPrivateNode { + // Cast from hex if a string was provided. + if (typeof seed === 'string') { + seed = Bytes.fromHex(seed).toBytes(); + } - /** - * 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. - /*if (seed.length !== 32) { + // If the seed is not 64 bytes, throw an error. + if (seed.length !== 64) { throw new Error( - `Failed to derive Private Node from seed: Seed must be 32 bytes (${seed.length} given)` + `Failed to derive Private Node from seed: Seed must be 64 bytes (${seed.length} given)`, ); - }*/ + } // Attempt to derive HD Private Node from the given seed. const deriveResult = deriveHdPrivateNodeFromSeed(seed); @@ -80,15 +72,15 @@ export class HDPrivateNode { /** * Creates a HD Private Node from the given mnemonic. * - * @param mnemonic The mnemonic. + * @param phrase The mnemonic phrase as a string or array of words. * * @throws {Error} If HD Private Node cannot be created. * * @returns The created HD Private Node. */ - public static fromMnemonic(mnemonic: string): HDPrivateNode { + public static fromMnemonic(phrase: string | Array): HDPrivateNode { // Derive the seed from the mnemonic. - const seed = deriveSeedFromBip39Mnemonic(mnemonic); + const seed = Mnemonic.from(phrase).toSeed(); // Return a new HDPrivateNode from the given seed. return this.fromSeed(seed); @@ -163,14 +155,74 @@ export class HDPrivateNode { } } + /** + * 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; + } + /** * Gets the Private Key entity for this node. * * @returns The Private Key Entity for this node. */ - public privateKey(): PrivateKey { + public toPrivateKey(): PrivateKey { // Return a Private Key Entity from the private key of our node. - return new PrivateKey(this.node.privateKey); + return PrivateKey.fromRaw(this.node.privateKey); + } + + /** + * Converts this node to an XPriv Key for export. + * + * @param network {HdKeyNetwork} The network to encode for. + * + * @returns The XPriv Key. + */ + public toXPriv(network: HdKeyNetwork = 'mainnet'): string { + // Create our node info structure. + const nodeInfo: DecodedHdKey = { + network: network, + node: this.node, + }; + + // 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; + } + + /** + * Derives a HD Public Node from this Private Node. + * + * @returns The derived HD Public Node + */ + public deriveHDPublicNode(): HDPublicNode { + // Derive a HD Public Node from this HD Private Node. + const publicNode = deriveHdPublicNode(this.node); + + // Return a new HD Public Node entity from our derived Public Node. + return HDPublicNode.fromRaw(publicNode); } /** @@ -216,64 +268,4 @@ export class HDPrivateNode { // 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 The derived HD Public Node - */ - public deriveHDPublicNode(): HDPublicNode { - // Derive a HD Public Node from this HD Private Node. - const publicNode = deriveHdPublicNode(this.node); - - // Return a new HD Public Node entity from our derived Public Node. - 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 The XPriv Key. - */ - public toXPriv(network: HdKeyNetwork = 'mainnet'): string { - // Create our node info structure. - const nodeInfo: DecodedHdKey = { - network: network, - node: this.node, - }; - - // 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; - } } diff --git a/packages/primitives/src/hd-public-node.ts b/packages/primitives/src/hd-public-node.ts index bebeeb2..baac5c6 100644 --- a/packages/primitives/src/hd-public-node.ts +++ b/packages/primitives/src/hd-public-node.ts @@ -24,7 +24,7 @@ export class HDPublicNode { * * @param node {HdPublicNode} The HD Public Node. */ - constructor(node: HdPublicNodeValid) { + protected constructor(node: HdPublicNodeValid) { this.node = node; } @@ -50,6 +50,14 @@ export class HDPublicNode { return new HDPublicNode(decodeResult.node); } + /** + * Creates a HDPublicNode instance from a raw object representation (LibAuth's HdPublicNodeValid). + * + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. + * + * @param obj - LibAuth Object of type HdPublicNodeValid + * @returns A new HDPublicNode instance + */ public static fromRaw(node: HdPublicNodeValid) { return new this(node); } @@ -68,9 +76,46 @@ export class HDPublicNode { * * @returns The Public Key Entity for this node. */ - public publicKey(): PublicKey { + public toPublicKey(): PublicKey { // Return a Public Key Entity from the public key of our node. - return new PublicKey(this.node.publicKey); + return PublicKey.fromRaw(this.node.publicKey); + } + + /** + * Converts this node to an XPub Key for export. + * + * @param network The network to encode for. + * + * @returns The XPub Key. + */ + public toXPub(network: HdKeyNetwork = 'mainnet'): string { + // Create our node info structure. + const nodeInfo = { + network: network, + node: this.node, + } as DecodedHdKey; + + // Encode the XPub from our node info. + return encodeHdPublicKey(nodeInfo).hdPublicKey; + } + + /** + * 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(); + } + + /** + * Converts the HDPublicKey to its raw Libauth representation (HDPublicNodeValid). + * + * @returns A LibAuth HDPublicNodeValid object + */ + public toRaw(): HdPublicNodeValid { + return { ...this.node }; } /** @@ -94,36 +139,4 @@ export class HDPublicNode { // Return a new HD Public Node Entity derived from the given path. return new HDPublicNode(derivePathResult); } - - /** - * Converts this node to an XPub Key for export. - * - * @param network The network to encode for. - * - * @returns The XPub Key. - */ - public toXPub(network: HdKeyNetwork = 'mainnet'): string { - // Create our node info structure. - const nodeInfo = { - network: network, - node: this.node, - } as DecodedHdKey; - - // 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(); - } } diff --git a/packages/primitives/src/index.ts b/packages/primitives/src/index.ts index a8b858b..7b5a2c2 100644 --- a/packages/primitives/src/index.ts +++ b/packages/primitives/src/index.ts @@ -2,7 +2,6 @@ export * from './address.js'; export * from './block-header.js'; export * from './bytes.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'; diff --git a/packages/primitives/src/mnemonic.ts b/packages/primitives/src/mnemonic.ts index f7cfbb1..a356bca 100644 --- a/packages/primitives/src/mnemonic.ts +++ b/packages/primitives/src/mnemonic.ts @@ -1,6 +1,7 @@ import { - generateBip39MnemonicNonStandard, bip39WordListEnglish, + deriveSeedFromBip39Mnemonic, + generateBip39MnemonicNonStandard, } from '@bitauth/libauth'; export type MnemonicRaw = { @@ -20,16 +21,19 @@ export class Mnemonic { * * @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) {} + protected 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). + * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). + * * @throws {Error} If mnemonic could not be generated. + * * @returns Instance of Mnemonic. */ public static from( @@ -41,18 +45,20 @@ export class Mnemonic { return this.fromWords(phraseOrWords, wordList); } - return this.fromString(phraseOrWords, wordList); + return this.fromPhrase(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). + * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). + * * @throws {Error} If mnemonic could not be generated. + * * @returns Instance of Mnemonic. */ - public static fromString( + public static fromPhrase( phrase: string, wordList: Array = bip39WordListEnglish, ) { @@ -69,8 +75,10 @@ export class Mnemonic { * 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). + * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). + * * @throws {Error} If mnemonic could not be generated. + * * @returns Instance of Mnemonic. */ public static fromWords( @@ -133,6 +141,7 @@ export class Mnemonic { * * @param mnemonicRaw The raw menmonic data. * @throws {Error} If mnemonic could not be generated. + * * @returns Instance of Mnemonic. */ public static fromRaw(mnemonicRaw: MnemonicRaw) { @@ -144,6 +153,7 @@ export class 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 generateRandom( @@ -173,6 +183,7 @@ export class Mnemonic { * * @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( @@ -192,6 +203,7 @@ export class Mnemonic { * * @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( @@ -202,21 +214,22 @@ export class Mnemonic { } /** - * Gets the passphrase if one exists. + * Returns the resulting seed for this mnemonic. * - * @returns The passphrase or undefined if none exists. + * @returns The seed (for use with HDPrivateNodes). */ - public getPassphrase(): string | undefined { - return this.mnemonic.passphrase; + public toSeed(): Uint8Array { + return deriveSeedFromBip39Mnemonic(this.toPhrase()); } /** * 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 { + public toPhrase(includePassphrase: boolean = true): string { // Join the menmonic words, separating them using a space character. const mnemonicWords = this.mnemonic.words.join(' '); @@ -252,4 +265,13 @@ export class Mnemonic { public toRaw(): MnemonicRaw { return { ...this.mnemonic }; } + + /** + * Gets the passphrase if one exists. + * + * @returns The passphrase or undefined if none exists. + */ + public getPassphrase(): string | undefined { + return this.mnemonic.passphrase; + } } diff --git a/packages/primitives/src/private-key.ts b/packages/primitives/src/private-key.ts index 5ca508c..d48fbd1 100644 --- a/packages/primitives/src/private-key.ts +++ b/packages/primitives/src/private-key.ts @@ -32,7 +32,7 @@ export class PrivateKey { * * @param bytes {Uint8Array} The raw bytes (32) for the Private Key. */ - constructor(bytes: Uint8Array) { + protected constructor(bytes: Uint8Array) { // Ensure that the Private Key is valid. if (!secp256k1.validatePrivateKey(bytes)) { throw new Error( @@ -43,6 +43,27 @@ export class PrivateKey { this.bytes = bytes; } + /** + * Instantiates a Private Key from either Bytes, Hex or WIF. + * + * @param privateKey The private key as either Bytes, Hex or WIF. + * + * @throws {Error} If the private key cannot be decoded. + * + * @returns The created Private Key Primitive. + */ + public static from(privateKey: Uint8Array | string) { + if (privateKey instanceof Uint8Array) { + return PrivateKey.fromBytes(privateKey); + } + + if (PrivateKey.isWIF(privateKey)) { + return PrivateKey.fromWIF(privateKey); + } + + return PrivateKey.fromHex(privateKey); + } + /** * Create a Private Key Entity from the given bytes. * @@ -124,6 +145,19 @@ export class PrivateKey { return new PrivateKey(decodeResult.privateKey); } + /** + * Creates an instance from serialized data. + * + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. + * + * @param bytes - Serialized Primitive Data + * + * @returns A new instance of this primitive. + */ + public static fromRaw(bytes: Uint8Array) { + return new this(bytes); + } + /** * Generate a random Private Key. * @@ -144,6 +178,57 @@ export class PrivateKey { } } + /** + * Convert the raw bytes of this Private Key to hexadecimal encoding. + * + * @returns {string} The raw bytes of the Private Key as a hexadecimal string. + */ + public toHex(): string { + // Return the hexadecimal encoded bytes of this Private Key. + return binToHex(this.bytes); + } + + /** + * Convert this Private Key to a raw 32 byte representation. + * + * @returns {Uint8Array} The raw Private Key as a Uint8Array. + */ + public toBytes(): Uint8Array { + // Return the raw bytes for this Private Key. + return this.bytes; + } + + /** + * Convert this Private Key to a WIF format. + * + * @param type {WalletImportFormatType} The type of WIF to create. + * + * @returns {string} The Private Key in WIF format. + */ + public toWif(type: WalletImportFormatType = 'mainnet'): string { + // Return the encoded Private Key WIF. + return encodePrivateKeyWif(this.bytes, type); + } + + /** + * Returns the Private Key as a mainnet WIF. + * + * @returns {string} The Private Key as a mainnet WIF. + */ + public toString(): string { + // Return the encoded Mainnet Private Key WIF. + return this.toWif(); + } + + /** + * Converts this primitive to a serializable raw object representation. + * + * @returns Serialzied primitive data. + */ + toRaw() { + return this.bytes; + } + /** * Derive the public key from this private key. * @@ -161,7 +246,7 @@ export class PrivateKey { } // Return the derived Public Key. - return new PublicKey(deriveResult); + return PublicKey.fromRaw(deriveResult); } /** @@ -406,46 +491,4 @@ export class PrivateKey { // Return the signature. return signResult; } - - /** - * Convert the raw bytes of this Private Key to hexadecimal encoding. - * - * @returns {string} The raw bytes of the Private Key as a hexadecimal string. - */ - public toHex(): string { - // Return the hexadecimal encoded bytes of this Private Key. - return binToHex(this.bytes); - } - - /** - * Convert this Private Key to a raw 32 byte representation. - * - * @returns {Uint8Array} The raw Private Key as a Uint8Array. - */ - public toBytes(): Uint8Array { - // Return the raw bytes for this Private Key. - return this.bytes; - } - - /** - * Convert this Private Key to a WIF format. - * - * @param type {WalletImportFormatType} The type of WIF to create. - * - * @returns {string} The Private Key in WIF format. - */ - public toWif(type: WalletImportFormatType = 'mainnet'): string { - // Return the encoded Private Key WIF. - return encodePrivateKeyWif(this.bytes, type); - } - - /** - * Returns the Private Key as a mainnet WIF. - * - * @returns {string} The Private Key as a mainnet WIF. - */ - public toString(): string { - // Return the encoded Mainnet Private Key WIF. - return this.toWif(); - } } diff --git a/packages/primitives/src/public-key.ts b/packages/primitives/src/public-key.ts index 899de7b..706683f 100644 --- a/packages/primitives/src/public-key.ts +++ b/packages/primitives/src/public-key.ts @@ -22,15 +22,25 @@ export class PublicKey { * * @param bytes The raw bytes (33) for the Public Key. */ - constructor(bytes: Uint8Array) { - // Ensure that exactly 33 Bytes are given. - if (!secp256k1.validatePublicKey(bytes)) { - throw new Error(`Invalid Public Key.`); - } - + protected constructor(bytes: Uint8Array) { this.bytes = bytes; } + /** + * Instantiates a Public Key from either Public Key Hex or Public Key Bytes. + * + * @param publicKey The public key in either hex or bytes. + * + * @throws {Error} If the public key cannot be decoded. + * + * @returns The created Public Key Primitive. + */ + public static from(publicKey: Uint8Array | string) { + return publicKey instanceof Uint8Array + ? PublicKey.fromBytes(publicKey) + : PublicKey.fromHex(publicKey); + } + /** * Create a Public Key entity from the given hexadecimal string. * @@ -41,6 +51,11 @@ export class PublicKey { * @returns The created Public Key entity. */ public static fromBytes(publicKeyBytes: Uint8Array): PublicKey { + // Ensure that exactly 33 Bytes are given. + if (!secp256k1.validatePublicKey(publicKeyBytes)) { + throw new Error(`Invalid Public Key.`); + } + return new PublicKey(publicKeyBytes); } @@ -68,6 +83,57 @@ export class PublicKey { return new PublicKey(publicKey); } + /** + * Creates an instance from serialized data. + * + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. + * + * @param bytes - Serialized Primitive Data + * + * @returns A new instance of this primitive. + */ + public static fromRaw(bytes: Uint8Array) { + return new this(bytes); + } + + /** + * Encode the raw bytes of this Public Key to a hexadecimal representation. + * + * @returns The raw bytes Public Key encoded as Hexadecimal. + */ + public toHex(): string { + return binToHex(this.bytes); + } + + /** + * Get the raw bytes of this Public Key. + * + * @returns The raw bytes of the Public Key. + */ + public toBytes(): Uint8Array { + // Return the raw bytes for this Public Key. + return this.bytes; + } + + /** + * Return the hexadecimal representation of the raw bytes of the Public Key. + * + * @returns The raw bytes of the Public Key as hexadecimal. + */ + public toString(): string { + // Return the raw Public Key byes encoded as a hexadecimal string. + return this.toHex(); + } + + /** + * Converts this primitive to a serializable representation. + * + * @returns Serialzied primitive data. + */ + toRaw() { + return this.bytes; + } + /** * Derive the Hash160 for this Public Key. * @@ -212,33 +278,4 @@ export class PublicKey { return validity; } - - /** - * Encode the raw bytes of this Public Key to a hexadecimal representation. - * - * @returns The raw bytes Public Key encoded as Hexadecimal. - */ - public toHex(): string { - return binToHex(this.bytes); - } - - /** - * Get the raw bytes of this Public Key. - * - * @returns The raw bytes of the Public Key. - */ - public toBytes(): Uint8Array { - // Return the raw bytes for this Public Key. - return this.bytes; - } - - /** - * Return the hexadecimal representation of the raw bytes of the Public Key. - * - * @returns The raw bytes of the Public Key as hexadecimal. - */ - public toString(): string { - // Return the raw Public Key byes encoded as a hexadecimal string. - return this.toHex(); - } } diff --git a/packages/primitives/src/satoshis.ts b/packages/primitives/src/satoshis.ts index 28f14ef..6f626d6 100644 --- a/packages/primitives/src/satoshis.ts +++ b/packages/primitives/src/satoshis.ts @@ -1,3 +1,5 @@ +export type BCHUnits = ['sats', 'bits', 'mBCH', 'BCH']; + /** * Satoshis Primitive */ @@ -11,7 +13,7 @@ export class Satoshis { * * @param satoshis {number} The satoshi amount. */ - constructor(satoshis: bigint | number) { + protected constructor(satoshis: bigint | number) { // Set the satoshi amount. this.satoshis = BigInt(satoshis); } diff --git a/packages/primitives/src/script.ts b/packages/primitives/src/script.ts index 0d6e725..996bcfb 100644 --- a/packages/primitives/src/script.ts +++ b/packages/primitives/src/script.ts @@ -1,12 +1,9 @@ import { assembleBytecodeBCH, disassembleBytecodeBCH, - base58AddressToLockingBytecode, - cashAddressToLockingBytecode, binToHex, hexToBin, isHex, - parseScript, } from '@bitauth/libauth'; /** @@ -20,14 +17,29 @@ export class Script { * * @param bytes {Uint8Array} The raw bytes of the Script. */ - constructor(bytes: Uint8Array) { + protected constructor(bytes: Uint8Array) { this.bytes = bytes; } /** - * Create a Script Entity from the given Assembly String. + * Instantiates a Script from either Bytes or Hex. * - * @param asm {string} String of assembly code. + * @param script The script as either Bytes or Hex. + * + * @throws {Error} If the script cannot be decoded. + * + * @returns The created Script Primitive. + */ + public static from(script: Uint8Array | string) { + return script instanceof Uint8Array + ? Script.fromBytes(script) + : Script.fromHex(script); + } + + /** + * Create a Script Entity from bytes. + * + * @param bytes {string} Script Bytecode * * @throws {Error} If script cannot be decoded. * @@ -86,44 +98,6 @@ export class Script { return new Script(scriptBin); } - /** - * Create a Script Entity from the given CashAddr. - * - * @param cashAddr {string} The Address in CashAddr format. - * - * @throws {Error} If address cannot be decoded. - * - * @returns {Script} The created Script Entity. - */ - public static fromCashAddr(cashAddr: string): Script { - const decodeResult = cashAddressToLockingBytecode(cashAddr); - - if (typeof decodeResult === 'string') { - throw new Error(decodeResult); - } - - return new Script(decodeResult.bytecode); - } - - /** - * Create a Script Entity from the given Legacy Address. - * - * @param legacyAddress {string} The Address in Legacy format. - * - * @throws {Error} If address cannot be decoded. - * - * @returns {Script} The created Script Entity. - */ - public static fromLegacyAddress(legacyAddress: string): Script { - const decodeResult = base58AddressToLockingBytecode(legacyAddress); - - if (typeof decodeResult === 'string') { - throw new Error(decodeResult); - } - - return new Script(decodeResult.bytecode); - } - /** * Convert this script to a human-readable string of assembly. * diff --git a/packages/primitives/src/transaction-hash.ts b/packages/primitives/src/transaction-hash.ts index 483fc45..3a715af 100644 --- a/packages/primitives/src/transaction-hash.ts +++ b/packages/primitives/src/transaction-hash.ts @@ -11,7 +11,35 @@ export class TransactionHash { * * @param bytes {Uint8Array} The Transaction Hash's bytes. */ - constructor(bytes: Uint8Array) { + protected constructor(bytes: Uint8Array) { + this.bytes = bytes; + } + + /** + * Instantiates a TransactionHash from either the Hash Hex or Hash Bytes. + * + * @param hash The Transaction Hash as either hex or bytes. + * + * @throws {Error} If the hash is invalid. + * + * @returns The created Public Key Primitive. + */ + public static from(hash: Uint8Array | string) { + return hash instanceof Uint8Array + ? TransactionHash.fromBytes(hash) + : TransactionHash.fromHex(hash); + } + + /** + * Instantiates TransactionHash from Hash Bytes. + * + * @param bytes The hash as bytes. + * + * @throws {Error} If hash is invalid. + * + * @returns {TransactionHash} Instance of TransactionHash. + */ + public static fromBytes(bytes: Uint8Array): TransactionHash { // If bytes is not exactly 32, then this is an invalid transaction hash... if (bytes.length !== 32) { throw new Error( @@ -19,7 +47,7 @@ export class TransactionHash { ); } - this.bytes = bytes; + return new TransactionHash(bytes); } /** @@ -38,7 +66,22 @@ export class TransactionHash { const bytes = hexToBin(hex); - return new TransactionHash(bytes); + return this.fromBytes(bytes); + } + + /** + * Instantiates a TransactionHash from either the Transaction Hex or Transaction Hash. + * + * @param hash The Transaction as either hex or bytes. + * + * @throws {Error} If the public key cannot be decoded. + * + * @returns The created Public Key Primitive. + */ + public static fromTransaction(transaction: Uint8Array | string) { + return transaction instanceof Uint8Array + ? TransactionHash.fromTransactionBytes(transaction) + : TransactionHash.fromTransactionHex(transaction); } /** @@ -75,6 +118,19 @@ export class TransactionHash { return TransactionHash.fromTransactionBytes(bytes); } + /** + * Creates an instance from serialized data. + * + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. + * + * @param bytes - Serialized Primitive Data + * + * @returns A new instance of this primitive. + */ + public static fromRaw(bytes: Uint8Array) { + return new this(bytes); + } + /** * Returns transaction hash as a hex string. * @@ -92,4 +148,13 @@ export class TransactionHash { public toBytes(): Uint8Array { return this.bytes; } + + /** + * Converts this primitive to a serializable representation. + * + * @returns Serialzied primitive data. + */ + toRaw() { + return this.bytes; + } } diff --git a/packages/primitives/src/transaction.ts b/packages/primitives/src/transaction.ts index 5aeed95..3c0f410 100644 --- a/packages/primitives/src/transaction.ts +++ b/packages/primitives/src/transaction.ts @@ -13,7 +13,7 @@ import type { TransactionBCH } from '@bitauth/libauth'; * Transaction Primitive. */ export class Transaction { - constructor(public transaction: TransactionBCH) {} + protected constructor(public transaction: TransactionBCH) {} /** * Instantiates a transaction from either: @@ -28,19 +28,9 @@ export class Transaction { * @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', - ); + return transaction instanceof Uint8Array + ? Transaction.fromBytes(transaction) + : Transaction.fromHex(transaction); } /** @@ -90,7 +80,7 @@ export class Transaction { /** * Create a Transaction Primitive from LibAuth's TransactionBCH type. * - * @remarks This method is UNSAFE and should only be used where input is trusted. + * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. * * @param transaction The transaction object (as per LibAuth). * @@ -104,6 +94,7 @@ export class Transaction { * Validates the structure of a transaction. * * @param transaction The transaction (as hex or bytes). + * * @returns boolean indicating whether the transaction structure is valid. */ public static isValid(transaction: Uint8Array | string): boolean { @@ -116,34 +107,67 @@ export class Transaction { return true; } + /** + * Serializes the transaction to a byte array. + * @returns The transaction serialized as a Uint8Array + */ public toBytes(): Uint8Array { const bytes = encodeTransaction(this.transaction); return bytes; } + /** + * Serializes the transaction to a byte array (represented as a hex string). + * @returns The transaction serialized in hex + */ public toHex(): string { const bytes = this.toBytes(); return binToHex(bytes); } + /** + * Hashes the transaction and returns a TransactionHash primitive. + * @returns The transaction hash primitive. + */ public toHash(): TransactionHash { return TransactionHash.fromTransactionBytes(this.toBytes()); } + /** + * Hashes the transaction and returns the hash as hexadecimal. + * @returns The transaction hash hex. + */ public toHashHex(): string { return this.toHash().toHex(); } + /** + * Hashes the transaction and returns the hash as a Uint8Array. + * @returns The transaction hash bytes. + */ public toHashBytes(): Uint8Array { return this.toHash().toBytes(); } + /** + * Converts transaction to a raw object representation. + * @returns A TransactionBCH LibAuth object. + */ public toRaw(): TransactionBCH { - return this.transaction; + return { ...this.transaction }; } + /** + * Checks whether this transaction is a Coinbase Transaction. + * + * @remarks UNTESTED + * + * @experimental + * + * @returns True if this is a Coinbase Transaction + */ public isCoinbase(): boolean { // A coinbase transaction MUST only have one input if (this.getInputs().length !== 1) { @@ -164,18 +188,46 @@ export class Transaction { return isOutpointZero && isOutpointIndexMax; } + /** + * Gets the version for this transaction. + * + * @experimental + * + * @returns The version of this transaction + */ public getVersion() { return this.transaction.version; } + /** + * Gets the inputs in this transaction. + * + * @experimental + * + * @returns The inputs in this transaction + */ public getInputs() { return this.transaction.inputs; } + /** + * Gets the outputs in this transaction. + * + * @experimental + * + * @returns The outputs in this transaction + */ public getOutputs() { return this.transaction.outputs; } + /** + * Gets the locktime for this transaction. + * + * @experimental + * + * @returns The locktime of this transaction + */ public getLocktime() { return this.transaction.locktime; } diff --git a/packages/primitives/tests/mnemonic.spec.ts b/packages/primitives/tests/mnemonic.spec.ts index 5156a95..51daee7 100644 --- a/packages/primitives/tests/mnemonic.spec.ts +++ b/packages/primitives/tests/mnemonic.spec.ts @@ -1,21 +1,23 @@ import { describe, it, expect } from 'vitest'; +import { Bytes } from '~/src/bytes'; import { Mnemonic, MnemonicRaw } from '~/src/mnemonic'; describe('Success cases', () => { // Simple array of cases const testCases: Array<{ - fromString: string; + fromPhrase: string; fromWords: Array; passphrase?: string; raw: MnemonicRaw; - toString: string; - toStringWithoutPassphrase: string; + toSeed: Uint8Array; + toPhrase: string; + toPhraseWithoutPassphrase: string; toWords: Array; toWordsWithoutPassphrase: Array; }> = [ // No pass-phrase { - fromString: + fromPhrase: 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust', fromWords: [ 'goose', @@ -47,9 +49,12 @@ describe('Success cases', () => { 'exhaust', ], }, - toString: + toSeed: Bytes.fromHex( + '7066f43a742829f0edce7d8e3c31651df320f13913995a6500f4e515db372b35202227f923ff181defc35cea02f8468df39f8fa9d97a202d252e61562e4bbfb5', + ).toBytes(), + toPhrase: 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust', - toStringWithoutPassphrase: + toPhraseWithoutPassphrase: 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust', toWords: [ 'goose', @@ -82,7 +87,7 @@ describe('Success cases', () => { }, // Passphrase { - fromString: + fromPhrase: 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust GeneralProtocols!', fromWords: [ 'goose', @@ -117,9 +122,12 @@ describe('Success cases', () => { ], passphrase: 'GeneralProtocols!', }, - toString: + toSeed: Bytes.fromHex( + '2f706756ab2083f7ce07c0c0fcde8f14f062da5de839f42763262b25e0d871f03dd2b561d55eaacc471c94da656ee7b0007fb48a64e31ec91a1a2ba706b46b89', + ).toBytes(), + toPhrase: 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust GeneralProtocols!', - toStringWithoutPassphrase: + toPhraseWithoutPassphrase: 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust', toWords: [ 'goose', @@ -153,44 +161,46 @@ describe('Success cases', () => { }, ]; - it.each(testCases)( - 'from(string)', - ({ fromString: phrase, raw: expected }) => { - const mnemonic = Mnemonic.from(phrase); - expect(mnemonic.toRaw()).toStrictEqual(expected); - }, - ); + it.each(testCases)('from(string)', ({ fromPhrase: phrase, raw }) => { + const mnemonic = Mnemonic.from(phrase); + expect(mnemonic.toRaw()).toStrictEqual(raw); + }); - it.each(testCases)('from(words)', ({ fromWords, raw: expected }) => { + it.each(testCases)('from(words)', ({ fromWords, raw }) => { const mnemonic = Mnemonic.from(fromWords); - expect(mnemonic.toRaw()).toStrictEqual(expected); + expect(mnemonic.toRaw()).toStrictEqual(raw); }); - it.each(testCases)('fromString', ({ fromString, raw: expected }) => { - const mnemonic = Mnemonic.fromString(fromString); - expect(mnemonic.toRaw()).toStrictEqual(expected); + it.each(testCases)('fromPhrase', ({ fromPhrase, raw }) => { + const mnemonic = Mnemonic.fromPhrase(fromPhrase); + expect(mnemonic.toRaw()).toStrictEqual(raw); }); - it.each(testCases)('fromWords', ({ fromWords, raw: expected }) => { + it.each(testCases)('fromWords', ({ fromWords, raw }) => { const mnemonic = Mnemonic.fromWords(fromWords); - expect(mnemonic.toRaw()).toStrictEqual(expected); + expect(mnemonic.toRaw()).toStrictEqual(raw); }); - it.each(testCases)('fromWords', ({ fromWords, raw: expected }) => { + it.each(testCases)('fromWords', ({ fromWords, raw }) => { const mnemonic = Mnemonic.fromWords(fromWords); - expect(mnemonic.toRaw()).toStrictEqual(expected); + expect(mnemonic.toRaw()).toStrictEqual(raw); }); - it.each(testCases)('toString', ({ toString, raw }) => { + it.each(testCases)('toSeed', ({ toSeed, raw }) => { const mnemonic = Mnemonic.fromRaw(raw); - expect(mnemonic.toString()).toStrictEqual(toString); + expect(mnemonic.toSeed()).toStrictEqual(toSeed); + }); + + it.each(testCases)('toPhrase', ({ toPhrase, raw }) => { + const mnemonic = Mnemonic.fromRaw(raw); + expect(mnemonic.toPhrase()).toStrictEqual(toPhrase); }); it.each(testCases)( - 'toString(withoutPassphrase)', - ({ toStringWithoutPassphrase, raw }) => { + 'toPhrase(withoutPassphrase)', + ({ toPhraseWithoutPassphrase, raw }) => { const mnemonic = Mnemonic.fromRaw(raw); - expect(mnemonic.toString(false)).toStrictEqual(toStringWithoutPassphrase); + expect(mnemonic.toPhrase(false)).toStrictEqual(toPhraseWithoutPassphrase); }, ); @@ -254,10 +264,10 @@ describe('Validations', () => { }); describe('Fail cases', () => { - it('fromString should fail if invalid number of words', () => { + it('fromPhrase should fail if invalid number of words', () => { expect(() => { // 11 words. - Mnemonic.fromString( + Mnemonic.fromPhrase( 'goose just fan kidney barrel album wave crowd movie flame cousin', ); }).toThrow(); @@ -289,10 +299,10 @@ describe('Fail cases', () => { }).toThrow(); }); - it('fromString should fail if word not in dictionary', () => { + it('fromPhrase should fail if word not in dictionary', () => { expect(() => { // Invalid word "fnord". - Mnemonic.fromString( + Mnemonic.fromPhrase( 'fnord just fan kidney barrel album wave crowd movie flame cousin exhaust', ); }).toThrow(); diff --git a/packages/stack/src/storage/base-store.ts b/packages/stack/src/storage/base-store.ts index c6419ca..5eef6fe 100644 --- a/packages/stack/src/storage/base-store.ts +++ b/packages/stack/src/storage/base-store.ts @@ -1,8 +1,8 @@ -import { Encryption, EventEmitter } from '../utils'; +import { Encrypter, EventEmitter } from '../utils'; export type StorageOpts = { syncInMemory?: boolean; - encryption?: Encryption; + encryption?: Encrypter; }; export type StoreEvents = { diff --git a/packages/stack/src/storage/store-indexeddb.ts b/packages/stack/src/storage/store-indexeddb.ts index f323998..e6267bd 100644 --- a/packages/stack/src/storage/store-indexeddb.ts +++ b/packages/stack/src/storage/store-indexeddb.ts @@ -1,7 +1,7 @@ import { BaseStorage, BaseStore, StorageOpts } from './base-store.js'; import { StoreInMemorySynced } from './store-inmemory-synced.js'; -import { Encryption } from '../utils'; +import { Encrypter } from '../utils'; import { encodeExtendedJsonObject, @@ -113,7 +113,7 @@ export class StoreIndexedDB extends BaseStore { constructor( public db: IDBPDatabase, public storeName: string, - public encryption?: Encryption, + public encryption?: Encrypter, ) { super(); @@ -143,7 +143,7 @@ export class StoreIndexedDB extends BaseStore { static async create( db: IDBPDatabase, storeName: string, - encryption?: Encryption, + encryption?: Encrypter, ) { return new this(db, storeName, encryption); } diff --git a/packages/stack/src/utils/encryption.ts b/packages/stack/src/utils/encrypter.ts similarity index 91% rename from packages/stack/src/utils/encryption.ts rename to packages/stack/src/utils/encrypter.ts index 1fb0480..fb82ac8 100644 --- a/packages/stack/src/utils/encryption.ts +++ b/packages/stack/src/utils/encrypter.ts @@ -9,7 +9,7 @@ import { utf8ToBin } from '@bitauth/libauth'; export const DEFAULT_XO_SALT = 'cash.xo.default_salt_for_encryption_aabbccddeeff00112233445566778899'; -export type EncryptionOpts = { +export type EncrypterOpts = { algorithm: string; keyLength: number; salt: Uint8Array; @@ -17,13 +17,13 @@ export type EncryptionOpts = { iterations: number; }; -export class Encryption { +export class Encrypter { constructor( private key: CryptoKey, - public opts: EncryptionOpts, + public opts: EncrypterOpts, ) {} - static async fromBytes(key: Uint8Array, opts: Partial = {}) { + static async fromBytes(key: Uint8Array, opts: Partial = {}) { // Merge the provided options into our default options. const encryptionOpts = { algorithm: 'AES-GCM', @@ -60,12 +60,12 @@ export class Encryption { ['encrypt', 'decrypt'], ); - return new Encryption(derivedKey, encryptionOpts); + return new Encrypter(derivedKey, encryptionOpts); } static async fromPassphrase( passphrase: string, - opts: Partial = {}, + opts: Partial = {}, ) { const key = utf8ToBin(passphrase); diff --git a/packages/primitives/src/hasher.ts b/packages/stack/src/utils/hasher.ts similarity index 100% rename from packages/primitives/src/hasher.ts rename to packages/stack/src/utils/hasher.ts diff --git a/packages/stack/src/utils/index.ts b/packages/stack/src/utils/index.ts index b42cf82..b85daf1 100644 --- a/packages/stack/src/utils/index.ts +++ b/packages/stack/src/utils/index.ts @@ -1,6 +1,7 @@ -export * from './encryption.js'; +export * from './encrypter.js'; export * from './event-emitter.js'; export * from './ext-error.js'; +export * from './hasher.js'; export * from './memoizer.js'; export * from './misc.js'; export * from './uuid.js'; diff --git a/packages/stack/src/wallet/wallet-hd.ts b/packages/stack/src/wallet/wallet-hd.ts index efddd9e..8f75163 100644 --- a/packages/stack/src/wallet/wallet-hd.ts +++ b/packages/stack/src/wallet/wallet-hd.ts @@ -418,7 +418,7 @@ export class WalletHD< // Derive the private key at the given path. const privateKeyHex = await this.cache.getOrSet(derivationPath, () => { const childNode = this.hdPrivateNode.derivePath(derivationPath); - return childNode.privateKey().toHex(); + return childNode.toPrivateKey().toHex(); }); // Create a child P2PKH wallet for this Private Key. diff --git a/packages/stack/tests/utils/encryption.spec.ts b/packages/stack/tests/utils/encryption.spec.ts index 22e5961..71f79e8 100644 --- a/packages/stack/tests/utils/encryption.spec.ts +++ b/packages/stack/tests/utils/encryption.spec.ts @@ -1,25 +1,25 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { Encryption } from '~/src/utils/encryption'; +import { Encrypter } from '~/src/utils/encrypter'; import { binToUtf8, utf8ToBin } from '@bitauth/libauth'; -describe('Encryption', () => { - let encryption: Encryption; +describe('Encrypter', () => { + let encryption: Encrypter; const testKey = 'test-secret-key-123'; beforeEach(async () => { - encryption = await Encryption.fromPassphrase(testKey); + encryption = await Encrypter.fromPassphrase(testKey); }); describe('constructor', () => { it('should create instance with string key', async () => { - const enc = await Encryption.fromPassphrase('test-key'); - expect(enc).toBeInstanceOf(Encryption); + const enc = await Encrypter.fromPassphrase('test-key'); + expect(enc).toBeInstanceOf(Encrypter); }); it('should create instance with Uint8Array key', async () => { - const enc = await Encryption.fromBytes(utf8ToBin('test-key')); - expect(enc).toBeInstanceOf(Encryption); + const enc = await Encrypter.fromBytes(utf8ToBin('test-key')); + expect(enc).toBeInstanceOf(Encrypter); }); }); @@ -77,8 +77,8 @@ describe('Encryption', () => { describe('cross-instance encryption', () => { it('should decrypt data encrypted with different instance using same key', async () => { - const enc1 = await Encryption.fromPassphrase(testKey); - const enc2 = await Encryption.fromPassphrase(testKey); + const enc1 = await Encrypter.fromPassphrase(testKey); + const enc2 = await Encrypter.fromPassphrase(testKey); const originalData = 'Hello, World!'; const encrypted = await enc1.encrypt(originalData);