Blockchain Traits
This commit is contained in:
@@ -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).
|
||||
- git checkout development
|
||||
# Forcefully pull the latest changes.
|
||||
- git pull
|
||||
|
||||
# Setup npm config for npmjs.com
|
||||
- npm config set registry https://registry.npmjs.org/
|
||||
|
||||
@@ -226,6 +226,21 @@ export class Address {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { EventEmitter } from '../utils';
|
||||
|
||||
import {
|
||||
type AddressCallback,
|
||||
type BlockchainCallback,
|
||||
type BlockchainUTXOs,
|
||||
type BlockchainTransactions,
|
||||
type TransactionCallback,
|
||||
} from './types.js';
|
||||
|
||||
import { type Transaction } from '@xocash/primitives';
|
||||
@@ -25,7 +24,10 @@ export type BlockchainEvents = {
|
||||
* @abstract
|
||||
* @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.
|
||||
// From this, we can convert it to address.
|
||||
// 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.
|
||||
* Should be called before performing any blockchain operations.
|
||||
*
|
||||
* @abstract
|
||||
* @returns {Promise<void>} A promise that resolves when the blockchain connection is 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.
|
||||
* Should be called when blockchain operations are no longer needed.
|
||||
*
|
||||
* @abstract
|
||||
* @returns {Promise<void>} A promise that resolves when the shutdown is complete
|
||||
*/
|
||||
abstract stop(): Promise<void>;
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// Traits (refer to traits.ts for docs)
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Retrieves all unspent transaction outputs (UTXOs) for a given address.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} address - The blockchain address to query
|
||||
* @returns {Promise<BlockchainUTXOs>} A promise that resolves with the unspent outputs
|
||||
* @throws {Error} If the address is invalid or the query fails
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} address - The blockchain address to query
|
||||
* @returns {Promise<BlockchainTransactions>} A promise that resolves with the list of transactions
|
||||
* @throws {Error} If the address is invalid or the query fails
|
||||
@@ -87,27 +79,55 @@ export abstract class BaseBlockchain extends EventEmitter<BlockchainEvents> {
|
||||
address: string,
|
||||
): 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.
|
||||
// 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.
|
||||
/**
|
||||
* Retrieves detailed information about a specific transaction.
|
||||
*
|
||||
* @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>;
|
||||
|
||||
/**
|
||||
* Broadcasts a raw transaction to the blockchain network.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Uint8Array} transaction - The serialized transaction data to broadcast
|
||||
* @returns {Promise<void>} A promise that resolves when the transaction is successfully broadcast
|
||||
* @throws {Error} If the transaction is invalid or broadcasting fails
|
||||
*/
|
||||
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.
|
||||
* The provided callback will be invoked when relevant events occur for the address.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} address - The blockchain address to monitor
|
||||
* @param {AddressCallback} callback - The callback function to handle address-related events
|
||||
* @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(
|
||||
address: string,
|
||||
callback: AddressCallback,
|
||||
callback: BlockchainCallback<AddressNotificationPayload>,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes a subscription for a specific blockchain address.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} address - The blockchain address to unsubscribe from
|
||||
* @param {AddressCallback} callback - The callback function to remove
|
||||
* @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(
|
||||
address: string,
|
||||
callback: AddressCallback,
|
||||
): Promise<void>;
|
||||
callback: BlockchainCallback<AddressNotificationPayload>,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Subscribes to updates for a specific transaction.
|
||||
* The provided callback will be invoked when relevant events occur for the transaction.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} transactionHash - The transaction hash to monitor
|
||||
* @param {TransactionCallback} callback - The callback function to handle transaction-related events
|
||||
* @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(
|
||||
transactionHash: string,
|
||||
callback: TransactionCallback,
|
||||
callback: BlockchainCallback<TransactionNotificationPayload>,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes a subscription for a specific transaction.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} transactionHash - The transaction hash to unsubscribe from
|
||||
* @param {TransactionCallback} callback - The callback function to remove
|
||||
* @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(
|
||||
transactionHash: string,
|
||||
callback: TransactionCallback,
|
||||
): Promise<void>;
|
||||
callback: BlockchainCallback<TransactionNotificationPayload>,
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BaseBlockchain } from './base-blockchain.js';
|
||||
import {
|
||||
type AddressCallback,
|
||||
type TransactionCallback,
|
||||
type BlockchainCallback,
|
||||
type BlockchainTransaction,
|
||||
type BlockchainUTXO,
|
||||
} from './types.js';
|
||||
@@ -53,7 +52,20 @@ export type BlockchainElectrumParams = {
|
||||
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.
|
||||
electrumClient!: ElectrumClient<ElectrumProtocolEvents>;
|
||||
|
||||
@@ -61,9 +73,17 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
isConnected = false;
|
||||
blockHeight: number | undefined = undefined;
|
||||
|
||||
// Subscriptions State.
|
||||
addressSubscriptions: Map<string, Set<AddressCallback>> = new Map();
|
||||
transactionSubscriptions: Map<string, Set<TransactionCallback>> = new Map();
|
||||
// Address Subscriptions State.
|
||||
addressSubscriptions: Map<
|
||||
string,
|
||||
Set<BlockchainCallback<AddressStatusPayload>>
|
||||
> = new Map();
|
||||
|
||||
// Transaction Subscription State.
|
||||
transactionSubscriptions: Map<
|
||||
string,
|
||||
Set<BlockchainCallback<BlockHeightPayload>>
|
||||
> = new Map();
|
||||
|
||||
// Options
|
||||
store: BaseStore;
|
||||
@@ -314,7 +334,7 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
}, timeoutMs);
|
||||
|
||||
// Create a subscribe function so that we get notified when transaction is seen by network
|
||||
const subscribeCallback = (_height: number | null) => {
|
||||
const subscribeCallback = () => {
|
||||
clearTimeout(timeout);
|
||||
this.unsubscribeTransaction(tx.toHashHex(), subscribeCallback);
|
||||
resolve();
|
||||
@@ -356,13 +376,47 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
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.
|
||||
// TODO: Consider doing this at a higher level.
|
||||
if (!this.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
|
||||
let callbacks = this.addressSubscriptions.get(address);
|
||||
if (!callbacks) {
|
||||
@@ -379,17 +433,28 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
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.
|
||||
// TODO: Consider doing this at a higher level.
|
||||
if (!this.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
|
||||
const callbacks = this.addressSubscriptions.get(address);
|
||||
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
|
||||
@@ -405,11 +470,13 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
address,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async subscribeTransaction(
|
||||
transactionHash: string,
|
||||
callback: TransactionCallback,
|
||||
callback: BlockchainCallback<BlockHeightPayload>,
|
||||
) {
|
||||
// Wait for connection to be established before attempting to subscribe.
|
||||
// TODO: Consider doing this at a higher level.
|
||||
@@ -435,7 +502,7 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
|
||||
async unsubscribeTransaction(
|
||||
transactionHash: string,
|
||||
callback: TransactionCallback,
|
||||
callback: BlockchainCallback<BlockHeightPayload>,
|
||||
) {
|
||||
// Wait for connection to be established before attempting to subscribe.
|
||||
// TODO: Consider doing this at a higher level.
|
||||
@@ -446,7 +513,11 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
// Get the callbacks set for this transaction
|
||||
const callbacks = this.transactionSubscriptions.get(transactionHash);
|
||||
if (!callbacks) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!callbacks.has(callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the specific callback
|
||||
@@ -462,6 +533,8 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
transactionHash,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async request(method: string, ...parameters: Array<RPCParameter>) {
|
||||
@@ -523,7 +596,7 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
// Execute all callbacks for this transaction.
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
callback(status);
|
||||
callback(address, { type: 'address-status', status });
|
||||
} catch (error) {
|
||||
// Handle or log any callback execution errors
|
||||
console.error(
|
||||
@@ -549,7 +622,7 @@ export class BlockchainElectrum extends BaseBlockchain {
|
||||
// Execute all callbacks for this transaction.
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
callback(height);
|
||||
callback(txHash, { type: 'block-height', height });
|
||||
} catch (error) {
|
||||
// Handle or log any callback execution errors
|
||||
console.error(
|
||||
|
||||
+82
-20
@@ -1,7 +1,6 @@
|
||||
import { BaseBlockchain } from './base-blockchain.js';
|
||||
import {
|
||||
type AddressCallback,
|
||||
type TransactionCallback,
|
||||
type BlockchainCallback,
|
||||
type BlockchainTransaction,
|
||||
type BlockchainTransactions,
|
||||
type BlockchainUTXO,
|
||||
@@ -24,10 +23,14 @@ import {
|
||||
|
||||
// Local Blockchain Adapter.
|
||||
// This simulates the BCH Blockchain.
|
||||
export class BlockchainLocal extends BaseBlockchain {
|
||||
export class BlockchainInMemory extends BaseBlockchain {
|
||||
static from() {
|
||||
return new BlockchainInMemory();
|
||||
}
|
||||
|
||||
private transactions: BlockchainTransactions;
|
||||
private addressSubscriptions: Map<string, Set<AddressCallback>>;
|
||||
private transactionSubscriptions: Map<string, Set<TransactionCallback>>;
|
||||
private addressSubscriptions: Map<string, Set<BlockchainCallback>>;
|
||||
private transactionSubscriptions: Map<string, Set<BlockchainCallback>>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -37,13 +40,9 @@ export class BlockchainLocal extends BaseBlockchain {
|
||||
this.transactionSubscriptions = new Map();
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log('BlockchainLocal: Started');
|
||||
}
|
||||
async start() {}
|
||||
|
||||
async stop() {
|
||||
console.log('BlockchainLocal: Stopped');
|
||||
}
|
||||
async stop() {}
|
||||
|
||||
async fetchUnspents(address: string) {
|
||||
// Derive the current UTXO set.
|
||||
@@ -117,7 +116,7 @@ export class BlockchainLocal extends BaseBlockchain {
|
||||
});
|
||||
|
||||
// Validate the transaction.
|
||||
BlockchainLocal.validateTransaction(
|
||||
BlockchainInMemory.validateTransaction(
|
||||
tx,
|
||||
sourceOutputs.map((utxo) => utxo.output),
|
||||
);
|
||||
@@ -135,7 +134,35 @@ export class BlockchainLocal extends BaseBlockchain {
|
||||
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.
|
||||
const lockscriptHex = Address.from(address).toLockscriptHex();
|
||||
|
||||
@@ -145,26 +172,35 @@ export class BlockchainLocal extends BaseBlockchain {
|
||||
}
|
||||
|
||||
// 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.
|
||||
const lockscriptHex = Address.from(address).toLockscriptHex();
|
||||
|
||||
// Find all callbacks this lockscript is subscribed to.
|
||||
const callbacks = this.addressSubscriptions.get(lockscriptHex);
|
||||
if (callbacks) {
|
||||
if (callbacks.has(callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
callbacks.delete(callback);
|
||||
|
||||
if (callbacks.size === 0) {
|
||||
this.addressSubscriptions.delete(address);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async subscribeTransaction(
|
||||
transactionHash: string,
|
||||
callback: TransactionCallback,
|
||||
callback: BlockchainCallback,
|
||||
) {
|
||||
if (!this.transactionSubscriptions.has(transactionHash)) {
|
||||
this.transactionSubscriptions.set(transactionHash, new Set());
|
||||
@@ -174,15 +210,21 @@ export class BlockchainLocal extends BaseBlockchain {
|
||||
|
||||
async unsubscribeTransaction(
|
||||
transactionHash: string,
|
||||
callback: TransactionCallback,
|
||||
callback: BlockchainCallback,
|
||||
) {
|
||||
const callbacks = this.transactionSubscriptions.get(transactionHash);
|
||||
if (callbacks) {
|
||||
if (!callbacks.has(callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
callbacks.delete(callback);
|
||||
if (callbacks.size === 0) {
|
||||
this.transactionSubscriptions.delete(transactionHash);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private notifyAddressSubscribers(tx: Transaction) {
|
||||
@@ -208,7 +250,7 @@ export class BlockchainLocal extends BaseBlockchain {
|
||||
const callbacks = this.addressSubscriptions.get(address);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
if (callbacks) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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?
|
||||
*/
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './base-blockchain.js';
|
||||
export * from './blockchain.js';
|
||||
export * from './blockchain-electrum.js';
|
||||
export * from './blockchain-local.js';
|
||||
export * from './blockchain-inmemory.js';
|
||||
export * from './traits.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -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.
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Transaction, TransactionHash, UTXO, ExtMap } from '@xocash/primitives';
|
||||
|
||||
export type AddressCallback = (status: string | null) => void;
|
||||
export type TransactionCallback = (height: number | null) => void;
|
||||
export type BlockchainCallback<Payload = undefined> = (
|
||||
/** 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 = {
|
||||
hash: TransactionHash;
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
type WalletTransactions,
|
||||
} from './types.js';
|
||||
|
||||
import {
|
||||
type BlockchainTransactions,
|
||||
type BlockchainUTXOs,
|
||||
} from '../blockchain';
|
||||
import { type BlockchainUTXOs } from '../blockchain';
|
||||
import { type Activities } from '../storage';
|
||||
import { EventEmitter } from '../utils';
|
||||
|
||||
@@ -16,7 +13,7 @@ import { type TokenBalances, type Address } from '@xocash/primitives';
|
||||
|
||||
export type WalletEvents = {
|
||||
shouldMonitorUpdated: boolean;
|
||||
transactionsUpdated: BlockchainTransactions;
|
||||
transactionsUpdated: WalletTransactions;
|
||||
unspentsUpdated: BlockchainUTXOs;
|
||||
stateUpdated: string | null;
|
||||
};
|
||||
|
||||
@@ -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 Address, type ExtMap } from '@xocash/primitives';
|
||||
@@ -12,9 +17,15 @@ import {
|
||||
type Output,
|
||||
} from '@bitauth/libauth';
|
||||
|
||||
// NOTE: For Wallets, the Blockchain Adapter must have Address and Transaction indexing.
|
||||
export type WalletBlockchain = BaseBlockchain<
|
||||
AddressStatusPayload,
|
||||
BlockHeightPayload
|
||||
>;
|
||||
|
||||
export type WalletDependencies = {
|
||||
activities?: Activities;
|
||||
blockchain: BaseBlockchain;
|
||||
blockchain: WalletBlockchain;
|
||||
cache?: BaseStore;
|
||||
};
|
||||
|
||||
@@ -24,7 +35,6 @@ export type InputTemplateBCH = InputTemplate<
|
||||
CompilationData
|
||||
>;
|
||||
|
||||
// TODO: Rename me to WalletUTXO
|
||||
export type WalletUTXO = {
|
||||
outpointIndex: number;
|
||||
outpointTransactionHash: Uint8Array;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { BaseWallet } from './base-wallet.js';
|
||||
import { type TransactionTemplate } from './types.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 { type BaseBlockchain } from '../blockchain';
|
||||
import { type BaseStore, Activities, StoreInMemory } from '../storage';
|
||||
|
||||
import {
|
||||
@@ -80,7 +79,7 @@ export class WalletHD extends BaseWallet {
|
||||
|
||||
// Dependencies.
|
||||
public readonly activities: Activities;
|
||||
public readonly blockchain: BaseBlockchain;
|
||||
public readonly blockchain: WalletBlockchain;
|
||||
public readonly cache: BaseStore;
|
||||
|
||||
// Genesis Data.
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
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 { type BaseBlockchain, type BlockchainTransaction } from '../blockchain';
|
||||
import {
|
||||
type AddressStatusPayload,
|
||||
type BlockchainTransaction,
|
||||
} from '../blockchain';
|
||||
import { type BaseStore, Activities, StoreInMemory } from '../storage';
|
||||
|
||||
import {
|
||||
@@ -116,7 +123,7 @@ export class WalletP2PKH extends BaseWallet {
|
||||
|
||||
// Dependencies.
|
||||
public readonly activities: Activities;
|
||||
public readonly blockchain: BaseBlockchain;
|
||||
public readonly blockchain: WalletBlockchain;
|
||||
public readonly cache: BaseStore;
|
||||
|
||||
// Genesis Data.
|
||||
@@ -408,11 +415,14 @@ export class WalletP2PKH extends BaseWallet {
|
||||
await this.blockchain.broadcastTransaction(generatedTxBytes);
|
||||
}
|
||||
|
||||
private async onAddressNotification(status: string | null) {
|
||||
private async onAddressNotification(
|
||||
_address: string,
|
||||
payload?: AddressStatusPayload,
|
||||
) {
|
||||
// If wallet state has changed...
|
||||
if (status !== this.status) {
|
||||
if (payload && payload.status !== this.status) {
|
||||
// Set our new status.
|
||||
this.status = status;
|
||||
this.status = payload.status;
|
||||
|
||||
// Emit an event.
|
||||
this.emit('stateUpdated', this.status);
|
||||
|
||||
Reference in New Issue
Block a user