diff --git a/docs/bitcoin.md b/docs/bitcoin.md index 1c241e7f2..3cca37e0c 100644 --- a/docs/bitcoin.md +++ b/docs/bitcoin.md @@ -31,7 +31,7 @@ This allotted amount is locked together with value time lock for simplicity of r ## Events -### `bitcoin_processor_databases_revert` +### `bitcoin_databases_revert` Occurs every time the databases are reverted due to a bitcoin reorg. Event data: @@ -41,12 +41,37 @@ Event data: } ``` -### `bitcoin_processor_observing_loop_failed` -Occurs every time the bitcoin processor fails an observing loop. +### `bitcoin_lock_monitor_lock_released` +Occurs every time the lock monitor releases a value lock. Event data: none -### `bitcoin_processor_observing_loop_success` -Occurs every time the bitcoin processor successfully completes a processing loop. +### `bitcoin_lock_monitor_lock_renewed` +Occurs every time the lock monitor renews an existing lock. -Event data: none \ No newline at end of file +Event data: none + +### `bitcoin_lock_monitor_new_lock` +Occurs every time the lock monitor creates a new lock. + +Event data: none + +### `bitcoin_lock_monitor_loop_failure` +Occurs every time the lock monitor loop fails. + +Event data: none + +### `bitcoin_lock_monitor_loop_success` +Occurs every time the lock monitor loop succeeds. + +Event data: none + +### `bitcoin_observing_loop_failure` +Occurs every time the bitcoin observing loop fails. + +Event data: none + +### `bitcoin_observing_loop_success` +Occurs every time the bitcoin observing loop succeeds. + +Event data: none diff --git a/lib/bitcoin/BitcoinProcessor.ts b/lib/bitcoin/BitcoinProcessor.ts index baf218eee..fd6e75713 100644 --- a/lib/bitcoin/BitcoinProcessor.ts +++ b/lib/bitcoin/BitcoinProcessor.ts @@ -20,6 +20,7 @@ import MongoDbBlockMetadataStore from './MongoDbBlockMetadataStore'; import MongoDbLockTransactionStore from './lock/MongoDbLockTransactionStore'; import MongoDbServiceStateStore from '../common/MongoDbServiceStateStore'; import MongoDbTransactionStore from '../common/MongoDbTransactionStore'; +import Monitor from './Monitor'; import RequestError from './RequestError'; import ResponseStatus from '../common/enums/ResponseStatus'; import ServiceInfoProvider from '../common/ServiceInfoProvider'; @@ -64,12 +65,12 @@ export default class BitcoinProcessor { /** The first Sidetree block in Bitcoin's blockchain. */ public readonly genesisBlockNumber: number; + /** Monitor of the running Bitcoin service. */ + public monitor: Monitor; + /** Store for the state of sidetree transactions. */ private readonly transactionStore: MongoDbTransactionStore; - /** Days of notice before the wallet is depleted of all funds */ - public lowBalanceNoticeDays: number; - private versionManager: VersionManager; /** Last seen block */ @@ -112,7 +113,6 @@ export default class BitcoinProcessor { BitcoinClient.convertBtcToSatoshis(config.bitcoinFeeSpendingCutoff), this.transactionStore); - this.lowBalanceNoticeDays = config.lowBalanceNoticeInDays || 28; this.serviceInfoProvider = new ServiceInfoProvider('bitcoin'); this.bitcoinClient = @@ -150,6 +150,8 @@ export default class BitcoinProcessor { BitcoinClient.convertBtcToSatoshis(valueTimeLockTransactionFeesInBtc), // Txn Fees amount in satoshis this.versionManager ); + + this.monitor = new Monitor(this.bitcoinClient); } /** @@ -536,17 +538,7 @@ export default class BitcoinProcessor { throw new RequestError(ResponseStatus.BadRequest, SharedErrorCode.SpendingCapPerPeriodReached); } - // Write a warning if the balance is running low const totalSatoshis = await this.bitcoinClient.getBalanceInSatoshis(); - - const estimatedBitcoinWritesPerDay = 6 * 24; - const lowBalanceAmount = this.lowBalanceNoticeDays * estimatedBitcoinWritesPerDay * transactionFee; - if (totalSatoshis < lowBalanceAmount) { - const daysLeft = Math.floor(totalSatoshis / (estimatedBitcoinWritesPerDay * transactionFee)); - Logger.error(`Low balance (${daysLeft} days remaining), please fund your wallet. Amount: >=${lowBalanceAmount - totalSatoshis} satoshis.`); - } - - // cannot make the transaction if (totalSatoshis < transactionFee) { const error = new Error(`Not enough satoshis to broadcast. Failed to broadcast anchor string ${anchorString}`); Logger.error(error); @@ -675,9 +667,9 @@ export default class BitcoinProcessor { await this.processTransactions(startingBlock); } - EventEmitter.emit(EventCode.BitcoinProcessorObservingLoopSuccessful); + EventEmitter.emit(EventCode.BitcoinObservingLoopSuccess); } catch (error) { - EventEmitter.emit(EventCode.BitcoinProcessorObservingLoopFailed); + EventEmitter.emit(EventCode.BitcoinObservingLoopFailure); Logger.error(error); } finally { this.pollTimeoutId = setTimeout(this.periodicPoll.bind(this), 1000 * interval, interval); @@ -766,7 +758,7 @@ export default class BitcoinProcessor { Logger.info(LogColor.lightBlue(`Reverting database to ${LogColor.green(lastKnownValidBlockHeight || 'genesis')} block...`)); await this.trimDatabasesToBlock(lastKnownValidBlockHeight); - EventEmitter.emit(EventCode.BitcoinProcessorDatabasesRevert, { blockHeight: lastKnownValidBlockHeight }); + EventEmitter.emit(EventCode.BitcoinDatabasesRevert, { blockHeight: lastKnownValidBlockHeight }); return lastKnownValidBlock; } diff --git a/lib/bitcoin/EventCode.ts b/lib/bitcoin/EventCode.ts index 37b5b4a7b..e00970bda 100644 --- a/lib/bitcoin/EventCode.ts +++ b/lib/bitcoin/EventCode.ts @@ -2,7 +2,12 @@ * Event codes used by Bitcoin Processor. */ export default { - BitcoinProcessorDatabasesRevert: 'bitcoin_processor_databases_revert', - BitcoinProcessorObservingLoopFailed: 'bitcoin_processor_observing_loop_failed', - BitcoinProcessorObservingLoopSuccessful: `bitcoin_processor_observing_loop_successful` + BitcoinDatabasesRevert: 'bitcoin_processor_databases_revert', + BitcoinLockMonitorLockReleased: `bitcoin_lock_monitor_lock_released`, + BitcoinLockMonitorLockRenewed: `bitcoin_lock_monitor_lock_renewed`, + BitcoinLockMonitorNewLock: `bitcoin_lock_monitor_new_lock`, + BitcoinLockMonitorLoopFailure: `bitcoin_lock_monitor_loop_failure`, + BitcoinLockMonitorLoopSuccess: `bitcoin_lock_monitor_loop_success`, + BitcoinObservingLoopFailure: 'bitcoin_observing_loop_failure', + BitcoinObservingLoopSuccess: `bitcoin_observing_loop_success` }; diff --git a/lib/bitcoin/Monitor.ts b/lib/bitcoin/Monitor.ts new file mode 100644 index 000000000..d8f8b57c3 --- /dev/null +++ b/lib/bitcoin/Monitor.ts @@ -0,0 +1,18 @@ +import BitcoinClient from './BitcoinClient'; + +/** + * Monitor for the running Bitcoin service. + */ +export default class Monitor { + + public constructor (private bitcoinClient: BitcoinClient) { } + + /** + * Gets the size of the operation queue. + */ + public async getWalletBalance (): Promise { + const walletBalanceInSatoshis = await this.bitcoinClient.getBalanceInSatoshis(); + const walletBalanceInBtc = walletBalanceInSatoshis / 100000000; + return { walletBalanceInBtc }; + } +} diff --git a/lib/bitcoin/lock/LockMonitor.ts b/lib/bitcoin/lock/LockMonitor.ts index 3ee47ff63..c056f1e19 100644 --- a/lib/bitcoin/lock/LockMonitor.ts +++ b/lib/bitcoin/lock/LockMonitor.ts @@ -1,6 +1,8 @@ import BitcoinClient from '../BitcoinClient'; import BitcoinLockTransactionModel from '../models/BitcoinLockTransactionModel'; import ErrorCode from '../ErrorCode'; +import EventCode from '../EventCode'; +import EventEmitter from '../../common/EventEmitter'; import LockIdentifier from '../models/LockIdentifierModel'; import LockIdentifierSerializer from './LockIdentifierSerializer'; import LockResolver from './LockResolver'; @@ -100,7 +102,9 @@ export default class LockMonitor { Logger.info(`Starting periodic polling for the lock monitor.`); await this.handlePeriodicPolling(); + EventEmitter.emit(EventCode.BitcoinLockMonitorLoopSuccess); } catch (e) { + EventEmitter.emit(EventCode.BitcoinLockMonitorLoopFailure); const message = `An error occurred during periodic poll: ${SidetreeError.stringify(e)}`; Logger.error(message); } finally { @@ -276,7 +280,10 @@ export default class LockMonitor { const height = await this.bitcoinClient.getCurrentBlockHeight(); const lockTransaction = await this.bitcoinClient.createLockTransaction(totalLockAmount, this.versionManager.getLockDurationInBlocks(height)); - return this.saveThenBroadcastTransaction(lockTransaction, SavedLockType.Create, desiredLockAmountInSatoshis); + const savedLockModel = await this.saveThenBroadcastTransaction(lockTransaction, SavedLockType.Create, desiredLockAmountInSatoshis); + EventEmitter.emit(EventCode.BitcoinLockMonitorNewLock); + + return savedLockModel; } /** @@ -311,6 +318,7 @@ export default class LockMonitor { // If we have gotten to here then we need to try renew. try { await this.renewLock(currentValueTimeLock, desiredLockAmountInSatoshis); + EventEmitter.emit(EventCode.BitcoinLockMonitorLockRenewed); } catch (e) { // If there is not enough balance for the relock then just release the lock. Let the next @@ -383,7 +391,10 @@ export default class LockMonitor { currentLockIdentifier.transactionId, currentLockDuration); - return this.saveThenBroadcastTransaction(releaseLockTransaction, SavedLockType.ReturnToWallet, desiredLockAmountInSatoshis); + const savedLockModel = await this.saveThenBroadcastTransaction(releaseLockTransaction, SavedLockType.ReturnToWallet, desiredLockAmountInSatoshis); + EventEmitter.emit(EventCode.BitcoinLockMonitorLockReleased); + + return savedLockModel; } private async saveThenBroadcastTransaction ( diff --git a/tests/bitcoin/BitcoinProcessor.spec.ts b/tests/bitcoin/BitcoinProcessor.spec.ts index d688b140c..bd15678fb 100644 --- a/tests/bitcoin/BitcoinProcessor.spec.ts +++ b/tests/bitcoin/BitcoinProcessor.spec.ts @@ -12,7 +12,6 @@ import BlockMetadataWithoutNormalizedFee from '../../lib/bitcoin/models/BlockMet import ErrorCode from '../../lib/bitcoin/ErrorCode'; import IBitcoinConfig from '../../lib/bitcoin/IBitcoinConfig'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; -import Logger from '../../lib/common/Logger'; import RequestError from '../../lib/bitcoin/RequestError'; import ResponseStatus from '../../lib/common/enums/ResponseStatus'; import ServiceVersionModel from '../../lib/common/models/ServiceVersionModel'; @@ -194,7 +193,6 @@ describe('BitcoinProcessor', () => { const bitcoinProcessor = new BitcoinProcessor(config); expect(bitcoinProcessor.genesisBlockNumber).toEqual(config.genesisBlockNumber); - expect(bitcoinProcessor.lowBalanceNoticeDays).toEqual(28); expect((bitcoinProcessor as any).config.transactionPollPeriodInSeconds).toEqual(60); expect((bitcoinProcessor as any).config.sidetreeTransactionPrefix).toEqual(config.sidetreeTransactionPrefix); expect(bitcoinProcessor['transactionStore']['databaseName']).toEqual(config.databaseName); @@ -230,7 +228,6 @@ describe('BitcoinProcessor', () => { const bitcoinProcessor = new BitcoinProcessor(config); expect(bitcoinProcessor.genesisBlockNumber).toEqual(config.genesisBlockNumber); - expect(bitcoinProcessor.lowBalanceNoticeDays).toEqual(28); expect((bitcoinProcessor as any).config.transactionPollPeriodInSeconds).toEqual(60); expect((bitcoinProcessor as any).config.sidetreeTransactionPrefix).toEqual(config.sidetreeTransactionPrefix); expect(bitcoinProcessor['transactionStore']['databaseName']).toEqual(config.databaseName); @@ -809,13 +806,10 @@ describe('BitcoinProcessor', () => { transactionFee: bitcoinFee, serializedTransactionObject: 'string' })); - const loggerErrorSpy = spyOn(Logger, 'error').and.callFake((message: string) => { - expect(message).toContain('fund your wallet'); - }); + await bitcoinProcessor.writeTransaction(hash, bitcoinFee); expect(getCoinsSpy).toHaveBeenCalled(); expect(broadcastSpy).toHaveBeenCalled(); - expect(loggerErrorSpy).toHaveBeenCalled(); expect(monitorAddSpy).toHaveBeenCalled(); done(); }); diff --git a/tests/bitcoin/Monitor.spec.ts b/tests/bitcoin/Monitor.spec.ts new file mode 100644 index 000000000..5b7809af5 --- /dev/null +++ b/tests/bitcoin/Monitor.spec.ts @@ -0,0 +1,19 @@ +import Monitor from '../../lib/bitcoin/Monitor'; + +describe('BitcoinFileReader', () => { + let monitor: Monitor; + beforeAll(() => { + const mockBitcoinClient = { getBalanceInSatoshis: () => {} }; + monitor = new Monitor(mockBitcoinClient as any); + }); + + describe('getWalletBalance', async () => { + it('should get wallet balance', async () => { + const mockBalance = 123; + spyOn(monitor['bitcoinClient'], 'getBalanceInSatoshis').and.returnValue(Promise.resolve(mockBalance)); + + const balance = await monitor.getWalletBalance(); + expect(balance).toEqual({ walletBalanceInBtc: 0.00000123 }); + }); + }); +});