Skip to content

Commit

Permalink
feat(ref-imp): added bitcoin lock monitor events
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai committed Mar 16, 2021
1 parent 51b8cf8 commit 009202d
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 35 deletions.
37 changes: 31 additions & 6 deletions docs/bitcoin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
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
26 changes: 9 additions & 17 deletions lib/bitcoin/BitcoinProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -150,6 +150,8 @@ export default class BitcoinProcessor {
BitcoinClient.convertBtcToSatoshis(valueTimeLockTransactionFeesInBtc), // Txn Fees amount in satoshis
this.versionManager
);

this.monitor = new Monitor(this.bitcoinClient);
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
11 changes: 8 additions & 3 deletions lib/bitcoin/EventCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
};
18 changes: 18 additions & 0 deletions lib/bitcoin/Monitor.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
const walletBalanceInSatoshis = await this.bitcoinClient.getBalanceInSatoshis();
const walletBalanceInBtc = walletBalanceInSatoshis / 100000000;
return { walletBalanceInBtc };
}
}
15 changes: 13 additions & 2 deletions lib/bitcoin/lock/LockMonitor.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
8 changes: 1 addition & 7 deletions tests/bitcoin/BitcoinProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
Expand Down
19 changes: 19 additions & 0 deletions tests/bitcoin/Monitor.spec.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
});

0 comments on commit 009202d

Please sign in to comment.