Blockchain Traits

This commit is contained in:
Jim Hamill
2025-04-20 02:22:30 +00:00
parent b097abbd1c
commit 22bff76a55
13 changed files with 302 additions and 189 deletions
+2
View File
@@ -44,6 +44,8 @@ publish:development:
# Checkout the development branch so that we're not on a detached HEAD (which GitLab CI will default to). # Checkout the development branch so that we're not on a detached HEAD (which GitLab CI will default to).
- git checkout development - git checkout development
# Forcefully pull the latest changes.
- git pull
# Setup npm config for npmjs.com # Setup npm config for npmjs.com
- npm config set registry https://registry.npmjs.org/ - npm config set registry https://registry.npmjs.org/
+15
View File
@@ -226,6 +226,21 @@ export class Address {
return this.fromRedeemScript(Bytes.fromHex(hex).toBytes(), type); return this.fromRedeemScript(Bytes.fromHex(hex).toBytes(), type);
} }
/**
* Attempt to instantiate Address from Locking Bytecode.
*
* @param lockscript The lockscript as bytes or hex.
*
* @throws {Error} If lock script cannot be decoded into an address.
*
* @returns Instance of Address.
*/
public static fromLockscript(lockscript: string | Uint8Array) {
return lockscript instanceof Uint8Array
? this.fromLockscriptBytes(lockscript)
: this.fromLockscriptHex(lockscript);
}
/** /**
* Attempt to instantiate Address from Locking Bytecode. * Attempt to instantiate Address from Locking Bytecode.
* *
@@ -1,10 +1,9 @@
import { EventEmitter } from '../utils'; import { EventEmitter } from '../utils';
import { import {
type AddressCallback, type BlockchainCallback,
type BlockchainUTXOs, type BlockchainUTXOs,
type BlockchainTransactions, type BlockchainTransactions,
type TransactionCallback,
} from './types.js'; } from './types.js';
import { type Transaction } from '@xocash/primitives'; import { type Transaction } from '@xocash/primitives';
@@ -25,7 +24,10 @@ export type BlockchainEvents = {
* @abstract * @abstract
* @extends {EventEmitter<BlockchainEvents>} * @extends {EventEmitter<BlockchainEvents>}
*/ */
export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> { export abstract class BaseBlockchain<
AddressNotificationPayload = undefined,
TransactionNotificationPayload = undefined,
> extends EventEmitter<BlockchainEvents> {
// TODO: Lockscript might be better as the Subscribe Handler. // TODO: Lockscript might be better as the Subscribe Handler.
// From this, we can convert it to address. // From this, we can convert it to address.
// Fulcrum has Scripthash too - but I'm hesitant about this. // Fulcrum has Scripthash too - but I'm hesitant about this.
@@ -40,7 +42,6 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
* Initializes the blockchain connection and required resources. * Initializes the blockchain connection and required resources.
* Should be called before performing any blockchain operations. * Should be called before performing any blockchain operations.
* *
* @abstract
* @returns {Promise<void>} A promise that resolves when the blockchain connection is established * @returns {Promise<void>} A promise that resolves when the blockchain connection is established
* @throws {Error} If the connection cannot be established * @throws {Error} If the connection cannot be established
*/ */
@@ -50,35 +51,26 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
* Gracefully terminates the blockchain connection and cleans up resources. * Gracefully terminates the blockchain connection and cleans up resources.
* Should be called when blockchain operations are no longer needed. * Should be called when blockchain operations are no longer needed.
* *
* @abstract
* @returns {Promise<void>} A promise that resolves when the shutdown is complete * @returns {Promise<void>} A promise that resolves when the shutdown is complete
*/ */
abstract stop(): Promise<void>; abstract stop(): Promise<void>;
//-----------------------------------------------------------------------
// Traits (refer to traits.ts for docs)
//-----------------------------------------------------------------------
/** /**
* Retrieves all unspent transaction outputs (UTXOs) for a given address. * Retrieves all unspent transaction outputs (UTXOs) for a given address.
* *
* @abstract
* @param {string} address - The blockchain address to query * @param {string} address - The blockchain address to query
* @returns {Promise<BlockchainUTXOs>} A promise that resolves with the unspent outputs * @returns {Promise<BlockchainUTXOs>} A promise that resolves with the unspent outputs
* @throws {Error} If the address is invalid or the query fails * @throws {Error} If the address is invalid or the query fails
*/ */
abstract fetchUnspents(address: string): Promise<BlockchainUTXOs>; abstract fetchUnspents(address: string): Promise<BlockchainUTXOs>;
/**
* Retrieves detailed information about a specific transaction.
*
* @abstract
* @param {string} txHash - The transaction hash/ID to look up
* @returns {Promise<Transaction>} A promise that resolves with the transaction details
* @throws {Error} If the transaction cannot be found or the query fails
*/
abstract fetchTransaction(txHash: string): Promise<Transaction>;
/** /**
* Retrieves all transactions associated with a specific address. * Retrieves all transactions associated with a specific address.
* *
* @abstract
* @param {string} address - The blockchain address to query * @param {string} address - The blockchain address to query
* @returns {Promise<BlockchainTransactions>} A promise that resolves with the list of transactions * @returns {Promise<BlockchainTransactions>} A promise that resolves with the list of transactions
* @throws {Error} If the address is invalid or the query fails * @throws {Error} If the address is invalid or the query fails
@@ -87,27 +79,55 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
address: string, address: string,
): Promise<BlockchainTransactions>; ): Promise<BlockchainTransactions>;
// TODO: We may have a fetchTokenHistory in future, but Fulcrum does not currently support indexing tokens. /**
// There are actually a few functional discrepancies with the adapters: different adapters grant different capabilities. * Retrieves detailed information about a specific transaction.
// For example, Chaingraph can fetch token and address history. But BCHN P2P network cannot even fetch address history. *
// We might want to handle this by having multiple BlockchainInterfaces (e.g. IBlockchainAddressSubscriptions) that describe capabilities. * @param {string} txHash - The transaction hash/ID to look up
// But we should return to that later. * @returns {Promise<Transaction>} A promise that resolves with the transaction details
* @throws {Error} If the transaction cannot be found or the query fails
*/
abstract fetchTransaction(txHash: string): Promise<Transaction>;
/** /**
* Broadcasts a raw transaction to the blockchain network. * Broadcasts a raw transaction to the blockchain network.
* *
* @abstract
* @param {Uint8Array} transaction - The serialized transaction data to broadcast * @param {Uint8Array} transaction - The serialized transaction data to broadcast
* @returns {Promise<void>} A promise that resolves when the transaction is successfully broadcast * @returns {Promise<void>} A promise that resolves when the transaction is successfully broadcast
* @throws {Error} If the transaction is invalid or broadcasting fails * @throws {Error} If the transaction is invalid or broadcasting fails
*/ */
abstract broadcastTransaction(transaction: Uint8Array): Promise<void>; abstract broadcastTransaction(transaction: Uint8Array): Promise<void>;
/**
* Subscribes to updates for a specific lockscript.
* The provided callback will be invoked when relevant events occur for the address.
*
* @param lockscript - The blockchain lockscript to monitor
* @param callback - The callback function to handle address-related events
* @returns {Promise<void>} A promise that resolves when the subscription is established
* @throws {Error} If the subscription cannot be created
*/
abstract subscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<void>;
/**
* Removes a subscription for a specific blockchain address.
*
* @param lockscript - The blockchain lockscript to unsubscribe from
* @param callback - The callback function to remove
* @returns {Promise<void>} A promise that resolves when the subscription is removed
* @throws {Error} If the subscription cannot be removed
*/
abstract unsubscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<boolean>;
/** /**
* Subscribes to updates for a specific blockchain address. * Subscribes to updates for a specific blockchain address.
* The provided callback will be invoked when relevant events occur for the address. * The provided callback will be invoked when relevant events occur for the address.
* *
* @abstract
* @param {string} address - The blockchain address to monitor * @param {string} address - The blockchain address to monitor
* @param {AddressCallback} callback - The callback function to handle address-related events * @param {AddressCallback} callback - The callback function to handle address-related events
* @returns {Promise<void>} A promise that resolves when the subscription is established * @returns {Promise<void>} A promise that resolves when the subscription is established
@@ -115,13 +135,12 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
*/ */
abstract subscribeAddress( abstract subscribeAddress(
address: string, address: string,
callback: AddressCallback, callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<void>; ): Promise<void>;
/** /**
* Removes a subscription for a specific blockchain address. * Removes a subscription for a specific blockchain address.
* *
* @abstract
* @param {string} address - The blockchain address to unsubscribe from * @param {string} address - The blockchain address to unsubscribe from
* @param {AddressCallback} callback - The callback function to remove * @param {AddressCallback} callback - The callback function to remove
* @returns {Promise<void>} A promise that resolves when the subscription is removed * @returns {Promise<void>} A promise that resolves when the subscription is removed
@@ -129,14 +148,13 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
*/ */
abstract unsubscribeAddress( abstract unsubscribeAddress(
address: string, address: string,
callback: AddressCallback, callback: BlockchainCallback<AddressNotificationPayload>,
): Promise<void>; ): Promise<boolean>;
/** /**
* Subscribes to updates for a specific transaction. * Subscribes to updates for a specific transaction.
* The provided callback will be invoked when relevant events occur for the transaction. * The provided callback will be invoked when relevant events occur for the transaction.
* *
* @abstract
* @param {string} transactionHash - The transaction hash to monitor * @param {string} transactionHash - The transaction hash to monitor
* @param {TransactionCallback} callback - The callback function to handle transaction-related events * @param {TransactionCallback} callback - The callback function to handle transaction-related events
* @returns {Promise<void>} A promise that resolves when the subscription is established * @returns {Promise<void>} A promise that resolves when the subscription is established
@@ -144,13 +162,12 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
*/ */
abstract subscribeTransaction( abstract subscribeTransaction(
transactionHash: string, transactionHash: string,
callback: TransactionCallback, callback: BlockchainCallback<TransactionNotificationPayload>,
): Promise<void>; ): Promise<void>;
/** /**
* Removes a subscription for a specific transaction. * Removes a subscription for a specific transaction.
* *
* @abstract
* @param {string} transactionHash - The transaction hash to unsubscribe from * @param {string} transactionHash - The transaction hash to unsubscribe from
* @param {TransactionCallback} callback - The callback function to remove * @param {TransactionCallback} callback - The callback function to remove
* @returns {Promise<void>} A promise that resolves when the subscription is removed * @returns {Promise<void>} A promise that resolves when the subscription is removed
@@ -158,6 +175,6 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
*/ */
abstract unsubscribeTransaction( abstract unsubscribeTransaction(
transactionHash: string, transactionHash: string,
callback: TransactionCallback, callback: BlockchainCallback<TransactionNotificationPayload>,
): Promise<void>; ): Promise<boolean>;
} }
@@ -1,7 +1,6 @@
import { BaseBlockchain } from './base-blockchain.js'; import { BaseBlockchain } from './base-blockchain.js';
import { import {
type AddressCallback, type BlockchainCallback,
type TransactionCallback,
type BlockchainTransaction, type BlockchainTransaction,
type BlockchainUTXO, type BlockchainUTXO,
} from './types.js'; } from './types.js';
@@ -53,7 +52,20 @@ export type BlockchainElectrumParams = {
opts?: Partial<BlockchainElectrumOpts>; opts?: Partial<BlockchainElectrumOpts>;
}; };
export class BlockchainElectrum extends BaseBlockchain { export type AddressStatusPayload = {
type: 'address-status';
status: string | null;
};
export type BlockHeightPayload = {
type: 'block-height';
height: number | null;
};
export class BlockchainElectrum extends BaseBlockchain<
AddressStatusPayload,
BlockHeightPayload
> {
// Composables. // Composables.
electrumClient!: ElectrumClient<ElectrumProtocolEvents>; electrumClient!: ElectrumClient<ElectrumProtocolEvents>;
@@ -61,9 +73,17 @@ export class BlockchainElectrum extends BaseBlockchain {
isConnected = false; isConnected = false;
blockHeight: number | undefined = undefined; blockHeight: number | undefined = undefined;
// Subscriptions State. // Address Subscriptions State.
addressSubscriptions: Map<string, Set<AddressCallback>> = new Map(); addressSubscriptions: Map<
transactionSubscriptions: Map<string, Set<TransactionCallback>> = new Map(); string,
Set<BlockchainCallback<AddressStatusPayload>>
> = new Map();
// Transaction Subscription State.
transactionSubscriptions: Map<
string,
Set<BlockchainCallback<BlockHeightPayload>>
> = new Map();
// Options // Options
store: BaseStore; store: BaseStore;
@@ -314,7 +334,7 @@ export class BlockchainElectrum extends BaseBlockchain {
}, timeoutMs); }, timeoutMs);
// Create a subscribe function so that we get notified when transaction is seen by network // Create a subscribe function so that we get notified when transaction is seen by network
const subscribeCallback = (_height: number | null) => { const subscribeCallback = () => {
clearTimeout(timeout); clearTimeout(timeout);
this.unsubscribeTransaction(tx.toHashHex(), subscribeCallback); this.unsubscribeTransaction(tx.toHashHex(), subscribeCallback);
resolve(); resolve();
@@ -356,13 +376,47 @@ export class BlockchainElectrum extends BaseBlockchain {
await waitForTxPromise; await waitForTxPromise;
} }
async subscribeAddress(address: string, callback: AddressCallback) { async subscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback<AddressStatusPayload>,
) {
// Fulcrum does not support subscribing to lockscripts, so we cast it to an address.
const address = Address.fromLockscript(lockscript);
// TODO: Refactor to work around ScriptHash for Fulcrum.
// Otherwise, we may get issues with CashAddr/Legacy Addresses.
// Use our subscribeAddress function.
return await this.subscribeAddress(address.toCashAddr(), callback);
}
async unsubscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback<AddressStatusPayload>,
): Promise<boolean> {
// Fulcrum does not support subscribing to lockscripts, so we cast it to an address.
const address = Address.fromLockscript(lockscript);
// TODO: Refactor to work around ScriptHash for Fulcrum.
// Otherwise, we may get issues with CashAddr/Legacy Addresses.
// Use our unsubscribeAddress function.
return await this.unsubscribeAddress(address.toCashAddr(), callback);
}
async subscribeAddress(
address: string,
callback: BlockchainCallback<AddressStatusPayload>,
) {
// Wait for connection to be established before attempting to subscribe. // Wait for connection to be established before attempting to subscribe.
// TODO: Consider doing this at a higher level. // TODO: Consider doing this at a higher level.
if (!this.isConnected) { if (!this.isConnected) {
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected); await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
} }
// TODO: Refactor to work around ScriptHash for Fulcrum.
// Otherwise, we may get issues with CashAddr/Legacy Addresses.
// Get or create the set of callbacks for this transaction // Get or create the set of callbacks for this transaction
let callbacks = this.addressSubscriptions.get(address); let callbacks = this.addressSubscriptions.get(address);
if (!callbacks) { if (!callbacks) {
@@ -379,17 +433,28 @@ export class BlockchainElectrum extends BaseBlockchain {
callbacks.add(callback); callbacks.add(callback);
} }
async unsubscribeAddress(address: string, callback: AddressCallback) { async unsubscribeAddress(
address: string,
callback: BlockchainCallback<AddressStatusPayload>,
) {
// Wait for connection to be established before attempting to subscribe. // Wait for connection to be established before attempting to subscribe.
// TODO: Consider doing this at a higher level. // TODO: Consider doing this at a higher level.
if (!this.isConnected) { if (!this.isConnected) {
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected); await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
} }
// TODO: Refactor to work around ScriptHash for Fulcrum.
// Otherwise, we may get issues with CashAddr/Legacy Addresses.
// Get the callbacks set for this address // Get the callbacks set for this address
const callbacks = this.addressSubscriptions.get(address); const callbacks = this.addressSubscriptions.get(address);
if (!callbacks) { if (!callbacks) {
return; return false;
}
// Find the index of the callback in the handlers array
if (!callbacks.has(callback)) {
return false;
} }
// Remove the specific callback // Remove the specific callback
@@ -405,11 +470,13 @@ export class BlockchainElectrum extends BaseBlockchain {
address, address,
); );
} }
return true;
} }
async subscribeTransaction( async subscribeTransaction(
transactionHash: string, transactionHash: string,
callback: TransactionCallback, callback: BlockchainCallback<BlockHeightPayload>,
) { ) {
// Wait for connection to be established before attempting to subscribe. // Wait for connection to be established before attempting to subscribe.
// TODO: Consider doing this at a higher level. // TODO: Consider doing this at a higher level.
@@ -435,7 +502,7 @@ export class BlockchainElectrum extends BaseBlockchain {
async unsubscribeTransaction( async unsubscribeTransaction(
transactionHash: string, transactionHash: string,
callback: TransactionCallback, callback: BlockchainCallback<BlockHeightPayload>,
) { ) {
// Wait for connection to be established before attempting to subscribe. // Wait for connection to be established before attempting to subscribe.
// TODO: Consider doing this at a higher level. // TODO: Consider doing this at a higher level.
@@ -446,7 +513,11 @@ export class BlockchainElectrum extends BaseBlockchain {
// Get the callbacks set for this transaction // Get the callbacks set for this transaction
const callbacks = this.transactionSubscriptions.get(transactionHash); const callbacks = this.transactionSubscriptions.get(transactionHash);
if (!callbacks) { if (!callbacks) {
return; return false;
}
if (!callbacks.has(callback)) {
return false;
} }
// Remove the specific callback // Remove the specific callback
@@ -462,6 +533,8 @@ export class BlockchainElectrum extends BaseBlockchain {
transactionHash, transactionHash,
); );
} }
return true;
} }
async request(method: string, ...parameters: Array<RPCParameter>) { async request(method: string, ...parameters: Array<RPCParameter>) {
@@ -523,7 +596,7 @@ export class BlockchainElectrum extends BaseBlockchain {
// Execute all callbacks for this transaction. // Execute all callbacks for this transaction.
for (const callback of callbacks) { for (const callback of callbacks) {
try { try {
callback(status); callback(address, { type: 'address-status', status });
} catch (error) { } catch (error) {
// Handle or log any callback execution errors // Handle or log any callback execution errors
console.error( console.error(
@@ -549,7 +622,7 @@ export class BlockchainElectrum extends BaseBlockchain {
// Execute all callbacks for this transaction. // Execute all callbacks for this transaction.
for (const callback of callbacks) { for (const callback of callbacks) {
try { try {
callback(height); callback(txHash, { type: 'block-height', height });
} catch (error) { } catch (error) {
// Handle or log any callback execution errors // Handle or log any callback execution errors
console.error( console.error(
@@ -1,7 +1,6 @@
import { BaseBlockchain } from './base-blockchain.js'; import { BaseBlockchain } from './base-blockchain.js';
import { import {
type AddressCallback, type BlockchainCallback,
type TransactionCallback,
type BlockchainTransaction, type BlockchainTransaction,
type BlockchainTransactions, type BlockchainTransactions,
type BlockchainUTXO, type BlockchainUTXO,
@@ -24,10 +23,14 @@ import {
// Local Blockchain Adapter. // Local Blockchain Adapter.
// This simulates the BCH Blockchain. // This simulates the BCH Blockchain.
export class BlockchainLocal extends BaseBlockchain { export class BlockchainInMemory extends BaseBlockchain {
static from() {
return new BlockchainInMemory();
}
private transactions: BlockchainTransactions; private transactions: BlockchainTransactions;
private addressSubscriptions: Map<string, Set<AddressCallback>>; private addressSubscriptions: Map<string, Set<BlockchainCallback>>;
private transactionSubscriptions: Map<string, Set<TransactionCallback>>; private transactionSubscriptions: Map<string, Set<BlockchainCallback>>;
constructor() { constructor() {
super(); super();
@@ -37,13 +40,9 @@ export class BlockchainLocal extends BaseBlockchain {
this.transactionSubscriptions = new Map(); this.transactionSubscriptions = new Map();
} }
async start() { async start() {}
console.log('BlockchainLocal: Started');
}
async stop() { async stop() {}
console.log('BlockchainLocal: Stopped');
}
async fetchUnspents(address: string) { async fetchUnspents(address: string) {
// Derive the current UTXO set. // Derive the current UTXO set.
@@ -117,7 +116,7 @@ export class BlockchainLocal extends BaseBlockchain {
}); });
// Validate the transaction. // Validate the transaction.
BlockchainLocal.validateTransaction( BlockchainInMemory.validateTransaction(
tx, tx,
sourceOutputs.map((utxo) => utxo.output), sourceOutputs.map((utxo) => utxo.output),
); );
@@ -135,7 +134,35 @@ export class BlockchainLocal extends BaseBlockchain {
this.notifyTransactionSubscribers(tx); this.notifyTransactionSubscribers(tx);
} }
async subscribeAddress(address: string, callback: AddressCallback) { async subscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback,
) {
// Fulcrum does not support subscribing to lockscripts, so we cast it to an address.
const address = Address.fromLockscript(lockscript);
// TODO: Refactor to work around ScriptHash for Fulcrum.
// Otherwise, we may get issues with CashAddr/Legacy Addresses.
// Use our subscribeAddress function.
return await this.subscribeAddress(address.toCashAddr(), callback);
}
async unsubscribeLockscript(
lockscript: string | Uint8Array,
callback: BlockchainCallback,
): Promise<boolean> {
// Fulcrum does not support subscribing to lockscripts, so we cast it to an address.
const address = Address.fromLockscript(lockscript);
// TODO: Refactor to work around ScriptHash for Fulcrum.
// Otherwise, we may get issues with CashAddr/Legacy Addresses.
// Use our unsubscribeAddress function.
return await this.unsubscribeAddress(address.toCashAddr(), callback);
}
async subscribeAddress(address: string, callback: BlockchainCallback) {
// Convert the address to it's lockscript hex so that it's consistently represented. // Convert the address to it's lockscript hex so that it's consistently represented.
const lockscriptHex = Address.from(address).toLockscriptHex(); const lockscriptHex = Address.from(address).toLockscriptHex();
@@ -145,26 +172,35 @@ export class BlockchainLocal extends BaseBlockchain {
} }
// Add the address notification callback. // Add the address notification callback.
this.addressSubscriptions.get(address)!.add(callback); this.addressSubscriptions.get(lockscriptHex)!.add(callback);
} }
async unsubscribeAddress(address: string, callback: AddressCallback) { async unsubscribeAddress(address: string, callback: BlockchainCallback) {
// Convert the address to it's lockscript hex so that it's consistently represented. // Convert the address to it's lockscript hex so that it's consistently represented.
const lockscriptHex = Address.from(address).toLockscriptHex(); const lockscriptHex = Address.from(address).toLockscriptHex();
// Find all callbacks this lockscript is subscribed to. // Find all callbacks this lockscript is subscribed to.
const callbacks = this.addressSubscriptions.get(lockscriptHex); const callbacks = this.addressSubscriptions.get(lockscriptHex);
if (callbacks) { if (callbacks) {
if (callbacks.has(callback)) {
return false;
}
callbacks.delete(callback); callbacks.delete(callback);
if (callbacks.size === 0) { if (callbacks.size === 0) {
this.addressSubscriptions.delete(address); this.addressSubscriptions.delete(address);
} }
return true;
} }
return false;
} }
async subscribeTransaction( async subscribeTransaction(
transactionHash: string, transactionHash: string,
callback: TransactionCallback, callback: BlockchainCallback,
) { ) {
if (!this.transactionSubscriptions.has(transactionHash)) { if (!this.transactionSubscriptions.has(transactionHash)) {
this.transactionSubscriptions.set(transactionHash, new Set()); this.transactionSubscriptions.set(transactionHash, new Set());
@@ -174,15 +210,21 @@ export class BlockchainLocal extends BaseBlockchain {
async unsubscribeTransaction( async unsubscribeTransaction(
transactionHash: string, transactionHash: string,
callback: TransactionCallback, callback: BlockchainCallback,
) { ) {
const callbacks = this.transactionSubscriptions.get(transactionHash); const callbacks = this.transactionSubscriptions.get(transactionHash);
if (callbacks) { if (callbacks) {
if (!callbacks.has(callback)) {
return false;
}
callbacks.delete(callback); callbacks.delete(callback);
if (callbacks.size === 0) { if (callbacks.size === 0) {
this.transactionSubscriptions.delete(transactionHash); this.transactionSubscriptions.delete(transactionHash);
} }
} }
return false;
} }
private notifyAddressSubscribers(tx: Transaction) { private notifyAddressSubscribers(tx: Transaction) {
@@ -208,7 +250,7 @@ export class BlockchainLocal extends BaseBlockchain {
const callbacks = this.addressSubscriptions.get(address); const callbacks = this.addressSubscriptions.get(address);
if (callbacks) { if (callbacks) {
callbacks.forEach((callback) => callback('updated')); callbacks.forEach((callback) => callback(address));
} }
}); });
@@ -221,7 +263,7 @@ export class BlockchainLocal extends BaseBlockchain {
const callbacks = this.addressSubscriptions.get(address); const callbacks = this.addressSubscriptions.get(address);
if (callbacks) { if (callbacks) {
callbacks.forEach((callback) => callback('updated')); callbacks.forEach((callback) => callback(address));
} }
}); });
} }
@@ -231,7 +273,7 @@ export class BlockchainLocal extends BaseBlockchain {
const callbacks = this.transactionSubscriptions.get(txHash); const callbacks = this.transactionSubscriptions.get(txHash);
if (callbacks) { if (callbacks) {
// In this local simulation, we're not tracking block height, so we'll use null // In this local simulation, we're not tracking block height, so we'll use null
callbacks.forEach((callback) => callback(null)); callbacks.forEach((callback) => callback(txHash));
} }
} }
@@ -274,6 +316,26 @@ export class BlockchainLocal extends BaseBlockchain {
return utxoMap; return utxoMap;
} }
async mineBlock(outputs: Array<Output>): Promise<void> {
// Create a transaction that pays to our subscribed address.
const transaction = Transaction.fromRaw({
version: 2,
locktime: 0,
inputs: [
{
outpointIndex: 0,
outpointTransactionHash: new Uint8Array(),
unlockingBytecode: new Uint8Array(),
sequenceNumber: 0,
},
],
outputs,
});
// Broadcast that transaction.
await this.broadcastTransaction(transaction, { skipValidation: true });
}
/** /**
* @remarks Consider moving this to Transaction Primitive. Trade-off is that it's probably big. Maybe standalone util? * @remarks Consider moving this to Transaction Primitive. Trade-off is that it's probably big. Maybe standalone util?
*/ */
-100
View File
@@ -1,100 +0,0 @@
import { BaseBlockchain } from './base-blockchain.js';
import { type AddressCallback, type TransactionCallback } from './types.js';
import { BlockchainElectrum } from './blockchain-electrum.js';
import { BlockchainLocal } from './blockchain-local.js';
import { Transaction } from '@xocash/primitives';
/**
* The idea here is that this will implement the IBlockchain interface.
* But, it will allow the dev to specify multiple child IBlockchain interfaces for configuration.
* So, if the network is not available, it can fallback to the local and sync later.
*
* @example
* ```ts
* // Actual interface will be more complex, but this is the rough idea.
* const blockchain = new Blockchain([
* new BlockchainLocal(...),
* new BlockchainElectrum(...)
* ])
* ```
*/
export class Blockchain extends BaseBlockchain {
blockchainLocal: BlockchainLocal;
blockchainElectrum: BlockchainElectrum;
constructor() {
super();
this.blockchainLocal = new BlockchainLocal();
this.blockchainElectrum = new BlockchainElectrum();
}
async start() {
await Promise.all([
this.blockchainLocal.start(),
this.blockchainElectrum.start(),
]);
}
async stop() {
await Promise.all([
this.blockchainLocal.stop(),
this.blockchainElectrum.stop(),
]);
}
async fetchUnspents(address: string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.fetchUnspents(address);
} else {
return await this.blockchainLocal.fetchUnspents(address);
}
}
async fetchTransaction(txHash: string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.fetchTransaction(txHash);
} else {
return await this.blockchainLocal.fetchTransaction(txHash);
}
}
async fetchAddressHistory(address: string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.fetchAddressHistory(address);
} else {
return await this.blockchainLocal.fetchAddressHistory(address);
}
}
async broadcastTransaction(tx: Uint8Array | Transaction | string) {
if (this.blockchainElectrum.isConnected) {
return await this.blockchainElectrum.broadcastTransaction(tx);
} else {
return await this.blockchainLocal.broadcastTransaction(tx);
}
}
async subscribeAddress(_address: string, _callback: AddressCallback) {
throw new Error('Not yet implemented');
}
async unsubscribeAddress(_address: string, _callback: AddressCallback) {
throw new Error('Not yet implemented');
}
async subscribeTransaction(
_transactionHash: string,
_callback: TransactionCallback,
) {
throw new Error('Not yet implemented');
}
async unsubscribeTransaction(
_transactionHash: string,
_callback: TransactionCallback,
) {
throw new Error('Not yet implemented');
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
export * from './base-blockchain.js'; export * from './base-blockchain.js';
export * from './blockchain.js';
export * from './blockchain-electrum.js'; export * from './blockchain-electrum.js';
export * from './blockchain-local.js'; export * from './blockchain-inmemory.js';
export * from './traits.js';
export * from './types.js'; export * from './types.js';
+24
View File
@@ -0,0 +1,24 @@
import { type BaseBlockchain } from './base-blockchain.js';
export type BlockchainLockscriptSubscribable<CallbackPayload = undefined> =
Pick<
BaseBlockchain<CallbackPayload>,
'subscribeLockscript' | 'unsubscribeLockscript'
>;
export type BlockchainAddressSubscribable<CallbackPayload = undefined> = Pick<
BaseBlockchain<CallbackPayload>,
'subscribeAddress' | 'unsubscribeAddress'
>;
export type BlockchainTransactionSubscribable<CallbackPayload = undefined> =
Pick<
BaseBlockchain<CallbackPayload>,
'subscribeTransaction' | 'unsubscribeTransaction'
>;
// TODO: We may have a fetchTokenHistory in future, but Fulcrum does not currently support indexing tokens.
// There are actually a few functional discrepancies with the adapters: different adapters grant different capabilities.
// For example, Chaingraph can fetch token and address history. But BCHN P2P network cannot even fetch address history.
// We might want to handle this by having multiple BlockchainInterfaces (e.g. IBlockchainAddressSubscriptions) that describe capabilities.
// But we should return to that later.
+6 -2
View File
@@ -1,7 +1,11 @@
import { Transaction, TransactionHash, UTXO, ExtMap } from '@xocash/primitives'; import { Transaction, TransactionHash, UTXO, ExtMap } from '@xocash/primitives';
export type AddressCallback = (status: string | null) => void; export type BlockchainCallback<Payload = undefined> = (
export type TransactionCallback = (height: number | null) => void; /** The transaction hash, address, lockscript or scriptHash of this subscription. */
id: string,
/** Additional payload information provided by this subscription */
payload?: Payload | undefined,
) => void;
export type BlockchainTransaction = { export type BlockchainTransaction = {
hash: TransactionHash; hash: TransactionHash;
+2 -5
View File
@@ -5,10 +5,7 @@ import {
type WalletTransactions, type WalletTransactions,
} from './types.js'; } from './types.js';
import { import { type BlockchainUTXOs } from '../blockchain';
type BlockchainTransactions,
type BlockchainUTXOs,
} from '../blockchain';
import { type Activities } from '../storage'; import { type Activities } from '../storage';
import { EventEmitter } from '../utils'; import { EventEmitter } from '../utils';
@@ -16,7 +13,7 @@ import { type TokenBalances, type Address } from '@xocash/primitives';
export type WalletEvents = { export type WalletEvents = {
shouldMonitorUpdated: boolean; shouldMonitorUpdated: boolean;
transactionsUpdated: BlockchainTransactions; transactionsUpdated: WalletTransactions;
unspentsUpdated: BlockchainUTXOs; unspentsUpdated: BlockchainUTXOs;
stateUpdated: string | null; stateUpdated: string | null;
}; };
+13 -3
View File
@@ -1,4 +1,9 @@
import { type BaseBlockchain, type BlockchainTransaction } from '../blockchain'; import {
type BaseBlockchain,
type AddressStatusPayload,
type BlockHeightPayload,
type BlockchainTransaction,
} from '../blockchain';
import { type Activities, type BaseStore } from '../storage'; import { type Activities, type BaseStore } from '../storage';
import { type Address, type ExtMap } from '@xocash/primitives'; import { type Address, type ExtMap } from '@xocash/primitives';
@@ -12,9 +17,15 @@ import {
type Output, type Output,
} from '@bitauth/libauth'; } from '@bitauth/libauth';
// NOTE: For Wallets, the Blockchain Adapter must have Address and Transaction indexing.
export type WalletBlockchain = BaseBlockchain<
AddressStatusPayload,
BlockHeightPayload
>;
export type WalletDependencies = { export type WalletDependencies = {
activities?: Activities; activities?: Activities;
blockchain: BaseBlockchain; blockchain: WalletBlockchain;
cache?: BaseStore; cache?: BaseStore;
}; };
@@ -24,7 +35,6 @@ export type InputTemplateBCH = InputTemplate<
CompilationData CompilationData
>; >;
// TODO: Rename me to WalletUTXO
export type WalletUTXO = { export type WalletUTXO = {
outpointIndex: number; outpointIndex: number;
outpointTransactionHash: Uint8Array; outpointTransactionHash: Uint8Array;
+2 -3
View File
@@ -1,10 +1,9 @@
import { BaseWallet } from './base-wallet.js'; import { BaseWallet } from './base-wallet.js';
import { type TransactionTemplate } from './types.js'; import { type TransactionTemplate } from './types.js';
import { TransactionBuilder } from './transaction-builder.js'; import { TransactionBuilder } from './transaction-builder.js';
import { type WalletDependencies } from './types.js'; import { type WalletBlockchain, type WalletDependencies } from './types.js';
import { WalletP2PKH } from './wallet-p2pkh.js'; import { WalletP2PKH } from './wallet-p2pkh.js';
import { type BaseBlockchain } from '../blockchain';
import { type BaseStore, Activities, StoreInMemory } from '../storage'; import { type BaseStore, Activities, StoreInMemory } from '../storage';
import { import {
@@ -80,7 +79,7 @@ export class WalletHD extends BaseWallet {
// Dependencies. // Dependencies.
public readonly activities: Activities; public readonly activities: Activities;
public readonly blockchain: BaseBlockchain; public readonly blockchain: WalletBlockchain;
public readonly cache: BaseStore; public readonly cache: BaseStore;
// Genesis Data. // Genesis Data.
+16 -6
View File
@@ -1,8 +1,15 @@
import { BaseWallet } from './base-wallet.js'; import { BaseWallet } from './base-wallet.js';
import { type TransactionTemplate, type WalletDependencies } from './types.js'; import {
type TransactionTemplate,
type WalletBlockchain,
type WalletDependencies,
} from './types.js';
import { TransactionBuilder } from './transaction-builder.js'; import { TransactionBuilder } from './transaction-builder.js';
import { type BaseBlockchain, type BlockchainTransaction } from '../blockchain'; import {
type AddressStatusPayload,
type BlockchainTransaction,
} from '../blockchain';
import { type BaseStore, Activities, StoreInMemory } from '../storage'; import { type BaseStore, Activities, StoreInMemory } from '../storage';
import { import {
@@ -116,7 +123,7 @@ export class WalletP2PKH extends BaseWallet {
// Dependencies. // Dependencies.
public readonly activities: Activities; public readonly activities: Activities;
public readonly blockchain: BaseBlockchain; public readonly blockchain: WalletBlockchain;
public readonly cache: BaseStore; public readonly cache: BaseStore;
// Genesis Data. // Genesis Data.
@@ -408,11 +415,14 @@ export class WalletP2PKH extends BaseWallet {
await this.blockchain.broadcastTransaction(generatedTxBytes); await this.blockchain.broadcastTransaction(generatedTxBytes);
} }
private async onAddressNotification(status: string | null) { private async onAddressNotification(
_address: string,
payload?: AddressStatusPayload,
) {
// If wallet state has changed... // If wallet state has changed...
if (status !== this.status) { if (payload && payload.status !== this.status) {
// Set our new status. // Set our new status.
this.status = status; this.status = payload.status;
// Emit an event. // Emit an event.
this.emit('stateUpdated', this.status); this.emit('stateUpdated', this.status);