Merge branch 'inverse_bloom_filter' into 'development'

Draft Inverse Bloom Filter

See merge request GeneralProtocols/xo/stack!19
This commit is contained in:
Jim Hamill
2025-01-20 07:47:08 +00:00
21 changed files with 1285 additions and 381 deletions
@@ -47,11 +47,9 @@
</template>
<template v-if="state.methods.length && state.primitive">
<h2>Methods</h2>
<div class="form-group-row" :class="{ 'extended': state.selectedMethod?.extendedInput }">
<div class="form-group select-group">
<label for="functionSelect">Select Function</label>
<label for="functionSelect">Method</label>
<select v-model="state.selectedMethod">
<option v-for="method of props.methods" :key="method.label" :value="method">{{ method.label }}</option>
</select>
@@ -192,6 +190,14 @@ async function setPrimitive(functionToExecute: () => Primitive | Promise<Primiti
state.value.toFunctions.forEach((wrappedMethod, i) => {
state.value.toFunctions[i].result = wrappedMethod.callback(state.value.primitive)
});
state.value.methods.forEach((method, i) => {
state.value.methods[i].models.forEach((model, j) => {
state.value.methods[i].models[j] = '';
});
state.value.methods[i].result = '';
});
} catch(error: any) {
state.value.error = `${error}`;
}
+4
View File
@@ -22,6 +22,10 @@ import {
# Primitive Tools
These primitives map closely to a primitives package that will be provided by XO in future.
Note that these are still unstable and will change in future.
## Address
<div class="tool">
+10
View File
@@ -3,3 +3,13 @@
**NOTE: This package is EXTREMELY UNSTABLE and WILL BREAK frequently. DO NOT USE.**
A collection of immutable primitives for use with BCH.
These Primitives follow a declarative style and are **NOT** optimized for tree-shaking.
## Quick Start
1. Install the Primitives.
```sh
npm install @xocash/primitives
```
+54 -34
View File
@@ -1,10 +1,9 @@
import { Bytes } from './bytes.js';
import {
Base58AddressFormatVersion,
addressContentsToLockingBytecode,
lockingBytecodeToAddressContents,
isHex,
binToHex,
hexToBin,
decodeBase58Address,
decodeCashAddress,
disassembleBytecodeBCH,
@@ -27,6 +26,25 @@ export const DEFAULT_ADDRESS_PREFIXES = [
/**
* Address Entity.
*
* @example
*
* ```ts
* // Import our primitives.
* import { Address } from '@xocash/primitives';
*
* // Instantiate from a CashAddr (Legacy Addresses also supported).
* const address = Address.from('bitcoincash:qpmlpygrtzznznhgsl4c245xgmglpfh0vgcqns0q0r');
*
* // Convert to Locking Bytecode.
* console.log(address.toLockscriptBytes());
*
* // Convert to Legacy Address.
* console.log(address.toLegacy());
*
* // Convert to Token Address.
* console.log(address.toTokenAddress());
* ```
*/
export class Address {
protected readonly addressContents: KnownAddressTypeContents;
@@ -81,7 +99,7 @@ export class Address {
.find((result) => typeof result !== 'string');
if (!decodeResult) {
throw new Error('Invalid address format');
throw new Error(`Cannot decode address '${cashAddr}': ${decodeResult}`);
}
// Define the encode type in advance.
@@ -98,7 +116,9 @@ export class Address {
encodeType = decodeResult.payload.length === 32 ? 'P2SH32' : 'P2SH20';
break;
default:
throw new Error(`Unsupported address type: ${decodeResult.type}`);
throw new Error(
`Cannot decode address '${cashAddr}': Unsupported address type '${decodeResult.type}'`,
);
}
// Return new instance of address.
@@ -120,7 +140,9 @@ export class Address {
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
throw new Error(
`Cannot decode address '${legacyAddress}': ${decodeResult}`,
);
}
// Define the encode type in advance.
@@ -137,7 +159,9 @@ export class Address {
encodeType = 'P2SH20';
break;
default:
throw new Error(`Unsupported address type: ${decodeResult.version}`);
throw new Error(
`Cannot decode address '${legacyAddress}': Unsupported address type '${decodeResult.version}'`,
);
}
return new this({ payload: decodeResult.payload, type: encodeType });
@@ -146,23 +170,37 @@ export class Address {
/**
* Instantiate an address from Contract Bytecode.
*
* @param contractBytes The contract bytecode
* @param bytes The contract bytecode
*
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
*
* @returns {Address} Instance of Address.
*/
public static fromRedeemScript(
contractBytes: Uint8Array,
bytes: Uint8Array,
type: 'P2SH20' | 'P2SH32' = 'P2SH32',
): Address {
// Do an initial SHA256 on the Contract Bytes.
let payload = sha256.hash(contractBytes);
let payload = sha256.hash(bytes);
if (type === 'P2SH20') {
// Make sure length is 20 bytes.
if (bytes.length !== 20) {
throw new Error(
`Invalid payload length. Received '${bytes.length}', expected 20 for P2SH20`,
);
}
// RIPEMD160 the SHA256 hash.
payload = ripemd160.hash(payload);
} else if (type === 'P2SH32') {
// Make sure length is 32 bytes.
if (bytes.length !== 32) {
throw new Error(
`Invalid payload length. Received '${bytes.length}', expected 32 for P2SH20`,
);
}
// SHA256 Hash the SHA256 hash a seconr time.
payload = sha256.hash(payload);
}
@@ -184,14 +222,8 @@ export class Address {
hex: string,
type: 'P2SH20' | 'P2SH32' = 'P2SH32',
): Address {
if (!isHex(hex)) {
throw new Error('Invalid hex');
}
const contractBytecode = hexToBin(hex);
// Return a new instance of address.
return this.fromRedeemScript(contractBytecode, type);
return this.fromRedeemScript(Bytes.fromHex(hex).toBytes(), type);
}
/**
@@ -224,13 +256,7 @@ export class Address {
* @returns {Address} Instance of Address.
*/
public static fromLockscriptHex(hex: string): Address {
if (!isHex(hex)) {
throw new Error('Invalid hex');
}
const lockScriptBytes = hexToBin(hex);
return this.fromLockscriptBytes(lockScriptBytes);
return this.fromLockscriptBytes(Bytes.fromHex(hex).toBytes());
}
/**
@@ -243,7 +269,7 @@ export class Address {
*
* @returns {Address} Instance of Address.
*/
public static fromHash160(
public static fromHash160Bytes(
hash160: Uint8Array,
type: AddressType = 'P2PKH',
): Address {
@@ -271,13 +297,7 @@ export class Address {
hash160Hex: string,
type: AddressType = 'P2PKH',
): Address {
if (!isHex(hash160Hex)) {
throw new Error('Invalid hex');
}
const hash160Bytes = hexToBin(hash160Hex);
return this.fromHash160(hash160Bytes, type);
return this.fromHash160Bytes(Bytes.fromHex(hash160Hex).toBytes(), type);
}
/**
@@ -373,7 +393,7 @@ export class Address {
* @returns {Uint8Array} The Hash160 of this address.
*/
public toHash160Hex(): string {
return binToHex(this.addressContents.payload);
return Bytes.fromBytes(this.addressContents.payload).toHex();
}
/**
@@ -496,7 +516,7 @@ export class Address {
* @returns {Uint8Array} The lockscript as a hex string.
*/
public toLockscriptHex(): string {
return binToHex(this.toLockscriptBytes());
return Bytes.fromBytes(this.toLockscriptBytes()).toHex();
}
/**
+23 -11
View File
@@ -1,4 +1,6 @@
import { binToHex, hexToBin, isHex, hash256 } from '@bitauth/libauth';
import { Bytes } from './bytes.js';
import { hash256 } from '@bitauth/libauth';
export interface BlockHeaderRaw {
version: number;
@@ -13,6 +15,21 @@ export interface BlockHeaderRaw {
* 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.
*
* @example
*
* ```ts
* // Import our primitives.
* import { BlockHeader } from '@xocash/primitives';
*
* // Instantiate BlockHeader Primitive from Hex.
* const blockHeader = BlockHeader.from('blockHeaderHex');
*
* // Output Block Header information.
* console.log(blockHeader.getVersion());
* console.log(blockHeader.getTimestamp());
* console.log(blockHeader.getNonce());
* ```
*/
export class BlockHeader {
/**
@@ -80,12 +97,7 @@ export class BlockHeader {
* @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 this.fromBytes(hexToBin(hex));
return this.fromBytes(Bytes.fromHex(hex).toBytes());
}
/**
@@ -149,7 +161,7 @@ export class BlockHeader {
* @returns 160-character hex string representing the block header
*/
toHex(): string {
return binToHex(this.toBytes());
return Bytes.fromBytes(this.toBytes()).toHex();
}
/**
@@ -193,7 +205,7 @@ export class BlockHeader {
* @returns 64-character hex string
*/
getPreviousBlockHashHex(): string {
return binToHex(this.previousBlockHash);
return Bytes.fromBytes(this.previousBlockHash).toHex();
}
/**
@@ -201,7 +213,7 @@ export class BlockHeader {
* @returns 64-character hex string
*/
getMerkleRootHex(): string {
return binToHex(this.merkleRoot);
return Bytes.fromBytes(this.merkleRoot).toHex();
}
/**
@@ -217,6 +229,6 @@ export class BlockHeader {
* @returns 64-character hex string
*/
getHashHex(): string {
return binToHex(this.getHash());
return Bytes.fromBytes(this.getHash()).toHex();
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ export class Bytes {
public static fromBase64(base64: string): Bytes {
// If the Base64 is invalid, throw an error.
if (!isBase64(base64)) {
throw new Error('Invalid base64 string');
throw new Error(`Cannot decode invalid base64 '${base64}'`);
}
return new this(base64ToBin(base64));
@@ -96,7 +96,7 @@ export class Bytes {
public static fromHex(hex: string): Bytes {
// If the hex is invalid, throw an error.
if (!isHex(hex)) {
throw new Error('Invalid hexidecimal string');
throw new Error(`Cannot decode invalid hex '${hex}'`);
}
return new this(hexToBin(hex));
+22 -6
View File
@@ -21,6 +21,25 @@ import type {
/**
* Hierarchically Deterministic Private Node Entity.
*
* @example
*
* ```ts
* // Import our primitives.
* import { Mnemonic, HDPrivateNode } from '@xocash/primitives';
*
* // Generate a mnemonic.
* const mnemonic = Mnemonic.generateRandom();
*
* // Initialize a HD Private Node from Mnemonic.
* const hdPrivateNode = HDPrivateNode.fromMnemonic(mnemonic);
*
* // Derive the first Private Key.
* const privateKey = hdPrivateNode.derivePath("m/44'/145'/0'/0/0").toPrivateKey();
*
* // Output the BCH Address.
* console.log(privateKey.derivePublicKey().deriveAddress().toCashAddr());
* ```
*/
export class HDPrivateNode {
// The HD Private Node.
@@ -72,18 +91,15 @@ export class HDPrivateNode {
/**
* Creates a HD Private Node from the given mnemonic.
*
* @param phrase The mnemonic phrase as a string or array of words.
* @param mnemonic The mnemonic to use.
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns The created HD Private Node.
*/
public static fromMnemonic(phrase: string | Array<string>): HDPrivateNode {
// Derive the seed from the mnemonic.
const seed = Mnemonic.from(phrase).toSeed();
public static fromMnemonic(mnemonic: Mnemonic): HDPrivateNode {
// Return a new HDPrivateNode from the given seed.
return this.fromSeed(seed);
return this.fromSeed(mnemonic.toSeed());
}
/**
-1
View File
@@ -10,6 +10,5 @@ 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';
+16
View File
@@ -12,6 +12,22 @@ export type MnemonicRaw = {
/**
* Class representing a BIP39 Mnemonic phrase.
* Supports 12, 15, 18, 21, and 24 word phrases with optional passphrase.
*
* @example
*
* ```ts
* // Import our primitives.
* import { Mnemonic } from '@xocash/primitives';
*
* // Generate a random mnemonic.
* const mnemonic = Mnemonic.generateRandom();
*
* // Output the mnemonic phrase.
* console.log(mnemonic.toPhrase());
*
* // Derive the seed from the mnemonic.
* console.log(mnemonic.toSeed());
* ```
*/
export class Mnemonic {
protected static VALID_WORD_COUNTS = new Set([12, 15, 18, 21, 24]);
+20 -8
View File
@@ -2,7 +2,8 @@ import { Bytes } from './bytes.js';
import { PublicKey } from './public-key.js';
import {
binToHex,
type RecoverableSignature,
type WalletImportFormatType,
decodePrivateKeyWif,
encodePrivateKeyWif,
generatePrivateKey,
@@ -11,13 +12,24 @@ import {
sha256,
} from '@bitauth/libauth';
import type {
RecoverableSignature,
WalletImportFormatType,
} from '@bitauth/libauth';
/**
* Private Key entity.
*
* @example
*
* ```ts
* // Import our primitives
* import { PrivateKey } from '@xocash/primitives';
*
* // Create a Private Key from a WIF.
* const privateKey = PrivateKey.from('wif');
*
* // Derive the Public Key
* const publicKey = PrivateKey.derivePublicKey();
*
* // Output the CashAddr.
* console.log(publicKey.deriveAddress().toCashAddr());
* ```
*/
export class PrivateKey {
// The raw bytes representing the Private Key.
@@ -101,7 +113,7 @@ export class PrivateKey {
*/
public static fromWIF(
wif: string,
removePrefixes = ['bitcoincash:', 'wif:'],
removePrefixes = ['bitcoincash:', 'wif:', 'bch-wif:'],
): PrivateKey {
// Remove the prefixes from the WIF (if they exist).
const wifWithoutPrefixes = removePrefixes.reduce((result, prefix) => {
@@ -176,7 +188,7 @@ export class PrivateKey {
*/
public toHex(): string {
// Return the hexadecimal encoded bytes of this Private Key.
return binToHex(this.bytes);
return Bytes.fromBytes(this.bytes).toHex();
}
/**
+26 -28
View File
@@ -1,17 +1,26 @@
import { Bytes } from './bytes.js';
import { Address } from './address.js';
import {
binToHex,
hexToBin,
isHex,
secp256k1,
sha256,
ripemd160,
utf8ToBin,
} from '@bitauth/libauth';
import { secp256k1, sha256, ripemd160, utf8ToBin } from '@bitauth/libauth';
/**
* Public Key Entity.
*
* @example
*
* ```ts
* // Import our primitives
* import { PublicKey } from '@xocash/primitives';
*
* // Create a Public Key from Hex.
* const publicKey = PublicKey.from('publicKeyHex');
*
* // Derive the address.
* const address = publicKey.deriveAddress();
*
* // Output as a CashAddr.
* console.log(address.toCashAddr());
* ```
*/
export class PublicKey {
// The raw bytes representing the Public Key.
@@ -69,18 +78,7 @@ export class PublicKey {
* @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.`,
);
}
// Convert the hex to binary (Uint8Array).
const publicKey = hexToBin(publicKeyHex);
// Create a Public Key Entity from the binary.
return new this(publicKey);
return new this(Bytes.fromHex(publicKeyHex).toBytes());
}
/**
@@ -102,7 +100,7 @@ export class PublicKey {
* @returns The raw bytes Public Key encoded as Hexadecimal.
*/
public toHex(): string {
return binToHex(this.bytes);
return Bytes.fromBytes(this.bytes).toHex();
}
/**
@@ -158,7 +156,7 @@ export class PublicKey {
public deriveHash160Hex(): string {
const hash160Bytes = this.deriveHash160();
return binToHex(hash160Bytes);
return Bytes.fromBytes(hash160Bytes).toHex();
}
/**
@@ -171,7 +169,7 @@ export class PublicKey {
const ripemd160Hash = this.deriveHash160();
// Return an instance of address
return Address.fromHash160(ripemd160Hash, 'P2PKH');
return Address.fromHash160Bytes(ripemd160Hash, 'P2PKH');
}
public verifyMessageSchnorr(
@@ -234,11 +232,11 @@ export class PublicKey {
signature: Uint8Array | string,
): boolean {
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
messageHash = Bytes.fromHex(messageHash).toBytes();
}
if (typeof signature === 'string') {
signature = hexToBin(signature);
signature = Bytes.fromHex(signature).toBytes();
}
const validity = secp256k1.verifySignatureDER(
@@ -263,11 +261,11 @@ export class PublicKey {
signature: Uint8Array | string,
): boolean {
if (typeof messageHash === 'string') {
messageHash = hexToBin(messageHash);
messageHash = Bytes.fromHex(messageHash).toBytes();
}
if (typeof signature === 'string') {
signature = hexToBin(signature);
signature = Bytes.fromHex(signature).toBytes();
}
const validity = secp256k1.verifySignatureSchnorr(
+42 -1
View File
@@ -2,6 +2,22 @@ export type BCHUnits = ['sats', 'bits', 'mBCH', 'BCH'];
/**
* Satoshis Primitive
*
* @example
*
* ```ts
* // Import our primitives
* import { Satoshis } from '@xocash/primitives';
*
* // Create an amount from BCH.
* const sats = Satoshis.fromBCH(0.54321);
*
* // Output as a satoshi number.
* console.log(satoshis.toSats());
*
* // Output as a satoshis bigint.
* console.log(satoshis.toBigInt());
* ```
*/
export class Satoshis {
protected readonly satoshis: bigint;
@@ -81,6 +97,19 @@ export class Satoshis {
return new this(satoshis);
}
/**
* 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(sats: bigint) {
return new this(sats);
}
/**
* Rounds a number to the given number of digits.
*
@@ -89,7 +118,10 @@ export class Satoshis {
*
* @returns {Number} The rounded number.
*/
public static roundToDigits(numberToRound: number, digits: number): number {
protected static roundToDigits(
numberToRound: number,
digits: number,
): number {
// Set the options of the Number Format object.
const options: Intl.NumberFormatOptions = {
style: 'decimal',
@@ -165,4 +197,13 @@ export class Satoshis {
public toBCH(): number {
return Satoshis.roundToDigits(Number(this.satoshis) / 100_000_000, 8);
}
/**
* Converts this primitive to a serializable raw object representation.
*
* @returns Serialzied primitive data.
*/
toRaw() {
return this.satoshis;
}
}
+67 -20
View File
@@ -1,13 +1,25 @@
import {
assembleBytecodeBCH,
disassembleBytecodeBCH,
binToHex,
hexToBin,
isHex,
} from '@bitauth/libauth';
import { Bytes } from './bytes.js';
import { assembleBytecodeBCH, disassembleBytecodeBCH } from '@bitauth/libauth';
/**
* Script Entity.
*
* @example
*
* ```ts
* // Import our primitives
* import { Script } from '@xocash/primitives';
*
* // Create a script from ASM.
* const script = Script.fromASM('OP_RETURN <0xbeef>');
*
* // Output as in hexadecimal representation.
* console.log(script.toHex());
*
* // Output as bytecode.
* console.log(script.toBytes());
* ```
*/
export class Script {
protected readonly bytes: Uint8Array;
@@ -46,7 +58,11 @@ export class Script {
* @returns {Script} The created Script Entity.
*/
public static fromBytes(bytes: Uint8Array): Script {
return new this(bytes);
// Disassemble the bytecode.
const disassembledBytecode = disassembleBytecodeBCH(bytes);
// And then try to instantiate it from ASM to catch any errors.
return this.fromASM(disassembledBytecode);
}
/**
@@ -58,7 +74,22 @@ export class Script {
*
* @returns {Script} The created Script Entity.
*/
public static fromASM(asm: string): Script {
public static fromASM(asm: string, disallowUnknown = true): Script {
// If we should disallow unknown OP codes...
if (disallowUnknown) {
// Check if there are any unknown OP Codes.
const containsUnknown = asm
.split(/\s+/)
.some((opCode) => opCode.startsWith('OP_UNKNOWN'));
// Throw an error if there are unknown OP codes.
if (containsUnknown) {
throw new Error(
`Failed to decode script '${asm}': Unknown OP codes detected (set disallowUnknown to false if this is expected)`,
);
}
}
// Attempt to convert the given ASM to bytecode.
const assembleResult = assembleBytecodeBCH(asm);
@@ -69,7 +100,9 @@ export class Script {
// If the success flag is not marked, this indicates an error...
if (!assembleResult.success) {
throw new Error('Could not assemble bytecode from assembly.');
throw new Error(
`Failed to decode '${asm}': Could not assemble bytecode from assembly.`,
);
}
// Return new Script Entity from the assembled bytecode.
@@ -86,16 +119,21 @@ export class Script {
* @returns {Script} The created Script Entity.
*/
public static fromHex(hex: string): Script {
// Ensure that the given string is hex.
if (!isHex(hex)) {
throw new Error(`The given string (${hex}) is not valid hexadecimal.`);
}
// Conver the given hex to binary (Uint8Array).
const scriptBin = hexToBin(hex);
// Return new Script entity constructed from the binary.
return new this(scriptBin);
return this.fromBytes(Bytes.fromHex(hex).toBytes());
}
/**
* 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);
}
/**
@@ -128,6 +166,15 @@ export class Script {
* @returns {string} The compiled bytes as a hex string.
*/
public toHex(): string {
return binToHex(this.bytes);
return Bytes.fromBytes(this.bytes).toHex();
}
/**
* Converts this primitive to a serializable raw object representation.
*
* @returns Serialzied primitive data.
*/
toRaw() {
return this.bytes;
}
}
-160
View File
@@ -1,160 +0,0 @@
import { binToHex, hexToBin, hashTransaction, isHex } from '@bitauth/libauth';
/**
* Transaction Hash Primitive.
*/
export class TransactionHash {
protected readonly bytes: Uint8Array;
/**
* Instantiates TransactionHash from a Transaction Hash's bytes.
*
* @param bytes {Uint8Array} The Transaction Hash's bytes.
*/
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
? this.fromBytes(hash)
: this.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(
`Invalid Transaction Hash: Expected 32 bytes but received ${bytes.length}`,
);
}
return new this(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');
}
const bytes = hexToBin(hex);
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
? this.fromTransactionBytes(transaction)
: this.fromTransactionHex(transaction);
}
/**
* 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 this(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');
}
const bytes = hexToBin(transactionHex);
return this.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.
*
* @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;
}
/**
* Converts this primitive to a serializable representation.
*
* @returns Serialzied primitive data.
*/
toRaw() {
return this.bytes;
}
}
+387 -106
View File
@@ -1,110 +1,20 @@
import { TransactionHash } from './transaction-hash.js';
import { Bytes } from './bytes.js';
import {
binToHex,
hexToBin,
encodeTransaction,
decodeTransaction,
isHex,
decodeTransactionBCH,
hashTransaction,
} from '@bitauth/libauth';
import type { TransactionBCH } from '@bitauth/libauth';
import type { ResolvedTransactionBCH, TransactionBCH } from '@bitauth/libauth';
/**
* Transaction Primitive.
*/
export class Transaction {
protected constructor(public transaction: TransactionBCH) {}
class BaseTransaction {
protected readonly transaction: TransactionBCH;
/**
* Instantiates a transaction from either:
*
* 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 The created Transaction Primitive.
*/
public static from(transaction: Uint8Array | string) {
return transaction instanceof Uint8Array
? this.fromBytes(transaction)
: this.fromHex(transaction);
}
/**
* 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.
const decodeResult = decodeTransaction(bytes);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new instance of the Transaction Primitive created from the decode result.
return new this(decodeResult);
}
/**
* Create a Transaction Primitive from the given byte data encoded as hexadecimal string.
*
* @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 Primitive.
*/
public static fromHex(hex: string): Transaction {
// Ensure that the given string is hex.
if (!isHex(hex)) {
throw new Error('The given string is not valid hexadecimal.');
}
// Convert our hex to binary.
const bytes = hexToBin(hex);
// Return the transaction constructed from our bytes.
return this.fromBytes(bytes);
}
/**
* Create a Transaction Primitive from LibAuth's TransactionBCH type.
*
* @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).
*
* @returns The created Transaction Primitive.
*/
public static fromRaw(transaction: TransactionBCH) {
return new this(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 {
try {
this.from(transaction);
} catch (_error) {
return false;
}
return true;
protected constructor(transaction: TransactionBCH) {
this.transaction = transaction;
}
/**
@@ -151,14 +61,6 @@ export class Transaction {
return this.toHash().toBytes();
}
/**
* Converts transaction to a raw object representation.
* @returns A TransactionBCH LibAuth object.
*/
public toRaw(): TransactionBCH {
return { ...this.transaction };
}
/**
* Gets the version for this transaction.
*
@@ -232,3 +134,382 @@ export class Transaction {
return isOutpointZero && isOutpointIndexMax;
}
}
/**
* Transaction Primitive.
*
* @example
*
* ```ts
* // Import our primitives.
* import { Transaction } from '@xocash/primitive';
*
* // Instantiate primitive from Transaction Hex.
* const transaction = Transaction.from('transactionHex');
*
* // Output Transaction Information.
* console.log(transaction.getVersion());
* console.log(transaction.getLocktime());
* console.log(transaction.getInputs());
* console.log(transaction.getOutputs());
*
* // Output the Transaction Hash.
* console.log(transaction.toHash().toHex());
* ```
*/
export class Transaction extends BaseTransaction {
protected constructor(transaction: TransactionBCH) {
super(transaction);
}
/**
* Instantiates a transaction from either:
*
* 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 The created Transaction Primitive.
*/
public static from(transaction: Uint8Array | string) {
return transaction instanceof Uint8Array
? this.fromBytes(transaction)
: this.fromHex(transaction);
}
/**
* 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.
const decodeResult = decodeTransaction(bytes);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new instance of the Transaction Primitive created from the decode result.
return new this(decodeResult);
}
/**
* Create a Transaction Primitive from the given byte data encoded as hexadecimal string.
*
* @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 Primitive.
*/
public static fromHex(hex: string): Transaction {
const bytes = Bytes.fromHex(hex).toBytes();
return this.fromBytes(bytes);
}
/**
* Create a Transaction Primitive from LibAuth's TransactionBCH type.
*
* @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).
*
* @returns The created Transaction Primitive.
*/
public static fromRaw(transaction: TransactionBCH) {
return new this(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 {
try {
this.from(transaction);
} catch (_error) {
return false;
}
return true;
}
/**
* Converts transaction to a raw object representation.
* @returns A TransactionBCH LibAuth object.
*/
public toRaw(): TransactionBCH {
return { ...this.transaction };
}
}
export class TransactionResolved extends BaseTransaction {
protected sourceOutputs: Array<any>;
protected constructor(
transaction: TransactionBCH,
sourceOutputs: Array<any>,
) {
super(transaction);
this.sourceOutputs = sourceOutputs;
}
/**
* Instantiates a transaction from either:
*
* 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 The created Transaction Primitive.
*/
public static from(
transaction: Uint8Array | string,
sourceOutputs: Array<any>,
) {
return transaction instanceof Uint8Array
? this.fromBytes(transaction, sourceOutputs)
: this.fromHex(transaction, sourceOutputs);
}
/**
* 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, sourceOutputs: Array<any>) {
// Attempt to decode the raw transaction bytes.
const decodeResult = decodeTransactionBCH(bytes);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new instance of the Transaction Primitive created from the decode result.
return new this(decodeResult, sourceOutputs);
}
/**
* Create a Transaction Primitive from the given byte data encoded as hexadecimal string.
*
* @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 Primitive.
*/
public static fromHex(hex: string, sourceOutputs: Array<any>) {
const bytes = Bytes.fromHex(hex).toBytes();
return this.fromBytes(bytes, sourceOutputs);
}
/**
* Create a Transaction Primitive from LibAuth's TransactionBCH type.
*
* @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).
*
* @returns The created Transaction Primitive.
*/
public static fromRaw(resolvedTransaction: ResolvedTransactionBCH) {
return new this(
resolvedTransaction.transaction,
resolvedTransaction.sourceOutputs,
);
}
/**
* 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,
sourceOutputs: Array<any>,
): boolean {
try {
this.from(transaction, sourceOutputs);
} catch (_error) {
return false;
}
return true;
}
public toRaw() {
return {
transaction: this.transaction,
sourceOutputs: this.sourceOutputs,
};
}
}
/**
* Transaction Hash Primitive.
*/
export class TransactionHash {
/**
* Instantiates TransactionHash from a Transaction Hash's bytes.
*
* @param bytes {Uint8Array} The Transaction Hash's bytes.
*/
protected constructor(protected readonly 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
? this.fromBytes(hash)
: this.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(
`Invalid Transaction Hash: Expected 32 bytes but received ${bytes.length}`,
);
}
return new this(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 {
const bytes = Bytes.fromHex(hex).toBytes();
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
? this.fromTransactionBytes(transaction)
: this.fromTransactionHex(transaction);
}
/**
* 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 this(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 {
const bytes = Bytes.fromHex(transactionHex).toBytes();
return this.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.
*
* @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;
}
/**
* Converts this primitive to a serializable representation.
*
* @returns Serialzied primitive data.
*/
toRaw() {
return this.bytes;
}
}
+142
View File
@@ -0,0 +1,142 @@
import { describe, it, expect } from 'vitest';
import { Address } from '~/src/address';
import { Bytes } from '~/src/bytes';
describe('Success cases', () => {
// Test cases with different address representations
type AddressCase = {
raw: {
type: 'P2PKH' | 'P2SH20' | 'P2SH32' | 'P2PK';
payload: Uint8Array;
};
representations: {
cashAddr: string;
legacy: string;
tokenAddr: string;
lockscriptBytes: Uint8Array;
lockscriptHex: string;
lockscriptASM: string;
hash160Bytes: Uint8Array;
hash160Hex: string;
};
};
const testCases: AddressCase[] = [
{
raw: {
type: 'P2PKH',
payload: Bytes.fromHex(
'626025854e5a67ca5eb122e213f409ec842d45e4',
).toBytes(),
},
representations: {
cashAddr: 'bitcoincash:qp3xqfv9fedx0jj7ky3wyyl5p8kggt29usvlhrrxws',
legacy: '19yAQBS32jTcMFutTVCyp5e1y38JA9Qq8j',
tokenAddr: 'bitcoincash:zp3xqfv9fedx0jj7ky3wyyl5p8kggt29ust4yadq3r',
lockscriptBytes: Bytes.fromHex(
'76a914626025854e5a67ca5eb122e213f409ec842d45e488ac',
).toBytes(),
lockscriptHex: '76a914626025854e5a67ca5eb122e213f409ec842d45e488ac',
lockscriptASM:
'OP_DUP OP_HASH160 OP_PUSHBYTES_20 0x626025854e5a67ca5eb122e213f409ec842d45e4 OP_EQUALVERIFY OP_CHECKSIG',
hash160Bytes: Bytes.fromHex(
'626025854e5a67ca5eb122e213f409ec842d45e4',
).toBytes(),
hash160Hex: '626025854e5a67ca5eb122e213f409ec842d45e4',
},
},
];
// Test from methods
const fromMethods = [
{ name: 'fromCashAddr', key: 'cashAddr' },
{ name: 'fromLegacy', key: 'legacy' },
{ name: 'fromLockscriptBytes', key: 'lockscriptBytes' },
{ name: 'fromLockscriptHex', key: 'lockscriptHex' },
{ name: 'fromHash160Bytes', key: 'hash160Bytes' },
{ name: 'fromHash160Hex', key: 'hash160Hex' },
] as const;
fromMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const value = representations[key];
const address = Address[name](value);
expect(address.toRaw()).toStrictEqual(raw);
});
});
// Test to methods
const toMethods = [
{ name: 'toCashAddr', key: 'cashAddr' },
{ name: 'toLegacy', key: 'legacy' },
{ name: 'toTokenAddress', key: 'tokenAddr' },
{ name: 'toLockscriptBytes', key: 'lockscriptBytes' },
{ name: 'toLockscriptHex', key: 'lockscriptHex' },
{ name: 'toLockscriptAsm', key: 'lockscriptASM' },
{ name: 'toHash160Bytes', key: 'hash160Bytes' },
{ name: 'toHash160Hex', key: 'hash160Hex' },
] as const;
toMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const address = Address.fromRaw(raw);
const expected = representations[key];
expect(address[name]()).toStrictEqual(expected);
});
});
// Test generic from method
it.each(testCases)('from(cashAddr)', ({ raw, representations }) => {
const address = Address.from(representations.cashAddr);
expect(address.toRaw()).toStrictEqual(raw);
});
it.each(testCases)('from(legacy)', ({ raw, representations }) => {
const address = Address.from(representations.legacy);
expect(address.toRaw()).toStrictEqual(raw);
});
// Test validation methods
it.each(testCases)('isValid() for CashAddr', ({ representations }) => {
expect(Address.isValid(representations.cashAddr)).toBe(true);
});
it.each(testCases)('isValid() for Legacy', ({ representations }) => {
expect(Address.isValid(representations.legacy)).toBe(true);
});
it.each(testCases)('isValidCashAddr()', ({ representations }) => {
expect(Address.isValidCashAddr(representations.cashAddr)).toBe(true);
});
it.each(testCases)('isValidLegacy()', ({ representations }) => {
expect(Address.isValidLegacy(representations.legacy)).toBe(true);
});
});
describe('Error cases', () => {
const invalidCases = [
{
method: 'fromCashAddr',
input: 'invalid:address',
},
{
method: 'fromLegacy',
input: 'invalid_legacy_address',
},
{
method: 'fromHash160Hex',
input: 'invalid_hex',
},
{
method: 'fromLockscriptHex',
input: 'invalid_hex',
},
];
invalidCases.forEach(({ method, input }) => {
it(`${method}() throws on invalid input`, () => {
expect(() => Address[method](input)).toThrow();
});
});
});
@@ -0,0 +1,158 @@
import { describe, it, expect } from 'vitest';
import { BlockHeader } from '~/src/block-header';
import { Bytes } from '~/src/bytes';
describe('Success cases', () => {
// Test cases with different block header representations
type BlockHeaderCase = {
raw: {
version: number;
previousBlockHash: Uint8Array;
merkleRoot: Uint8Array;
timestamp: number;
bits: number;
nonce: number;
};
representations: {
bytes: Uint8Array;
hex: string;
};
};
const testCases: BlockHeaderCase[] = [
{
raw: {
version: 592633856,
previousBlockHash: Bytes.fromHex(
'df4da87b838b2dde9aaf26cb08380f15a57f32d99f2007000000000000000000',
).toBytes(),
merkleRoot: Bytes.fromHex(
'5e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b476',
).toBytes(),
timestamp: 1737338908,
bits: 402807112,
nonce: 3478133258,
},
representations: {
bytes: Bytes.fromHex(
'00e05223df4da87b838b2dde9aaf26cb08380f15a57f32d99f20070000000000000000005e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b4761cb08d67485902180a1a50cf',
).toBytes(),
hex: '00e05223df4da87b838b2dde9aaf26cb08380f15a57f32d99f20070000000000000000005e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b4761cb08d67485902180a1a50cf',
},
},
];
// Test from methods with their respective types
const fromMethods = [
{ name: 'fromBytes', key: 'bytes' },
{ name: 'fromHex', key: 'hex' },
] as const;
fromMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const blockHeader = BlockHeader[name](representations[key]);
expect(blockHeader.toRaw()).toStrictEqual(raw);
});
});
// Test generic from method
it.each(testCases)('from(Uint8Array)', ({ raw, representations }) => {
const blockHeader = BlockHeader.from(representations.bytes);
expect(blockHeader.toRaw()).toStrictEqual(raw);
});
it.each(testCases)('from(string)', ({ raw, representations }) => {
const blockHeader = BlockHeader.from(representations.hex);
expect(blockHeader.toRaw()).toStrictEqual(raw);
});
// Test to methods with their respective types
const toMethods = [
{ name: 'toBytes', key: 'bytes' },
{ name: 'toHex', key: 'hex' },
] as const;
toMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const blockHeader = BlockHeader.fromRaw(raw);
expect(blockHeader[name]()).toStrictEqual(representations[key]);
});
});
// Test getter methods
it.each(testCases)('getter methods return correct values', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
expect(blockHeader.getVersion()).toBe(raw.version);
expect(blockHeader.getPreviousBlockHash()).toStrictEqual(
raw.previousBlockHash,
);
expect(blockHeader.getMerkleRoot()).toStrictEqual(raw.merkleRoot);
expect(blockHeader.getTimestamp()).toBe(raw.timestamp);
expect(blockHeader.getBits()).toBe(raw.bits);
expect(blockHeader.getNonce()).toBe(raw.nonce);
});
// Test hex getter methods
it.each(testCases)('hex getter methods return correct values', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
expect(blockHeader.getPreviousBlockHashHex()).toBe(
Bytes.fromBytes(raw.previousBlockHash).toHex(),
);
expect(blockHeader.getMerkleRootHex()).toBe(
Bytes.fromBytes(raw.merkleRoot).toHex(),
);
});
// Test hash methods
it.each(testCases)('getHash() returns correct hash', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
const hash = blockHeader.getHash();
expect(hash).toBeInstanceOf(Uint8Array);
expect(hash.length).toBe(32);
});
it.each(testCases)('getHashHex() returns correct hash hex', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
const hashHex = blockHeader.getHashHex();
expect(typeof hashHex).toBe('string');
expect(hashHex.length).toBe(64);
});
// Test validation method
it.each(testCases)(
'isValid() returns true for valid inputs',
({ representations }) => {
expect(BlockHeader.isValid(representations.bytes)).toBe(true);
expect(BlockHeader.isValid(representations.hex)).toBe(true);
},
);
});
describe('Error cases', () => {
const invalidCases = [
{
method: 'fromBytes',
input: new Uint8Array(79), // Wrong length (should be 80 bytes)
},
{
method: 'fromHex',
input: 'invalid_hex',
},
{
method: 'fromHex',
input: '00'.repeat(79), // Wrong length (should be 80 bytes / 160 hex chars)
},
];
invalidCases.forEach(({ method, input }) => {
it(`${method}() throws on invalid input`, () => {
expect(() => BlockHeader[method](input)).toThrow();
});
});
it('isValid() returns false for invalid inputs', () => {
expect(BlockHeader.isValid(new Uint8Array(79))).toBe(false);
expect(BlockHeader.isValid('invalid_hex')).toBe(false);
expect(BlockHeader.isValid('00'.repeat(79))).toBe(false);
});
});
@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';
import { Satoshis } from '~/src/satoshis';
describe('Success cases', () => {
// Test cases with single representation for each unit
type ConversionCase = {
raw: bigint;
units: {
sats: number | bigint;
satoshis: number | bigint;
bits: number;
mBCH: number;
bch: number;
};
};
const testCases: ConversionCase[] = [
{
raw: 1n,
units: {
sats: 1,
satoshis: 1,
bits: 0.01,
mBCH: 0.00001,
bch: 0.00000001,
},
},
{
raw: 333333333n,
units: {
sats: 333333333,
satoshis: 333333333,
bits: 3333333.33,
mBCH: 3333.33333,
bch: 3.33333333,
},
},
];
// Test from methods with their respective types
const fromMethods = [
{ name: 'fromSats', key: 'sats' },
{ name: 'fromSatoshis', key: 'satoshis' },
{ name: 'fromBits', key: 'bits' },
{ name: 'fromMBCH', key: 'mBCH' },
{ name: 'fromBCH', key: 'bch' },
] as const;
fromMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, units }) => {
const satoshis = Satoshis[name](units[key]);
expect(satoshis.toRaw()).toStrictEqual(raw);
});
});
// Test to methods with their respective types
const toMethods = [
{ name: 'toSats', key: 'sats' },
{ name: 'toSatoshis', key: 'satoshis' },
{ name: 'toBits', key: 'bits' },
{ name: 'toMBCH', key: 'mBCH' },
{ name: 'toBCH', key: 'bch' },
] as const;
toMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, units }) => {
const satoshis = Satoshis.fromRaw(raw);
expect(satoshis[name]()).toStrictEqual(units[key]);
});
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, it, expect } from 'vitest';
import { Bytes } from '~/src/bytes';
import { Script } from '~/src/script';
describe('Success cases', () => {
// Test cases with different script representations
type ScriptCase = {
raw: Uint8Array;
representations: {
bytes: Uint8Array;
asm: string;
hex: string;
};
};
const testCases: ScriptCase[] = [
{
raw: Bytes.fromHex(
'76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
).toBytes(),
representations: {
bytes: Bytes.fromHex(
'76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
).toBytes(),
asm: 'OP_DUP\nOP_HASH160\nOP_PUSHBYTES_20\n0x876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d6\nOP_EQUALVERIFY\nOP_CHECKSIG',
hex: '76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
},
},
];
// Test from methods with their respective types
const fromMethods = [
{ name: 'fromBytes', key: 'bytes' },
{ name: 'fromASM', key: 'asm' },
{ name: 'fromHex', key: 'hex' },
] as const;
fromMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const script = Script[name](representations[key]);
expect(script.toRaw()).toStrictEqual(raw);
});
});
// Test generic from method
it.each(testCases)('from(Uint8Array)', ({ raw, representations }) => {
const script = Script.from(representations.bytes);
expect(script.toRaw()).toStrictEqual(raw);
});
it.each(testCases)('from(string)', ({ raw, representations }) => {
const script = Script.from(representations.hex);
expect(script.toRaw()).toStrictEqual(raw);
});
// Test to methods with their respective types
const toMethods = [
{ name: 'toBytes', key: 'bytes' },
{ name: 'toASM', key: 'asm' },
{ name: 'toHex', key: 'hex' },
] as const;
toMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const script = Script.fromRaw(raw);
expect(script[name]()).toStrictEqual(representations[key]);
});
});
// Test fromRaw specifically
it.each(testCases)('fromRaw() creates correct instance', ({ raw }) => {
const script = Script.fromRaw(raw);
expect(script.toRaw()).toStrictEqual(raw);
});
});
describe('Error cases', () => {
const invalidCases = [
{
method: 'fromASM',
input: 'OP_INVALID_OPCODE',
},
{
method: 'fromHex',
input: 'INVALID_HEX',
},
];
invalidCases.forEach(({ method, input }) => {
it(`${method}() throws on invalid input`, () => {
expect(() => Script[method](input)).toThrow();
});
});
});
@@ -0,0 +1,134 @@
// NOTE: Needs testing.
// Appears to work on 100,000 items though.
// Probably want to make it so that custom hash functions can be specified.
// Also, in future, we might want to be able to add to this on-the-fly (I forget the name of this particlar Bloom Filter, but there is one).
// Reason being that we might want to avoid re-computation of the filter each time a node requests it.
// That said, 100,000 only takes a few seconds.
// Rough Flow: Node B contacts Node A and sends some basic stats (how many activities it has, etc - needs more thought)
// Node A replies with its own stats.
// Node B sends a Bloom Filter of its current activities.
// Node A replies with any that might be missing.
interface Cell {
count: number;
idSum: bigint;
hashSum: bigint;
}
class InverseBloomFilter {
private size: number;
private cells: Cell[];
private items: Set<string>;
constructor(items: string[], expectedDiffSize?: number) {
this.items = new Set(items);
this.size = expectedDiffSize ?? Math.max(256, Math.ceil(items.length / 2));
this.cells = Array(this.size)
.fill(null)
.map(() => ({
count: 0,
idSum: 0n,
hashSum: 0n,
}));
for (const item of items) {
this.addItem(item);
}
}
private hashToIndex(value: string): number {
let hash = 2166136261;
for (let i = 0; i < value.length; i++) {
hash ^= value.charCodeAt(i);
hash +=
(hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
hash = hash >>> 0; // Keep as 32-bit unsigned
}
return hash % this.size;
}
private hashToId(value: string): bigint {
// Create a 64-bit max hash
let hash = 0n;
const mod = (1n << 63n) - 1n; // Use 63 bits to ensure we stay within signed 64-bit range
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5n) - hash + BigInt(value.charCodeAt(i))) % mod;
}
return hash;
}
private addItem(item: string): void {
const idx = this.hashToIndex(item);
const itemId = this.hashToId(item);
const cell = this.cells[idx];
cell.count += 1;
cell.idSum ^= itemId;
cell.hashSum ^= itemId;
}
toBytes(): Uint8Array {
const cellSize = 4 + 8 + 8;
const bytes = new Uint8Array(8 + this.size * cellSize);
const view = new DataView(bytes.buffer);
view.setBigInt64(0, BigInt(this.size), true);
let offset = 8;
for (const cell of this.cells) {
view.setInt32(offset, cell.count, true);
view.setBigInt64(offset + 4, cell.idSum, true);
view.setBigInt64(offset + 12, cell.hashSum, true);
offset += cellSize;
}
return bytes;
}
private subtractFilter(otherBytes: Uint8Array): Cell[] {
const view = new DataView(otherBytes.buffer);
const otherSize = Number(view.getBigInt64(0, true));
if (otherSize !== this.size) {
throw new Error('Filter sizes do not match');
}
const result = Array(this.size);
let offset = 8;
const cellSize = 20;
for (let i = 0; i < this.size; i++) {
const otherCount = view.getInt32(offset, true);
const otherIdSum = view.getBigInt64(offset + 4, true);
const otherHashSum = view.getBigInt64(offset + 12, true);
result[i] = {
count: this.cells[i].count - otherCount,
idSum: this.cells[i].idSum ^ otherIdSum,
hashSum: this.cells[i].hashSum ^ otherHashSum,
};
offset += cellSize;
}
return result;
}
findMaybeMissing(otherFilterBytes: Uint8Array): string[] {
const diffCells = this.subtractFilter(otherFilterBytes);
const missing: string[] = [];
for (const item of this.items) {
const idx = this.hashToIndex(item);
const itemId = this.hashToId(item);
const cell = diffCells[idx];
if (cell.count === 1 && cell.idSum === itemId) {
missing.push(item);
}
}
return missing;
}
}
+4 -1
View File
@@ -24,6 +24,7 @@ import {
type TokenBalances,
HDPrivateNode,
ExtMap,
Mnemonic,
calculateBalanceSats,
calculateBalanceTokens,
encodeExtendedJson,
@@ -149,7 +150,9 @@ export class WalletHD<
hexToBin(retrievedGenesisData.seed),
);
} else if ('mnemonic' in retrievedGenesisData) {
hdPrivateNode = HDPrivateNode.fromMnemonic(retrievedGenesisData.mnemonic);
hdPrivateNode = HDPrivateNode.fromMnemonic(
Mnemonic.fromPhrase(retrievedGenesisData.mnemonic),
);
} else {
hdPrivateNode = HDPrivateNode.fromXPriv(retrievedGenesisData.xpriv);
}