@node-dlc/chainmon
Version:
Bitcoin on-chain transaction monitoring tool for DLCs
187 lines (163 loc) • 5.33 kB
text/typescript
import { OutPoint, Script, Tx, Value } from '@node-dlc/bitcoin';
import { BitcoindClient, BlockSummary } from '@node-dlc/bitcoind';
import { Block } from 'bitcoinjs-lib';
import { EventEmitter } from 'events';
export class BlockWatcher extends EventEmitter {
/**
* Outpoints that are being watched
*/
public watchedOutPoints: Map<string, OutPoint>;
/**
* ScriptPubKeys that are being watched
*/
public watchedScriptPubKeys: Map<string, [Script, Value]>;
public receivedBlocks: Map<string, BlockSummary>;
public previousHashToHash: Map<string, string>;
private _client: BitcoindClient;
/**
* TxWatcher listens for transactions that match certain patterns
* and events when a transaction is found matching the pattern
*
* @param client
*/
constructor(client: BitcoindClient) {
super();
this._client = client;
this.watchedOutPoints = new Map<string, OutPoint>();
this.watchedScriptPubKeys = new Map<string, [Script, Value]>();
this.receivedBlocks = new Map<string, BlockSummary>();
this.previousHashToHash = new Map<string, string>();
}
/**
* Starts watching for transactions
*/
public start(latestBlock: BlockSummary): void {
this.receivedBlocks.set(latestBlock.hash, latestBlock);
this._client.subscribeRawBlock();
this._client.on('rawblock', this._onRawBlock.bind(this));
}
/**
* Stops watching for transactions
*/
public stop(): void {
// this._client.close();
}
/**
* Watches an outpoint for broadcase in a new transaction
* @param outpoint
*/
public watchOutpoint(outpoint: OutPoint): void {
const key = outpoint.toString();
this.watchedOutPoints.set(key, outpoint);
}
/**
* Watches a scriptpubkey for broadcast in a new transaction
* @param scriptPubKey
* @param value
*/
public watchScriptPubKey(scriptPubKey: Script, value?: Value): void {
const key = scriptPubKey.toString();
this.watchedScriptPubKeys.set(key, [scriptPubKey, value]);
}
////////////////////////////////////////////////////////////////
private _checkOutpoints(block: BlockSummary, tx: Tx) {
for (const vin of tx.inputs) {
const key = vin.outpoint.toString();
const watchedOutpoint = this.watchedOutPoints.get(key);
if (watchedOutpoint) {
this.watchedOutPoints.delete(key);
this.emit('outpointspent', block, tx, watchedOutpoint);
}
}
}
private _checkScriptPubkeys(block: BlockSummary, tx: Tx) {
for (const vout of tx.outputs) {
const key = vout.scriptPubKey.toString();
const watchedScriptPubKey = this.watchedScriptPubKeys.get(key);
if (watchedScriptPubKey) {
const [, value] = watchedScriptPubKey;
if (!value || (value && vout.value === value)) {
this.watchedScriptPubKeys.delete(key);
this.emit('scriptpubkeyreceived', block, tx, watchedScriptPubKey);
}
}
}
}
private _onRawBlock(buf: Buffer) {
const block = Block.fromBuffer(buf);
let txs: [string];
for (const transaction of block.transactions) {
if (!txs) txs = [transaction.getId()];
}
const previousblockhash = Buffer.from(block.prevHash)
.reverse()
.toString('hex');
const previousBlock = this.receivedBlocks.get(previousblockhash);
const blockSize = buf.length;
if (previousBlock) {
this._processBlock(block, blockSize, txs, previousBlock);
} else {
this._findPreviousBlockAndProcess(buf, previousblockhash);
}
}
private _processBlock(
block: Block,
blockSize: number,
txs: [string],
previousBlock: BlockSummary,
) {
const blockHash = block.getId();
const blockSummary: BlockSummary = {
hash: blockHash,
confirmations: 1,
size: blockSize,
weight: block.weight(),
height: previousBlock.height + 1,
version: block.version,
versionHex: '',
merkleroot: block.merkleRoot.toString('hex'),
tx: txs,
time: block.timestamp,
mediantime: block.timestamp,
nonce: block.nonce,
bits: '',
difficulty: '',
chainwork: '',
nTx: block.transactions.length,
previousblockhash: previousBlock.hash,
nextblockhash: '',
};
this.receivedBlocks.set(blockSummary.hash, blockSummary);
for (const transaction of block.transactions) {
try {
const tx = Tx.fromBuffer(transaction.toBuffer());
this._checkOutpoints(blockSummary, tx);
this._checkScriptPubkeys(blockSummary, tx);
} catch (e) {
console.error(
'Failed to deserialize tx',
transaction.toBuffer().toString('hex'),
);
}
}
const existingBlockWithPrevHash = this.previousHashToHash.get(
blockSummary.previousblockhash,
);
if (existingBlockWithPrevHash) {
this.emit('orphanblock', existingBlockWithPrevHash, blockSummary.hash);
} else {
this.previousHashToHash.set(
blockSummary.previousblockhash,
blockSummary.hash,
);
}
}
private async _findPreviousBlockAndProcess(
currentBlockBuf: Buffer,
previousblockhash: string,
) {
const previousBuf = await this._client.getRawBlock(previousblockhash);
this._onRawBlock(previousBuf);
this._onRawBlock(currentBlockBuf);
}
}