Monorepo stuff

Signed-off-by: James Zuccon <zuccon@gmail.com>
This commit is contained in:
James Zuccon
2024-09-02 20:05:35 +10:00
parent f2210e631a
commit 2ac7af4069
62 changed files with 13227 additions and 149 deletions
+3
View File
@@ -0,0 +1,3 @@
# XO Primitives
TODO
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@xo/primitives",
"version": "0.0.1",
"description": "XO Stack Primitives",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"build-old": "rollup -c"
},
"author": "",
"license": "ISC"
}
+422
View File
@@ -0,0 +1,422 @@
import {
Base58AddressFormatVersion,
addressContentsToLockingBytecode,
lockingBytecodeToAddressContents,
isHex,
binToHex,
hexToBin,
decodeBase58Address,
decodeCashAddress,
disassembleBytecodeBCH,
encodeBase58AddressFormat,
encodeCashAddress,
sha256,
ripemd160,
} from '@bitauth/libauth';
import type { KnownAddressTypeContents } from '@bitauth/libauth';
export type AddressType = 'P2PK' | 'P2PKH' | 'P2SH20' | 'P2SH32';
/**
* Address Entity.
*/
export class Address {
public readonly addressContents: KnownAddressTypeContents;
constructor(addressContents: KnownAddressTypeContents) {
this.addressContents = addressContents;
}
/**
* Decode a CashAddr into an Address Entity.
*
* @param address {string} The CashAddr to decode.
*
* @throws {Error} If address cannot be decoded.
*
* @returns {Address} Instance of Address.
*
* @example
* const address = Address.fromCashAddr('bitcoincash:qpeqv9k9j3l7fepp9du24f9pr5jdu7c2mvn4wwldav')
*/
public static fromCashAddr(cashAddr: string): Address {
// CashAddr
const decodeResult = decodeCashAddress(cashAddr);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Define the encode type in advance.
let encodeType: AddressType;
// Determine the encoding to use based on the Decoding Result's Type.
switch (decodeResult.type) {
case 'p2pkh':
encodeType = 'P2PKH';
break;
case 'p2sh':
encodeType = decodeResult.payload.length === 32 ? 'P2SH32' : 'P2SH20';
break;
default:
throw new Error(`Unsupported address type: ${decodeResult.type}`);
}
// Return new instance of address.
return new Address({ payload: decodeResult.payload, type: encodeType });
}
/**
* Decode a Legacy Address into an Address Entity.
*
* @param address {string} The Legacy (Base58) Address to decode.
*
* @throws {Error} If address cannot be decoded.
*
* @returns {Address} Instance of Address.
*/
public static fromLegacy(legacyAddress: string): Address {
// Decode the Legacy (Base58) Address.
const decodeResult = decodeBase58Address(legacyAddress);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Define the encode type in advance.
let encodeType: AddressType;
// Determine the encoding to use based on the Decoding Result's Type.
switch (decodeResult.version) {
case Base58AddressFormatVersion.p2pkh:
case Base58AddressFormatVersion.p2pkhTestnet:
encodeType = 'P2PKH';
break;
case Base58AddressFormatVersion.p2sh20:
case Base58AddressFormatVersion.p2sh20Testnet:
encodeType = 'P2SH20';
break;
default:
throw new Error(`Unsupported address type: ${decodeResult.version}`);
}
return new Address({ payload: decodeResult.payload, type: encodeType });
}
/**
* Decode a Cash Address or Legacy Address.
*
* @param address {string} The address to attempt to decode.
*
* @throws {Error} If address cannot be decoded.
*
* @returns {Address} Instance of Address.
*/
public static fromCashAddrOrLegacy(address: string): Address {
// Attempt to decode as a Cash Address.
try {
return Address.fromCashAddr(address);
} catch (error) {
// And if that fails, attempt to decode as a Base58 Address.
return Address.fromLegacy(address);
}
}
/**
* Instantiate an address from Contract Bytecode.
*
* @param contractBytes {Uint8Array} The contract bytecode
*
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
*
* @returns {Address} Instance of Address.
*/
public static fromRedeemScript(
contractBytes: Uint8Array,
type: 'P2SH20' | 'P2SH32' = 'P2SH32'
): Address {
// Do an initial SHA256 on the Contract Bytes.
let payload = sha256.hash(contractBytes);
if (type === 'P2SH20') {
// RIPEMD160 the SHA256 hash.
payload = ripemd160.hash(payload);
} else if (type === 'P2SH32') {
// SHA256 Hash the SHA256 hash a seconr time.
payload = sha256.hash(payload);
}
// Return a new instance of address.
return new Address({ payload, type });
}
/**
* Instantiate an address from Contract Bytecode as a hex string.
*
* @param contractBytes {Uint8Array} The contract bytecode
*
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
*
* @returns {Address} Instance of Address.
*/
public static fromRedeemScriptHex(
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 Address.fromRedeemScript(contractBytecode, type);
}
/**
* Attempt to instantiate Address from Locking Bytecode.
*
* @param lockScript {Uint8Array} The lockscript bytes.
*
* @throws {Error} If lock script cannot be decoded into an address.
*
* @returns {Address} Instance of Address.
*/
public static fromLockscriptBytes(lockScript: Uint8Array): Address {
const addressContents = lockingBytecodeToAddressContents(lockScript);
// If the type is not recognized, throw an error...
if (addressContents.type === 'unknown') {
throw new Error(`Invalid type (${addressContents.type})`);
}
return new Address(addressContents);
}
/**
* Attempt to instantiate Address from Locking Bytecode as a hex string.
*
* @param hex {Uint8Array} The lockscript bytes as hex.
*
* @throws {Error} If lock script cannot be decoded into an 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 Address.fromLockscriptBytes(lockScriptBytes);
}
/**
* Instantiate an address from a Ripemd160 hash.
*
* @param hash160 {Uint8Array} The hash to use.
* @param type {AddressType} The type of the address to instantiate (e.g. P2PKH).
*
* @throws {Error} If hash160 is not 20 bytes.
*
* @returns {Address} Instance of Address.
*/
public static fromHash160(
hash160: Uint8Array,
type: AddressType = 'P2PKH'
): Address {
// If the Hash160 bytes is not 20 in length, it is invalid...
if (hash160.length !== 20) {
throw new Error(
`Invalid hash160 length: Expected 20 (Received ${hash160.length})`
);
}
return new Address({ payload: hash160, type });
}
/**
* Instantiate an address from a Ripemd160 hash (as hex).
*
* @param hash160Hex {string} The hash to use (as hex).
* @param type {AddressType} The type of the address to instantiate (e.g. P2PKH).
*
* @throws {Error} If hash160 is not 20 bytes.
*
* @returns {Address} Instance of Address.
*/
public static fromHash160Hex(
hash160Hex: string,
type: AddressType = 'P2PKH'
): Address {
if (!isHex(hash160Hex)) {
throw new Error('Invalid hex');
}
const hash160Bytes = hexToBin(hash160Hex);
return Address.fromHash160(hash160Bytes, type);
}
public static isValid(address: string): boolean {
try {
Address.fromCashAddrOrLegacy(address);
} catch (error) {
return false;
}
return true;
}
public static isValidLegacy(address: string): boolean {
try {
Address.fromLegacy(address);
} catch (error) {
return false;
}
return true;
}
public static isValidCashAddr(address: string): boolean {
try {
Address.fromCashAddr(address);
} catch (error) {
return false;
}
return true;
}
/**
* Gets the type of address (P2PKH, P2SH).
*
* @returns {AddressType} The type of Address.
*/
public type(): AddressType {
return this.addressContents.type;
}
/**
* Gets this address' raw Hash160 (Ripemd160) hash as a Uint8Array.
*
* @returns {Uint8Array} The Hash160 of this address.
*/
public toHash160Bytes(): Uint8Array {
return this.addressContents.payload;
}
/**
* Gets this address' raw Hash160 (Ripemd16) hash as a hex string.
*
* @returns {Uint8Array} The Hash160 of this address.
*/
public toHash160Hex(): string {
return binToHex(this.addressContents.payload);
}
/**
* Converts this address to CashAddr format.
*
* @param networkPrefix {'bitcoincash' | 'bchtest' | 'bchreg'} The Cash Address Network prefix.
*
* @throws {Error} If address cannot be encoded as a CashAddr.
*
* @returns {string} The address in CashAddr format.
*/
public toCashAddr(
networkPrefix: 'bitcoincash' | 'bchtest' | 'bchreg' = 'bitcoincash',
supportTokens = false
): string {
// Define the encode type in advance.
let encodeType: 'p2pkh' | 'p2sh' | 'p2pkhWithTokens' | 'p2shWithTokens';
// Determine the encoding to use based on the Address Content's Type.
switch (this.addressContents.type) {
case 'P2PKH':
encodeType = supportTokens ? 'p2pkhWithTokens' : 'p2pkh';
break;
case 'P2SH20':
encodeType = supportTokens ? 'p2shWithTokens' : 'p2sh';
break;
case 'P2SH32':
encodeType = supportTokens ? 'p2shWithTokens' : 'p2sh';
break;
default:
throw new Error(
`Unsupported address type: ${this.addressContents.type}`
);
}
// NOTE: This function will throw if the payload is invalid.
return encodeCashAddress({
prefix: networkPrefix,
type: encodeType,
payload: this.addressContents.payload,
}).address;
}
/**
* Converts this address to Legacy (Base58) Address format.
*
* @throws {Error} If address cannot be encoded as a Legacy Address (Base58).
*
* @returns {string} The address in Legacy Address format.
*/
public toLegacy(): string {
// Define the encode type in advance.
let encodeType: Base58AddressFormatVersion;
// Determine the encoding to use based on the Address Content's Type.
switch (this.addressContents.type) {
case 'P2PKH':
encodeType = Base58AddressFormatVersion.p2pkh;
break;
case 'P2SH20':
encodeType = Base58AddressFormatVersion.p2sh20;
break;
default:
throw new Error(
`Unsupported address type: ${this.addressContents.type}`
);
}
// Encode the address in Base58 format.
return encodeBase58AddressFormat(encodeType, this.addressContents.payload);
}
/**
* Get the corresponding lockscript for this address as a Uint8Array.
*
* @returns {Uint8Array} The lockscript as a Uint8Array.
*/
public toLockscriptBytes(): Uint8Array {
return addressContentsToLockingBytecode(this.addressContents);
}
/**
* Get the corresponding lockscript for this address as a hex string.
*
* @returns {Uint8Array} The lockscript as a hex string.
*/
public toLockscriptHex(): string {
return binToHex(this.toLockscriptBytes());
}
/**
* Get the corresponding lockscript for this address as ASM.
*
* @returns {Uint8Array} The lockscript as ASN.
*/
public toLockscriptAsm(): string {
const lockscriptBytes = this.toLockscriptBytes();
const lockscriptAsm = disassembleBytecodeBCH(lockscriptBytes);
return lockscriptAsm;
}
}
+142
View File
@@ -0,0 +1,142 @@
import {
binToHex,
hexToBin,
isHex,
base64ToBin,
binToBase64,
isBase64,
binToUtf8,
utf8ToBin,
bigIntToBinUint64LE,
bigIntToBinUintLE,
binToBigIntUintLE,
} from '@bitauth/libauth';
/**
* Bytes Primitive.
*/
export class Bytes {
public readonly bytes: Uint8Array;
/**
* Instantiate Bytes from a Uint8Array.
*
* @param bytes {Uint8Array} The bytes to use.
*/
constructor(bytes: Uint8Array) {
this.bytes = bytes;
}
/**
* Instantiates bytes from a Base64 string.
*
* @param utf8 {string} The Base64 string.
*
* @throws {Error} If Base64 is invalid.
*
* @returns {Bytes} Instance of Bytes.
*/
public static fromBase64(base64: string): Bytes {
// If the Base64 is invalid, throw an error.
if (!isBase64(base64)) {
throw new Error('Invalid base64 string');
}
return new Bytes(base64ToBin(base64));
}
/**
* Instantiates bytes from a BigInt (LE encoded).
*
* @param number {number} The bigint number.
*
* @throws {Error} If number is invalid.
*
* @returns {Bytes} Instance of Bytes.
*/
public static fromBigInt(number: bigint): Bytes {
return new Bytes(bigIntToBinUintLE(number));
}
/**
* Instantiates bytes from a BigInt (64 Bit LE encoded).
*
* @param number {number} The bigint number.
*
* @throws {Error} If number is invalid.
*
* @returns {Bytes} Instance of Bytes.
*/
public static fromBigInt64(number: bigint): Bytes {
return new Bytes(bigIntToBinUint64LE(number));
}
/**
* Instantiates bytes from a hex string.
*
* @param hex {string} The hex string.
*
* @throws {Error} If hex is invalid.
*
* @returns {Bytes} Instance of Bytes.
*/
public static fromHex(hex: string): Bytes {
// If the hex is invalid, throw an error.
if (!isHex(hex)) {
throw new Error('Invalid hexidecimal string');
}
return new Bytes(hexToBin(hex));
}
/**
* Instantiates bytes from a UTF8 string.
*
* @param utf8 {string} The UTF8 string.
*
* @returns {Bytes} Instance of Bytes.
*/
public static fromUtf8(utf8: string): Bytes {
return new Bytes(utf8ToBin(utf8));
}
/**
* Return bytes as a Base64 string.
*
* @returns {string} Bytes as a Base64 string.
*/
public toBase64(): string {
return binToBase64(this.bytes);
}
public toBigInt(): bigint {
return binToBigIntUintLE(this.bytes);
}
/**
* Return bytes as a hex string.
*
* @returns {string} Bytes as a hex string.
*/
public toHex(): string {
return binToHex(this.bytes);
}
/**
* Return bytes as a UTF8 string
*
* @returns {string} Bytes as a UTF8 string.
*/
public toUtf8(): string {
return binToUtf8(this.bytes);
}
/**
* Return bytes as a Uint8Array
*
* @returns {Uint8Array} Bytes as a Uint8Array.
*/
public toBytes(): Uint8Array {
return this.bytes;
}
}
+58
View File
@@ -0,0 +1,58 @@
import {
ripemd160,
sha256,
} from '@bitauth/libauth';
/**
* Crypto Module.
*/
export class Crypto
{
/**
* SHA256 hash the given input
*
* @param input {Uint8Array} The input to hash.
*
* @returns {Uint8Array} The SHA256 Hash of the given input.
*/
public static sha256(input: Uint8Array): Uint8Array
{
return sha256.hash(input);
}
/**
* RIPEMD160 hash the given input
*
* @param input {Uint8Array} The input to hash.
*
* @returns {Uint8Array} The RIPEMD160 Hash of the given input.
*/
public static ripemd160(input: Uint8Array): Uint8Array
{
return ripemd160.hash(input);
}
/**
* Double SHA256 hash the given input
*
* @param input {Uint8Array} The input to hash.
*
* @returns {Uint8Array} The Double SHA256 Hash of the given input.
*/
public static hash256(input: Uint8Array): Uint8Array
{
return sha256.hash(sha256.hash(input));
}
/**
* RIPEMD160 and then SHA256 hash the given input
*
* @param input {Uint16Array} The input to hash.
*
* @returns {Uint8Array} The RIPEMD160+SHA256 Hash of the given input.
*/
public static hash160(input: Uint8Array): Uint8Array
{
return ripemd160.hash(sha256.hash(input));
}
}
+178
View File
@@ -0,0 +1,178 @@
import { HDPublicNode } from './hd-public-node.js';
import { PrivateKey } from './private-key.js';
import {
encodeHdPrivateKey,
decodeHdPrivateKey,
deriveHdPath,
deriveHdPublicNode,
deriveHdPrivateNodeFromSeed,
generateRandomBytes,
} from '@bitauth/libauth';
import type {
DecodedHdKey,
HdKeyNetwork,
HdPrivateNodeValid,
} from '@bitauth/libauth';
/**
* Hierarchically Deterministic Private Node Entity.
*/
export class HDPrivateNode {
// The HD Private Node.
public readonly node: HdPrivateNodeValid;
/**
* Construct a new HD Private Node.
*
* @param node {HdPrivateNodeValid} The HD Private Node.
*/
constructor(node: HdPrivateNodeValid) {
this.node = node;
}
/**
* Creates a HD Private Node from the given XPriv Key.
*
* @param xpriv {string} The XPriv Key.
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
*/
public static fromXPriv(xpriv: string): HDPrivateNode {
// Attempt to decode the XPriv Key.
const decodeResult = decodeHdPrivateKey(xpriv);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new HD Private Node from the given XPriv.
return new HDPrivateNode(decodeResult.node);
}
/**
* Creates a HD Private Node from a raw seed.
*
* @param seed {Uint8Array} The raw seed bytes.
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
*/
public static fromSeed(seed: Uint8Array): HDPrivateNode {
// If the seed is not 32 bytes, throw an error.
/*if (seed.length !== 32) {
throw new Error(
`Failed to derive Private Node from seed: Seed must be 32 bytes (${seed.length} given)`
);
}*/
// Attempt to derive HD Private Node from the given seed.
const deriveResult = deriveHdPrivateNodeFromSeed(seed);
// If a string is returned, this indicates an error...
if (typeof deriveResult === 'string') {
throw new Error(deriveResult);
}
// Return a new HD Private Node from the given seed.
return new HDPrivateNode(deriveResult);
}
/**
* Generates a random HD Private Node.
*
* @throws {Error} If HD Private Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Private Node.
*/
public static generateRandom(): HDPrivateNode {
// Attempt to derive HD Private Node from randomly generated bytes.
const deriveResult = deriveHdPrivateNodeFromSeed(generateRandomBytes(32));
// If a string is returned, this indicates an error...
if (typeof deriveResult === 'string') {
throw new Error(deriveResult);
}
// Return the new HD Private Node from the random bytes.
return new HDPrivateNode(deriveResult);
}
/**
* Returns the XPriv string for this node.
*
* @returns {string} The XPriv string for this node.
*/
public toString(): string {
// Return the Mainnet XPriv.
return this.toXPriv();
}
/**
* Gets the Private Key entity for this node.
*
* @returns {PrivateKey} The Private Key Entity for this node.
*/
public privateKey(): PrivateKey {
// Return a Private Key Entity from the private key of our node.
return new PrivateKey(this.node.privateKey);
}
/**
* Derives a HD Private Node from the given BIP32 path.
*
* @param path {string} The BIP32 Path (e.g. m/44'/145'/0'/0/1).
*
* @throws {Error} If HD Private Node cannot be derived.
*
* @returns {HDPrivateNode} The derived HD Private Node
*/
public derivePath(path: string): HDPrivateNode {
// Attempt to derive the given path.
const derivePathResult = deriveHdPath(this.node, path);
// If a string is returned, this indicates an error...
if (typeof derivePathResult === 'string') {
throw new Error(derivePathResult);
}
// Return a new HD Private Node Entity derived from the given path.
return new HDPrivateNode(derivePathResult);
}
/**
* Derives a HD Public Node from this Private Node.
*
* @returns {HDPublicNode} The derived HD Public Node
*/
public deriveHDPublicNode(): HDPublicNode {
// Derive a HD Public Node from this HD Private Node.
const publicNode = deriveHdPublicNode(this.node);
// Return a new HD Public Node entity from our derived Public Node.
return new HDPublicNode(publicNode);
}
/**
* Converts this node to an XPriv Key for export.
*
* @param network {HdKeyNetwork} The network to encode for.
*
* @returns {string} The XPriv Key.
*/
public toXPriv(network: HdKeyNetwork = 'mainnet'): string {
// Create our node info structure.
const nodeInfo: DecodedHdKey<HdPrivateNodeValid> = {
network: network,
node: this.node,
};
// Encode the XPriv from our node info.
return encodeHdPrivateKey(nodeInfo).hdPrivateKey;
}
}
+112
View File
@@ -0,0 +1,112 @@
import { PublicKey } from './public-key.js';
import {
encodeHdPublicKey,
decodeHdPublicKey,
deriveHdPath,
} from '@bitauth/libauth';
import type {
DecodedHdKey,
HdKeyNetwork,
HdPublicNodeValid,
} from '@bitauth/libauth';
/**
* Hierarchically Deterministic Public Node Entity.
*/
export class HDPublicNode {
// The HD Public Node.
public readonly node: HdPublicNodeValid;
/**
* Construct a new HD Public Node.
*
* @param node {HdPublicNode} The HD Public Node.
*/
constructor(node: HdPublicNodeValid) {
this.node = node;
}
/**
* Creates a HD Public Node from the given XPriv Key.
*
* @param xpub {string} The XPriv Key.
*
* @throws {Error} If HD Public Node cannot be created.
*
* @returns {HDPrivateNode} The created HD Public Node.
*/
public static fromXPub(xpub: string): HDPublicNode {
// Attempt to decode the XPub Key.
const decodeResult = decodeHdPublicKey(xpub);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new HD Public Node from the given XPub.
return new HDPublicNode(decodeResult.node);
}
/**
* Returns the XPub string for this node.
*
* @returns {string} The XPub string for this node.
*/
public toString(): string {
// Return the Mainnet XPub.
return this.toXPub();
}
/**
* Gets the Public Key Entity for this node.
*
* @returns {PublicKey} The Public Key Entity for this node.
*/
public publicKey(): PublicKey {
// Return a Public Key Entity from the public key of our node.
return new PublicKey(this.node.publicKey);
}
/**
* Derives a HD Public Node from the given BIP32 path.
*
* @param path {string} The BIP32 Path (e.g. m/44'/145'/0'/0/1).
*
* @throws {Error} If HD Public Node cannot be derived.
*
* @returns {HDPrivateNode} The derived HD Public Node
*/
public derivePath(path: string): HDPublicNode {
// Attempt to derive the given path.
const derivePathResult = deriveHdPath(this.node, path);
// If a string is returned, this indicates an error...
if (typeof derivePathResult === 'string') {
throw new Error(derivePathResult);
}
// Return a new HD Public Node Entity derived from the given path.
return new HDPublicNode(derivePathResult);
}
/**
* Converts this node to an XPub Key for export.
*
* @param network {HdKeyNetwork} The network to encode for.
*
* @returns {string} The XPub Key.
*/
public toXPub(network: HdKeyNetwork = 'mainnet'): string {
// Create our node info structure.
const nodeInfo = {
network: network,
node: this.node,
} as DecodedHdKey<HdPublicNodeValid>;
// Encode the XPub from our node info.
return encodeHdPublicKey(nodeInfo).hdPublicKey;
}
}
+10
View File
@@ -0,0 +1,10 @@
export * from './address.js';
export * from './bytes.js';
export * from './crypto.js';
export * from './hd-private-node.js';
export * from './hd-public-node.js';
export * from './private-key.js';
export * from './public-key.js';
export * from './satoshis.js';
export * from './script.js';
export * from './transaction.js';
+310
View File
@@ -0,0 +1,310 @@
import { PublicKey } from './public-key.js';
import {
binToHex,
decodePrivateKeyWif,
encodePrivateKeyWif,
generatePrivateKey,
hexToBin,
isHex,
binToUtf8,
utf8ToBin,
bigIntToCompactUint,
secp256k1,
sha256,
} from '@bitauth/libauth';
import type {
RecoverableSignature,
WalletImportFormatType,
} from '@bitauth/libauth';
/**
* Private Key entity.
*/
export class PrivateKey {
// The raw bytes representing the Private Key.
public readonly bytes: Uint8Array;
/**
* Construct a new Private Key.
*
* @throws {Error} If Private Key cannot be constructed.
*
* @param bytes {Uint8Array} The raw bytes (32) for the Private Key.
*/
constructor(bytes: Uint8Array) {
// Ensure that the Private Key is valid.
if (!secp256k1.validatePrivateKey(bytes)) {
throw new Error(
`Private Key is invalid: Must be >= 0x01 and <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140.`
);
}
this.bytes = bytes;
}
/**
* Create a Private Key Entity from the given bytes (as hexadecimal string).
*
* @param privateKeyHex {string} Raw Private Key bytes encoded as hexadecimal string.
*
* @throws {Error} If Private Key could not be created.
*
* @returns {PrivateKey} The created Private Key Entity.
*/
public static fromHex(privateKeyHex: string): PrivateKey {
// Ensure that the given string is hex.
if (!isHex(privateKeyHex)) {
throw new Error(
`The given string (${privateKeyHex}) is not valid hexadecimal.`
);
}
// Convert the hex to binary (Uint8Array).
const privateKeyBin = hexToBin(privateKeyHex);
// Create a Private Key Entity from the binary.
return new PrivateKey(privateKeyBin);
}
/**
* Create a Private Key from a given WIF (Note that the network information from the WIF will be lost).
*
* @throws {Error} If Private Key cannot be created.
*
* @returns {PrivateKey} The Private Key
*/
public static fromWIF(
wif: string,
removePrefixes = ['bitcoincash:', 'wif:']
): PrivateKey {
// Remove the prefixes from the WIF (if they exist).
const wifWithoutPrefixes = removePrefixes.reduce((result, prefix) => {
return result.startsWith(prefix) ? result.slice(prefix.length) : result;
}, wif);
// Attempt to decode the WIF.
const decodeResult = decodePrivateKeyWif(wifWithoutPrefixes);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new Private Key entity.
return new PrivateKey(decodeResult.privateKey);
}
/**
* Generate a random Private Key.
*
* @returns {PrivateKey} The randomly generated Private Key
*/
public static generateRandom(): PrivateKey {
const bytes = generatePrivateKey();
return new PrivateKey(bytes);
}
/**
* Derive the public key from this private key.
*
* @throws {Error} If Public Key cannot be derived.
*
* @returns {PublicKey} The derived Public Key.
*/
public derivePublicKey(): PublicKey {
// Attempt to derive the public key.
const deriveResult = secp256k1.derivePublicKeyCompressed(this.bytes);
// If a string is returned, this indicates an error...
if (typeof deriveResult === 'string') {
throw new Error(deriveResult);
}
// Return the derived Public Key.
return new PublicKey(deriveResult);
}
/**
* Sign a message hash with this Private Key using a Recoverable Compact Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashRecoverableCompact(
messageHash: Uint8Array
): RecoverableSignature {
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashRecoverableCompact(
this.bytes,
messageHash
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using a Compact Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashCompact(messageHash: Uint8Array): Uint8Array {
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashCompact(
this.bytes,
messageHash
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using a Schnorr Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashSchnorr(messageHash: Uint8Array): Uint8Array {
// Attempt to sign the message.
const signResult = secp256k1.signMessageHashSchnorr(
this.bytes,
messageHash
);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using a DER Signature.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageHashDER(messageHash: Uint8Array): Uint8Array {
const signResult = secp256k1.signMessageHashDER(this.bytes, messageHash);
// If a string is returned, this indicates an error...
if (typeof signResult === 'string') {
throw new Error(signResult);
}
// Return the signature.
return signResult;
}
/**
* Sign a message hash with this Private Key using an Electron Cash Signature.
*
* This applies the Bitcoin and Electron Magic Bytes.
*
* NOTE: Electron can verify this message successfully, but generates them differently internally.
*
* @param messageHash {Uint8Array} The hash of the message.
*
* @throws {Error} If message hash cannot be signed.
*
* @returns {Uint8Array} The generated signature.
*/
public signMessageElectron(message: string): Uint8Array {
// Compile the message.
const magicBytes = utf8ToBin('\x18Bitcoin Signed Message:\n');
const messageBytes = utf8ToBin(message);
const messageLengthBytes = bigIntToCompactUint(BigInt(messageBytes.length)); // hexToBin(messageBytes.length.toString(16)); //
const messageFinal = new Uint8Array([
...magicBytes,
...messageLengthBytes,
...messageBytes,
]);
// Hash the message.
// NOTE: That we hash256 (2 x sha256) the message.
const messageHash = sha256.hash(sha256.hash(messageFinal));
// Use Recoverable Compact Signature.
const signature = this.signMessageHashRecoverableCompact(messageHash);
// Apply Electon encoding.
const electronEncoding = new Uint8Array([
...[31 + signature.recoveryId],
...signature.signature,
]);
// Return the signature.
return electronEncoding;
}
/**
* Convert the raw bytes of this Private Key to hexadecimal encoding.
*
* @returns {string} The raw bytes of the Private Key as a hexadecimal string.
*/
public toHex(): string {
// Return the hexadecimal encoded bytes of this Private Key.
return binToHex(this.bytes);
}
/**
* Convert this Private Key to a raw 32 byte representation.
*
* @returns {Uint8Array} The raw Private Key as a Uint8Array.
*/
public toBytes(): Uint8Array {
// Return the raw bytes for this Private Key.
return this.bytes;
}
/**
* Convert this Private Key to a WIF format.
*
* @param type {WalletImportFormatType} The type of WIF to create.
*
* @returns {string} The Private Key in WIF format.
*/
public toWif(type: WalletImportFormatType = 'mainnet'): string {
// Return the encoded Private Key WIF.
return encodePrivateKeyWif(this.bytes, type);
}
/**
* Returns the Private Key as a mainnet WIF.
*
* @returns {string} The Private Key as a mainnet WIF.
*/
public toString(): string {
// Return the encoded Mainnet Private Key WIF.
return this.toWif();
}
}
+188
View File
@@ -0,0 +1,188 @@
import { Address } from './address.js';
import {
binToHex,
hexToBin,
isHex,
secp256k1,
sha256,
ripemd160,
} from '@bitauth/libauth';
/**
* Public Key Entity.
*/
export class PublicKey {
// The raw bytes representing the Public Key.
public readonly bytes: Uint8Array;
/**
* Construct a new Public Key.
*
* @param bytes {Uint8Array} The raw bytes (33) for the Public Key.
*/
constructor(bytes: Uint8Array) {
// Ensure that exactly 33 Bytes are given.
if (!secp256k1.validatePublicKey(bytes)) {
throw new Error(`Invalid Public Key.`);
}
this.bytes = bytes;
}
/**
* Create a Public Key entity from the given hexadecimal string.
*
* @param publicKeyHex {string} Hexadecimal encoding of the Public Key bytes.
*
* @throws {Error} If Public Key cannot be created.
*
* @returns {PublicKey} 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 PublicKey(publicKey);
}
/**
* Derive the Hash160 for this Public Key.
*
* @returns {Uint8Array} The Hash160 of the Public Key.
*/
public deriveHash160(): Uint8Array {
// SHA256 Hash the public key.
const sha256Hash = sha256.hash(this.bytes);
// RIPEMD160 the SHA256 hash.
const ripemd160Hash = ripemd160.hash(sha256Hash);
// And then return it.
return ripemd160Hash;
}
/**
* Derive the Hash160 as Hex for this Public Key.
*
* @returns {string} The Hash160 of the Public Key as hex.
*/
public deriveHash160Hex(): string {
const hash160Bytes = this.deriveHash160();
return binToHex(hash160Bytes);
}
/**
* Derive the Address for this Public Key.
*
* @returns {Address} The Address for this Public Key.
*/
public deriveAddress(): Address {
// Get the RIPEMD160 Hash of the Public Key.
const ripemd160Hash = this.deriveHash160();
// Return an instance of address
return Address.fromHash160(ripemd160Hash, 'P2PKH');
}
/**
* Verify that a compact message hash was signed with the corresponding Private Key.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param signature {Uint8Array) The signature of the message.
*
* @returns {boolean} True if signature and message hash verifies, false otherwise..
*/
public verifyMessageHashCompact(
messageHash: Uint8Array,
signature: Uint8Array
): boolean {
const validity = secp256k1.verifySignatureCompact(
signature,
this.bytes,
messageHash
);
return validity;
}
/**
* Verify that a DER message hash was signed with the corresponding Private Key.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param signature {Uint8Array) The signature of the message.
*
* @returns {boolean} True if signature and message hash verifies, false otherwise..
*/
public verifyMessageHashDER(
messageHash: Uint8Array,
signature: Uint8Array
): boolean {
const validity = secp256k1.verifySignatureDER(
signature,
this.bytes,
messageHash
);
return validity;
}
/**
* Verify that a Schnorr message hash was signed with the corresponding Private Key.
*
* @param messageHash {Uint8Array} The hash of the message.
* @param signature {Uint8Array) The signature of the message.
*
* @returns {boolean} True if signature and message hash verifies, false otherwise..
*/
public verifyMessageHashSchnorr(
messageHash: Uint8Array,
signature: Uint8Array
): boolean {
const validity = secp256k1.verifySignatureSchnorr(
signature,
this.bytes,
messageHash
);
return validity;
}
/**
* Encode the raw bytes of this Public Key to a hexadecimal representation.
*
* @returns {string} The raw bytes Public Key encoded as Hexadecimal.
*/
public toHex(): string {
return binToHex(this.bytes);
}
/**
* Get the raw bytes of this Public Key.
*
* @returns {Uint8Array} The raw bytes of the Public Key.
*/
public toBytes(): Uint8Array {
// Return the raw bytes for this Public Key.
return this.bytes;
}
/**
* Return the hexadecimal representation of the raw bytes of the Public Key.
*
* @returns {string} The raw bytes of the Public Key as hexadecimal.
*/
public toString(): string {
// Return the raw Public Key byes encoded as a hexadecimal string.
return this.toHex();
}
}
+166
View File
@@ -0,0 +1,166 @@
/**
* Satoshis Primitive
*/
export class Satoshis {
public readonly satoshis: bigint;
/**
* Construct a new instance of Satoshis.
*
* This is the same as using fromSatoshis().
*
* @param satoshis {number} The satoshi amount.
*/
constructor(satoshis: bigint | number) {
// Set the satoshi amount.
this.satoshis = BigInt(satoshis);
}
/**
* Instantiates from a Satoshi Amount.
*
* @param satoshis {number} The Satoshi amount.
*
* @returns {Satoshis} New instance of Satoshis.
*/
public static fromSats(satoshis: bigint | number): Satoshis {
return new Satoshis(satoshis);
}
/**
* Instantiates from a Satoshi Amount.
*
* @param satoshis {number} The Satoshi amount.
*
* @deprecated Use fromSats() instead.
*
* @returns {Satoshis} New instance of Satoshis.
*/
public static fromSatoshis(satoshis: bigint | number): Satoshis {
return new Satoshis(satoshis);
}
/**
* Instantiates from a Bits Amount (100 satoshis = 1 bit).
*
* @param bits {number} The Bits amount.
*
* @returns {Satoshis} New instance of Satoshis.
*/
public static fromBits(bits: number): Satoshis {
const satoshis = Satoshis.roundToDigits(bits * 100, 0);
return new Satoshis(satoshis);
}
/**
* Instantiates from a mBCH Amount (100,000 satoshis = 1 mBCH).
*
* @param mBCH {number} The mBCH amount.
*
* @returns {Satoshis} New instance of Satoshis.
*/
public static fromMBCH(mBCH: number): Satoshis {
const satoshis = Satoshis.roundToDigits(mBCH * 100_000, 0);
return new Satoshis(satoshis);
}
/**
* Instantiates from a BCH Amount (100,000,000 satoshis = 1 BCH).
*
* @param bch {number} The BCH amount.
*
* @returns {Satoshis} New instance of Satoshis.
*/
public static fromBCH(bch: number): Satoshis {
const satoshis = Satoshis.roundToDigits(bch * 100_000_000, 0);
return new Satoshis(satoshis);
}
/**
* Rounds a number to the given number of digits.
*
* @param numberToRound {number} The number to round.
* @param digits {number} The number of digits to round to.
*
* @returns {Number} The rounded number.
*/
public static roundToDigits(numberToRound: number, digits: number): number {
// Set the options of the Number Format object.
const options: Intl.NumberFormatOptions = {
style: 'decimal',
minimumFractionDigits: digits,
maximumFractionDigits: digits,
useGrouping: false,
numberingSystem: 'latn',
};
// Create an instance of number format using above options.
// NOTE: We force the locale to en-GB so that the number is formatted correctly (e.g. with a decimal, not a comma).
const numberFormat = new Intl.NumberFormat('en-GB', options);
// Format the number.
const formattedAmount = numberFormat.format(numberToRound);
// Return the formatted number.
return Number(formattedAmount);
}
/**
* Converts the amount to Satoshis (as a BigInt).
*
* @returns {bigint} The satoshi amount.
*/
public toBigInt(): bigint {
return this.satoshis;
}
/**
* Converts the amount to Satoshis.
*
* @returns {number} The satoshi amount.
*/
public toSats(): number {
return Number(this.satoshis);
}
/**
* Converts the amount to Satoshis.
*
* @deprecated Use toSats() instead.
*
* @returns {number} The satoshi amount.
*/
public toSatoshis(): number {
return Number(this.satoshis);
}
/**
* Converts the amount to Bits (100 satoshis = 1 bit).
*
* @returns {number} The bit amount.
*/
public toBits(): number {
return Satoshis.roundToDigits(Number(this.satoshis) / 100, 2);
}
/**
* Converts the amount to mBCH (100,000 satoshis = 1 mBCH).
*
* @returns {number} The mBCH amount.
*/
public toMBCH(): number {
return Satoshis.roundToDigits(Number(this.satoshis) / 100_000, 5);
}
/**
* Converts the amount to BCH (100,000,000 satoshis = 1 BCH).
*
* @returns {number} The BCH amount.
*/
public toBCH(): number {
return Satoshis.roundToDigits(Number(this.satoshis) / 100_000_000, 8);
}
}
+145
View File
@@ -0,0 +1,145 @@
import {
assembleBytecodeBCH,
disassembleBytecodeBCH,
base58AddressToLockingBytecode,
cashAddressToLockingBytecode,
binToHex,
hexToBin,
isHex,
} from '@bitauth/libauth';
/**
* Script Entity.
*/
export class Script {
public readonly bytes: Uint8Array;
/**
* Construct a new Script
*
* @param bytes {Uint8Array} The raw bytes of the Script.
*/
constructor(bytes: Uint8Array) {
this.bytes = bytes;
}
/**
* Create a Script Entity from the given Assembly String.
*
* @param asm {string} String of assembly code.
*
* @throws {Error} If script cannot be decoded.
*
* @returns {Script} The created Script Entity.
*/
public static fromASM(asm: string): Script {
// Attempt to convert the given ASM to bytecode.
const assembleResult = assembleBytecodeBCH(asm);
// If a string is returned, this indicates an error...
if (typeof assembleResult === 'string') {
throw new Error(assembleResult);
}
// If the success flag is not marked, this indicates an error...
if (!assembleResult.success) {
throw new Error('Could not assemble bytecode from assembly.');
}
// Return new Script Entity from the assembled bytecode.
return new Script(assembleResult.bytecode);
}
/**
* Create a Script Entity from the given bytes (as hexadecimal string).
*
* @param hex {string} Raw script bytes encoded as hexadecimal.
*
* @throws {Error} If script cannot be decoded.
*
* @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 Script(scriptBin);
}
/**
* Create a Script Entity from the given CashAddr.
*
* @param cashAddr {string} The Address in CashAddr format.
*
* @throws {Error} If address cannot be decoded.
*
* @returns {Script} The created Script Entity.
*/
public static fromCashAddr(cashAddr: string): Script {
const decodeResult = cashAddressToLockingBytecode(cashAddr);
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
return new Script(decodeResult.bytecode);
}
/**
* Create a Script Entity from the given Legacy Address.
*
* @param legacyAddress {string} The Address in Legacy format.
*
* @throws {Error} If address cannot be decoded.
*
* @returns {Script} The created Script Entity.
*/
public static fromLegacyAddress(legacyAddress: string): Script {
const decodeResult = base58AddressToLockingBytecode(legacyAddress);
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
return new Script(decodeResult.bytecode);
}
/**
* Convert this script to a human-readable string of assembly.
*
* @returns {string} Human-readable assembly.
*/
public toASM(joinWith = '\n'): string {
const disassembledBytecode = disassembleBytecodeBCH(this.bytes);
if (joinWith) {
return disassembledBytecode.split(' ').join(joinWith);
}
return disassembledBytecode;
}
/**
* Get the raw bytecode for this script.
*
* @returns {Uint8Array} The compiled bytes.
*/
public toBytes(): Uint8Array {
return this.bytes;
}
/**
* Get the raw bytecode for this script as a hex string.
*
* @returns {string} The compiled bytes as a hex string.
*/
public toHex(): string {
return binToHex(this.bytes);
}
}
+87
View File
@@ -0,0 +1,87 @@
import {
binToHex,
hexToBin,
encodeTransaction,
decodeTransaction,
isHex,
} from '@bitauth/libauth';
import type { TransactionBCH } from '@bitauth/libauth';
/**
* Transaction Entity.
*/
export class Transaction {
constructor(public transaction: TransactionBCH) {}
/**
* Create a Transaction Entity from the given byte data.
*
* @param bytes {Uint8Array} The raw byte data of the transaction.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns {Transaction} The created Transaction Entity.
*/
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 Entity created from the decode result.
return new Transaction(decodeResult);
}
/**
* Create a Transaction Entity from the given byte data encoded as hexadecimal string.
*
* @param hex {string} The raw byte data (encoded as hexadecimal string) of the transaction.
*
* @throws {Error} If transaction cannot be decoded.
*
* @returns {Transaction} The created Transaction Entity.
*/
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 Transaction.fromBytes(bytes);
}
public toBytes(): Uint8Array {
const bytes = encodeTransaction(this.transaction);
return bytes;
}
public toHex(): string {
const bytes = this.toBytes();
return binToHex(bytes);
}
public getVersion() {
return this.transaction.version;
}
public getInputs() {
return this.transaction.inputs;
}
public getOutputs() {
return this.transaction.outputs;
}
public getLocktime() {
return this.transaction.locktime;
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
+3
View File
@@ -0,0 +1,3 @@
# XO Wallet
TODO
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@xo/vue",
"version": "0.0.1",
"description": "XO Stack Wallet - Vue",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"build-old": "rollup -c",
"prepublishOnly": "npm run build"
},
"author": "",
"license": "ISC",
"dependencies": {
"@xo/primitives": "^0.0.1",
"@xo/wallet": "^0.0.1",
"vue": "^3.4.38"
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './wallet-hd-vue.js';
export * from './wallet-p2pkh-vue.js';
+38
View File
@@ -0,0 +1,38 @@
import { Blockchain, WalletHD } from '@xo/wallet';
import { WalletP2PKHVue, useWalletP2PKHVue } from './wallet-p2pkh-vue.js';
import { computed, ref, shallowRef } from 'vue';
import { HdPrivateNodeValid } from '@bitauth/libauth';
export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
// Reactive State.
public rShouldMonitor = ref(false);
public rWallets = shallowRef<Array<WalletP2PKHVue>>([]);
// Computeds.
public rBalance = computed(() => {
return this.rWallets.value.reduce(
(total, node) => total + node.rBalance.value,
0
);
});
constructor(
node: HdPrivateNodeValid,
electrum: Blockchain,
walletFactory = useWalletP2PKHVue
) {
super(node, electrum, walletFactory);
this.events.on(
'walletsUpdated',
(wallets) => (this.rWallets.value = wallets)
);
this.events.on(
'shouldMonitorUpdated',
(shouldMonitor) => (this.rShouldMonitor.value = shouldMonitor)
);
}
}
+47
View File
@@ -0,0 +1,47 @@
import { Transaction } from '@xo/primitives';
import { type AddressListUnspent, Blockchain, WalletP2PKH } from '@xo/wallet';
import { computed, shallowRef } from 'vue';
/**
* @example
* ```ts
* const walletP2PKH = WalletP2PKHVue.fromMnemonic('12 words');
* ```
*
* Reactive variables are prefixed with `r`.
* ```vue
* <div>{{ walletP2PKH.rBalance.value }}</div>
* ```
*/
export class WalletP2PKHVue extends WalletP2PKH {
// Reactive State.
public rUnspents = shallowRef<AddressListUnspent['response']>([]);
public rTransactions = shallowRef<Array<Transaction>>([]);
// Computeds.
public rBalance = computed(() => {
return this.rUnspents.value.reduce(
(total, unspent) => total + unspent.value,
0
);
});
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
super(privateKeyBytes, electrum);
this.events.on('unspentsUpdated', (unspents) => {
this.rUnspents.value = unspents;
});
this.events.on('transactionsUpdated', (transactions) => {
this.rTransactions.value = transactions;
});
}
}
export function useWalletP2PKHVue(
privateKeyBytes: Uint8Array,
electrum: Blockchain
): WalletP2PKHVue {
return new WalletP2PKHVue(privateKeyBytes, electrum);
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
+3
View File
@@ -0,0 +1,3 @@
# XO Wallet
TODO
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@xo/wallet",
"version": "0.0.1",
"description": "XO Stack Wallet",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"build-old": "rollup -c",
"prepublishOnly": "npm run build"
},
"author": "",
"license": "ISC",
"dependencies": {
"@electrum-cash/network": "^3.3.0",
"@xo/primitives": "^0.0.1"
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './utils/index.js';
export * from './wallet/index.js';
@@ -0,0 +1,61 @@
export class EventEmitter<
T extends Record<string, unknown>
> extends EventTarget {
private listeners: Map<keyof T, Set<EventListener>> = new Map();
constructor() {
super();
}
on<K extends keyof T>(type: K, listener: (detail: T[K]) => void) {
const wrappedListener = ((e: CustomEvent<T[K]>) =>
listener(e.detail)) as EventListener;
this.addEventListener(type as string, wrappedListener);
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)?.add(wrappedListener);
return wrappedListener;
}
off<K extends keyof T>(type: K, listener: EventListener) {
this.removeEventListener(type as string, listener);
this.listeners.get(type)?.delete(listener);
}
emit<K extends keyof T>(type: K, detail: T[K]) {
const event = new CustomEvent(type as string, { detail });
return this.dispatchEvent(event);
}
removeAllListeners() {
this.listeners.forEach((listeners, type) => {
listeners.forEach((listener) => {
this.removeEventListener(type as string, listener);
});
});
this.listeners.clear();
}
monitorProperty<ObjectType, Property extends keyof ObjectType>(
target: ObjectType,
key: Property,
eventName: keyof T
) {
let value = target[key];
Object.defineProperty(target, key, {
get: () => {
return value;
},
set: (newValue) => {
value = newValue;
this.emit(eventName as string, newValue);
},
enumerable: true,
configurable: true,
});
}
}
+1
View File
@@ -0,0 +1 @@
export * from './event-emitter.js';
@@ -0,0 +1,131 @@
// TODO: I think @monsterbitar (Jonathan) is working on something like this too.
// So I haven't bothered making this complete. We'll just use his once it's done.
// Or, if he's busy with other stuff, we can complete this and create an MR into Electrum-Cash.
import type { RequestResponse } from '@electrum-cash/network';
// TODO: This should be imported from electrum-cash library.
// But it is not exported yet.
export type RPCParameter = string | number | boolean | null;
export interface ElectrumRequest {
method: string;
params: Array<RPCParameter>;
response: RequestResponse;
}
export interface AddressGetBalance extends ElectrumRequest {
method: 'blockchain.address.get_balance';
params: [string];
response: {
confirmed: number;
unconfirmed: number;
};
}
export interface AddressGetHistory extends ElectrumRequest {
method: 'blockchain.address.get_history';
// TODO: RPCParameter accepts a null type, but this should probably be changed to undefined instead.
params: [string, number | null, number | null];
response: Array<{
height: number;
tx_hash: string;
fee?: number;
}>;
}
export interface AddressGetMempool extends ElectrumRequest {
method: 'blockchain.address.get_mempool';
params: [string];
response: Array<{
height: number;
tx_hash: string;
fee?: number;
}>;
}
export interface AddressGetScriptHash extends ElectrumRequest {
method: 'blockchain.address.get_scripthash';
params: [string];
response: string;
}
export interface AddressListUnspent extends ElectrumRequest {
method: 'blockchain.address.listunspent';
params: [string, 'include_tokens' | 'exclude_tokens' | 'tokens_only'];
response: Array<{
height: number;
token_data?: {
amount: string;
category: string;
nft?: {
capability: 'none' | 'mutable' | 'minting';
commitment: string;
};
};
tx_hash: string;
tx_pos: number;
value: number;
}>;
}
export interface AddressSubscribe extends ElectrumRequest {
method: 'blockchain.address.subscribe';
params: [string];
// [Address, Status]
response: [string, string | null];
}
export interface AddressUnsubscribe extends ElectrumRequest {
method: 'blockchain.address.unsubscribe';
params: [string];
response: boolean;
}
export interface ScriptHashGetBalance extends ElectrumRequest {
method: 'blockchain.scripthash.get_balance';
params: [string];
response: {
confirmed: number;
unconfirmed: number;
};
}
export interface TransactionBroadcast extends ElectrumRequest {
method: 'blockchain.transaction.broadcast';
params: [string];
response: string;
}
export interface TransactionGet extends ElectrumRequest {
method: 'blockchain.transaction.get';
params: [string, boolean];
response: string;
}
export interface TransactionGetVerbose extends ElectrumRequest {
method: 'blockchain.transaction.get';
params: [string, true];
response: {
// TODO
};
}
export interface TransactionSubscribe extends ElectrumRequest {
method: 'blockchain.transaction.subscribe';
params: [string];
response: number | null;
}
export interface TransactionUnsubscribe extends ElectrumRequest {
method: 'blockchain.transaction.unsubscribe';
params: [string];
response: boolean;
}
export interface AddressNotification {
jsonrpc: '2.0';
method: 'blockchain.address.subscribe';
// [Address, Status]
params: [string, string | null];
}
+122
View File
@@ -0,0 +1,122 @@
import { AddressNotification, ElectrumRequest } from './blockchain-types.js';
import { ElectrumClient } from '@electrum-cash/network';
export type AddressCallback = (status: string | null) => void;
// TODO: Rename this to Blockcain Service and refactor such that it can store local state.
// For local state, we'll want to track a tree of dependencies locally.
// Might be able to copy whatever structure a proper node uses (DAG?).
export class Blockchain {
electrumClient!: ElectrumClient;
// Address subscriptions.
addressSubscriptions: { [address: string]: AddressCallback } = {};
constructor(
public readonly servers: string[],
public readonly application = 'BCHApp',
public readonly version = '1.5'
) {}
async start(): Promise<void> {
// TODO: We want to use a Cluster instead of a Client, but as of 2022-07-07 there are issues with Subscriptions on Cluster.
// See: https://gitlab.com/GeneralProtocols/anyhedge/whitelabel/frontend/-/issues/76
// Iterate over each server in our list of servers.
// NOTE: We are still using an Electrum Client due to the above bug.
// But, if we cannot connect to the first server, we will try the second (and third if given, etc) as a fallback.
for (const electrumServer of this.servers) {
// Initialize an Electrum client using the given server.
const electrum = new ElectrumClient(
this.application,
this.version,
electrumServer,
50004,
'wss'
);
// Attempt to connect to this Electrum Server.
try {
await electrum.connect();
} catch (error) {
console.warn(error);
// If it fails to connect, try the next server in our list.
continue;
}
// Save the client.
this.electrumClient = electrum;
// Setup notification handler.
this.electrumClient.on('notification', this.onNotification.bind(this));
// Return to prevent further execution.
return;
}
// Throw an error if we failed to connect to any of our Electrum Servers.
throw new Error(
'Failed to connect to Electrum. Please try restarting your browser and ensure you are currently connected to the internet.'
);
}
async request<T extends ElectrumRequest>(
endpoint: T['method'],
...params: T['params']
): Promise<T['response']> {
const response = await this.electrumClient.request(endpoint, ...params);
if (response instanceof Error) {
throw response;
}
return response;
}
async subscribeAddress(address: string, callback: AddressCallback) {
await this.electrumClient.subscribe(
'blockchain.address.subscribe',
address
);
this.addressSubscriptions[address] = callback;
}
async unsubscribeAddress(address: string, _callback: AddressCallback) {
if (!this.addressSubscriptions[address]) {
return;
}
delete this.addressSubscriptions[address];
try {
await this.electrumClient.unsubscribe(
'blockchain.address.unsubscribe',
address
);
} catch (error) {
console.warn(`${error}`);
}
}
onNotification(data: AddressNotification) {
// Handle Address notification.
if (data.method === 'blockchain.address.subscribe') {
const address = data.params[0];
const status = data.params[1];
const subscription = this.addressSubscriptions[address];
if (!subscription) {
console.warn(
`Notification for address ${address} subscribed to, but has no handler`
);
return;
}
// Trigger the subscription's callback.
subscription(status);
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Blockchain } from './blockchain.js';
export type ServiceConfig<BlockchainType, WalletType> = {
createBlockchain: () => BlockchainType;
createWallet: (
privateKeyBytes: Uint8Array,
electrum: Blockchain
) => WalletType;
};
+5
View File
@@ -0,0 +1,5 @@
export * from './blockchain.js';
export * from './blockchain-types.js';
export * from './storage.js';
export * from './wallet-hd.js';
export * from './wallet-p2pkh.js';
+14
View File
@@ -0,0 +1,14 @@
/**
* TODO
*
* @remarks
* Storage should support Adapters.
* We don't want to extend this - we want to compose it.
* Reason being that we want to support multiple storage adapters.
* And we should allow the possibility of having non-critical storage adapters fail (e.g. remote vs local).
*/
export class Storage {
constructor() {
console.log('TODO');
}
}
+52
View File
@@ -0,0 +1,52 @@
import { type AddressListUnspent } from './blockchain-types.js';
import { Blockchain } from './blockchain.js';
import { PrivateKey, Transaction } from '@xo/primitives';
import {
type CompilerBCH,
type CompilationData,
type InputTemplate,
} from '@bitauth/libauth';
export type InputTemplateBCH = InputTemplate<
CompilerBCH,
false,
CompilationData
>;
export abstract class WalletBase extends PrivateKey {
// Dependencies/Services
public electrum: Blockchain;
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
super(privateKeyBytes);
this.electrum = electrum;
}
abstract startMonitoring(): Promise<void>;
abstract stopMonitoring(): Promise<void>;
abstract getAddress(): string;
abstract fetchHistory(): Promise<Array<Transaction>>;
abstract fetchUnspentOutputs(): Promise<AddressListUnspent['response']>;
abstract getUnspentDirectives(): Promise<Array<InputTemplateBCH>>;
async refresh(): Promise<void> {
await Promise.all([this.fetchUnspentOutputs(), this.fetchHistory()]);
}
async onAddressNotification(status: string | null) {
// If status is null, it simply means that our subscribe call to Electrum was successful.
if (!status) {
return;
}
// Refresh our wallet's state.
await this.refresh();
}
}
+251
View File
@@ -0,0 +1,251 @@
// Import the Stamp class to derive the private keys
import { HDPrivateNode } from '@xo/primitives';
import { EventEmitter } from '../utils/index.js';
import { Blockchain } from './blockchain.js';
import {
WalletP2PKH,
WalletP2PKHFactory,
useWalletP2PKH,
} from './wallet-p2pkh.js';
import {
type HdPrivateNodeValid,
deriveSeedFromBip39Mnemonic,
encodeTransaction,
generateTransaction,
getMinimumFee,
} from '@bitauth/libauth';
export const DERIVATION_PATH = "m/44'/145'/0'";
export const ADDRESS_GAP = 20;
export type WalletHDEvents<WalletType> = {
shouldMonitorUpdated: boolean;
walletsUpdated: Array<WalletType>;
};
export class WalletHD<
WalletType extends WalletP2PKH = WalletP2PKH
> extends HDPrivateNode {
// Events.
public events: EventEmitter<WalletHDEvents<WalletType>> = new EventEmitter();
// Mutable State.
public shouldMonitor = false;
public wallets: Array<WalletType> = [];
constructor(
public node: HdPrivateNodeValid,
public electrum: Blockchain,
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH
) {
// Instantiate parent class.
super(node);
// Monitor the following properties and emit an event when they change.
this.events.monitorProperty(this, 'shouldMonitor', 'shouldMonitorUpdated');
this.events.monitorProperty(this, 'wallets', 'walletsUpdated');
}
static fromMnemonic<T extends WalletHD<any>>(
this: WalletHDConstructor<T>,
mnemonic: string,
electrum: Blockchain,
walletFactory?: WalletP2PKHFactory<any>
): T {
// Derive the seed from the mnemonic.
const seed = deriveSeedFromBip39Mnemonic(mnemonic);
// Derive node from mnemonic.
const hdPrivateNode = HDPrivateNode.fromSeed(seed);
// Return a new instance of this class.
return new this(hdPrivateNode.node, electrum, walletFactory);
}
async destroy() {
this.events.removeAllListeners();
}
async setQuantity(quantity: number) {
// Clear existing nodes.
this.wallets = this.deriveWallets(quantity);
}
async startMonitoring() {
this.shouldMonitor = true;
await Promise.all(this.wallets.map((wallet) => wallet.startMonitoring()));
}
async stopMonitoring() {
this.shouldMonitor = false;
await Promise.all(this.wallets.map((wallet) => wallet.stopMonitoring()));
}
deriveWallets(count: number, startIndex = 0) {
// Create an array to store our wallets.
const wallets: Array<WalletType> = [];
// Create the given count of wallets.
for (let i = 0; i < count; i++) {
const childNode = this.derivePath(
`${DERIVATION_PATH}/0/${startIndex + i}`
);
const wallet = this.walletFactory(
childNode.privateKey().toBytes(),
this.electrum
);
if (this.shouldMonitor) {
wallet.startMonitoring();
}
wallets.push(wallet);
}
this.wallets = wallets;
// Return the wallets.
return wallets;
}
async scan(startIndex = 0, addressGap = 20) {
// Array to store active wallet nodes
const nodes: Array<WalletType> = [];
let currentIndex = startIndex;
let emptyAddressCount = 0;
// Continue scanning until we find 'addressGap' consecutive empty addresses
while (emptyAddressCount < addressGap) {
// Derive a number of wallets equivalent to our addressGap.
const batch: Array<WalletType> = this.deriveWallets(
addressGap,
currentIndex
);
// Get the transaction history of each wallet in the batch
const results = await Promise.all(
batch.map((wallet) => wallet.fetchHistory())
);
// Process the results
for (let i = 0; i < results.length; i++) {
if (results[i].length > 0) {
// This address has transactions, add it to nodes and reset empty address count
nodes.push(batch[i]);
emptyAddressCount = 0;
} else {
// This address is empty, increment the empty address count
emptyAddressCount++;
}
currentIndex++;
// If we've found 'addressGap' consecutive empty addresses, stop scanning
if (emptyAddressCount >= addressGap) {
break;
}
}
}
this.wallets = nodes;
return nodes;
}
async refreshChildNodes() {
await Promise.all(this.wallets.map((node) => node.refresh()));
}
async sweep(payoutBytecode: Uint8Array) {
// Get a list of inputs belonging to each wallet.
const inputs = await Promise.all(
this.wallets.map((wallet) => wallet.getUnspentDirectives())
);
// Flatten the inputs.
const inputsFlattened = inputs.flat();
// Calculate the total sats available in our inputs.
const totalSats = inputsFlattened.reduce((total, input) => {
if (input.unlockingBytecode instanceof Uint8Array) {
return (total += 0n);
}
return (total += input.unlockingBytecode.valueSatoshis);
}, 0n);
/*
// TODO: Determine the amounts and categories of any tokens in the inputs.
const tokens = inputsFlattened.reduce((tokens, input) => {
// Improve legibility by getting the token.
const token = input.unlockingBytecode.token;
if(!token) {
return tokens;
}
tokens[token.
return tokens;
}, {});
*/
// TODO: Add an output for each token.
// We need to calculate the number of bytes so that we can calculate the fee.
// So we loop twice and store the final transaction here each time.
// 1st time will have zero fee. 2nd time will accommodate the fee.
let encodedTransaction = new Uint8Array();
// Create the transaction by looping twice.
// 1st loop: Transaction without a fee.
// 2nd loop: Accommodate the fee.
for (let i = 0; i < 2; i++) {
// Get the fee using 1000 sats/KB.
const feeSats = getMinimumFee(BigInt(encodedTransaction.length), 1000n);
// Attempt to generate the transaction.
const generatedTransaction = generateTransaction({
version: 2,
locktime: 0,
inputs: inputsFlattened,
outputs: [
{
lockingBytecode: payoutBytecode,
valueSatoshis: totalSats - feeSats,
},
],
});
if (!generatedTransaction.success) {
console.error(generatedTransaction.errors);
throw new Error('Failed to generate transaction');
}
// Encode the transaction for broadcasting (and fee estimation).
encodedTransaction = encodeTransaction(generatedTransaction.transaction);
}
return encodedTransaction;
}
}
//-----------------------------------------------------------------------------
// Factories Types/Functions
//-----------------------------------------------------------------------------
export type WalletHDParams = ConstructorParameters<typeof WalletHD>;
export type WalletHDConstructor<T> = new (...args: WalletHDParams) => T;
export type WalletHDFactory<T extends WalletHD = WalletHD> = (
...args: WalletHDParams
) => T;
export function useWalletHD<T extends WalletHD = WalletHD>(
...args: WalletHDParams
): T {
return new WalletHD(...args) as T;
}
+183
View File
@@ -0,0 +1,183 @@
import { Transaction } from '@xo/primitives';
import { EventEmitter } from '../utils/index.js';
import {
type AddressListUnspent,
type AddressGetHistory,
type TransactionGet,
} from './blockchain-types.js';
import { Blockchain } from './blockchain.js';
import { WalletBase } from './wallet-base.js';
import {
hexToBin,
walletTemplateP2pkhNonHd,
walletTemplateToCompilerBCH,
} from '@bitauth/libauth';
export type WalletP2PKHEvents = {
transactionsUpdated: Array<Transaction>;
unspentsUpdated: AddressListUnspent['response'];
};
/**
* @example
* ```ts
* // Instantiate P2PKH Wallet.
* const walletP2PKH = WalletP2PKH.fromWIF('someWif');
*
* // Start monitoring the balance.
* await walletP2PKH.startMonitoring();
* ```
*/
export class WalletP2PKH extends WalletBase {
// Events.
public events: EventEmitter<WalletP2PKHEvents> = new EventEmitter();
// State.
public transactions: Array<Transaction> = [];
public unspents: AddressListUnspent['response'] = [];
constructor(privateKeyBytes: Uint8Array, electrum: Blockchain) {
super(privateKeyBytes, electrum);
// Monitor the following properties and emit an event when they change.
this.events.monitorProperty(this, 'transactions', 'transactionsUpdated');
this.events.monitorProperty(this, 'unspents', 'unspentsUpdated');
}
async destroy() {
this.events.removeAllListeners();
}
async startMonitoring() {
await this.electrum.subscribeAddress(
this.getAddress(),
this.onAddressNotification.bind(this)
);
}
async stopMonitoring() {
await this.electrum.unsubscribeAddress(
this.getAddress(),
this.onAddressNotification.bind(this)
);
}
getAddress() {
return this.derivePublicKey().deriveAddress().toCashAddr();
}
async fetchHistory() {
// Get this node's address.
const address = this.getAddress();
// Get the history of the address from electrum
const history = await this.electrum.request<AddressGetHistory>(
'blockchain.address.get_history',
address,
0,
-1
);
// Get the transactions for each history item.
const transactions = await Promise.all(
history.map((item) =>
this.electrum.request<TransactionGet>(
'blockchain.transaction.get',
item.tx_hash,
false
)
)
);
// Decode the transactions.
const decodedTransactions = transactions.map((tx) =>
Transaction.fromHex(tx)
);
// Store the transactions.
this.transactions = decodedTransactions;
return decodedTransactions;
}
async fetchUnspentOutputs() {
const address = this.getAddress();
const unspentOutputs = await this.electrum.request<AddressListUnspent>(
'blockchain.address.listunspent',
address,
'include_tokens'
);
// Emit event with unspent outputs.
this.unspents = unspentOutputs;
return unspentOutputs;
}
async getUnspentDirectives() {
// Get a list of unspent outputs.
const unspentOutputs = await this.fetchUnspentOutputs();
// Create our P2PKH Compiler.
const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd);
// Transform our inputs into unlocking directives.
const inputDirectives = unspentOutputs.map((unspent) => ({
outpointIndex: unspent.tx_pos,
outpointTransactionHash: hexToBin(unspent.tx_hash),
sequenceNumber: 0,
unlockingBytecode: {
compiler: compilerP2PKH,
data: {
keys: { privateKeys: { key: this.toBytes() } },
},
script: 'unlock',
token: unspent.token_data
? {
amount: BigInt(unspent.token_data.amount),
category: hexToBin(unspent.token_data.category),
nft: unspent.token_data.nft
? {
capability: unspent.token_data.nft.capability,
commitment: hexToBin(unspent.token_data.nft.commitment),
}
: undefined,
}
: undefined,
valueSatoshis: BigInt(unspent.value),
},
}));
// Return the directives.
return inputDirectives;
}
async refresh(): Promise<void> {
await Promise.all([this.fetchUnspentOutputs(), this.fetchHistory()]);
}
async onAddressNotification(status: string | null) {
// If status is null, it simply means that our subscribe call to Electrum was successful.
if (!status) {
return;
}
// Refresh our wallet's state.
await this.refresh();
}
}
export type WalletP2PKHFactory<T extends WalletP2PKH = WalletP2PKH> = (
privateKeyBytes: Uint8Array,
electrum: Blockchain
) => T;
export function useWalletP2PKH<T extends WalletP2PKH = WalletP2PKH>(
privateKeyBytes: Uint8Array,
electrum: Blockchain
): T {
return new WalletP2PKH(privateKeyBytes, electrum) as T;
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}