@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Blockchain } from './blockchain.js';
|
||||
|
||||
export type ServiceConfig<BlockchainType, WalletType> = {
|
||||
createBlockchain: () => BlockchainType;
|
||||
createWallet: (
|
||||
privateKeyBytes: Uint8Array,
|
||||
electrum: Blockchain
|
||||
) => WalletType;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user