Merge branch 'inverse_bloom_filter' into 'development'
Draft Inverse Bloom Filter See merge request GeneralProtocols/xo/stack!19
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user