vue-blocklink
Version:
Vue support for the Blockchain Link browser extension
133 lines (132 loc) • 6.42 kB
JavaScript
import { AbiDecoder, intervalUtils, logUtils } from './utils';
import { marshaller, Web3Wrapper } from './0xw3w';
import { BlockParamLiteral, } from './types';
import { BlockAndLogStreamer } from 'ethereumjs-blockstream';
import { SubscriptionErrors } from './types';
import { filterUtils } from './utils/filter_utils';
const DEFAULT_BLOCK_POLLING_INTERVAL = 1000;
export class SubscriptionManager {
constructor(abi, prov) {
this.abi = abi;
this._web3Wrapper = new Web3Wrapper(prov);
this._filters = {};
this._filterCallbacks = {};
this._blockAndLogStreamerIfExists = undefined;
this._onLogAddedSubscriptionToken = undefined;
this._onLogRemovedSubscriptionToken = undefined;
}
static _onBlockAndLogStreamerError(isVerbose, err) {
if (isVerbose) {
logUtils.warn(err);
}
}
unsubscribeAll() {
const filterTokens = Object.keys(this._filterCallbacks);
filterTokens.forEach(filterToken => this.unsubscribe(filterToken));
}
unsubscribe(filterToken, err) {
if (this._filters[filterToken] === undefined) {
throw new Error(SubscriptionErrors.SubscriptionNotFound);
}
if (err !== undefined) {
const callback = this._filterCallbacks[filterToken];
callback(err, undefined);
}
delete this._filters[filterToken];
delete this._filterCallbacks[filterToken];
if (Object.keys(this._filters).length === 0) {
this._stopBlockAndLogStream();
}
}
subscribe(address, eventName, indexFilterValues, abi, callback, isVerbose = false, blockPollingIntervalMs) {
const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi);
if (this._blockAndLogStreamerIfExists === undefined) {
this._startBlockAndLogStream(isVerbose, blockPollingIntervalMs);
}
const filterToken = filterUtils.generateUUID();
this._filters[filterToken] = filter;
this._filterCallbacks[filterToken] = callback;
return filterToken;
}
async getLogsAsync(address, eventName, blockRange, indexFilterValues, abi) {
const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi, blockRange);
const logs = await this._web3Wrapper.getLogsAsync(filter);
const logsWithDecodedArguments = logs.map(this._tryToDecodeLogOrNoop.bind(this));
return logsWithDecodedArguments;
}
_tryToDecodeLogOrNoop(log) {
const abiDecoder = new AbiDecoder([this.abi]);
const logWithDecodedArgs = abiDecoder.tryToDecodeLogOrNoop(log);
return logWithDecodedArgs;
}
_onLogStateChanged(isRemoved, blockHash, rawLogs) {
const logs = rawLogs.map(rawLog => marshaller.unmarshalLog(rawLog));
logs.forEach(log => {
Object.entries(this._filters).forEach(([filterToken, filter]) => {
if (filterUtils.matchesFilter(log, filter)) {
const decodedLog = this._tryToDecodeLogOrNoop(log);
const logEvent = {
log: decodedLog,
isRemoved,
};
this._filterCallbacks[filterToken](null, logEvent);
}
});
});
}
_startBlockAndLogStream(isVerbose, blockPollingIntervalMs) {
if (this._blockAndLogStreamerIfExists !== undefined) {
throw new Error(SubscriptionErrors.SubscriptionAlreadyPresent);
}
this._blockAndLogStreamerIfExists = new BlockAndLogStreamer(this._blockstreamGetBlockOrNullAsync.bind(this), this._blockstreamGetLogsAsync.bind(this), SubscriptionManager._onBlockAndLogStreamerError.bind(this, isVerbose));
const catchAllLogFilter = {};
this._blockAndLogStreamerIfExists.addLogFilter(catchAllLogFilter);
const _blockPollingIntervalMs = blockPollingIntervalMs === undefined ? DEFAULT_BLOCK_POLLING_INTERVAL : blockPollingIntervalMs;
this._blockAndLogStreamIntervalIfExists = intervalUtils.setAsyncExcludingInterval(this._reconcileBlockAsync.bind(this), _blockPollingIntervalMs, SubscriptionManager._onBlockAndLogStreamerError.bind(this, isVerbose));
let isRemoved = false;
this._onLogAddedSubscriptionToken = this._blockAndLogStreamerIfExists.subscribeToOnLogsAdded(this._onLogStateChanged.bind(this, isRemoved));
isRemoved = true;
this._onLogRemovedSubscriptionToken = this._blockAndLogStreamerIfExists.subscribeToOnLogsRemoved(this._onLogStateChanged.bind(this, isRemoved));
}
async _blockstreamGetBlockOrNullAsync(hash) {
const shouldIncludeTransactionData = false;
const blockOrNull = await this._web3Wrapper.sendRawPayloadAsync({
method: 'eth_getBlockByHash',
params: [hash, shouldIncludeTransactionData],
});
return blockOrNull;
}
async _blockstreamGetLatestBlockOrNullAsync() {
const shouldIncludeTransactionData = false;
const blockOrNull = await this._web3Wrapper.sendRawPayloadAsync({
method: 'eth_getBlockByNumber',
params: [BlockParamLiteral.Latest, shouldIncludeTransactionData],
});
return blockOrNull;
}
async _blockstreamGetLogsAsync(filterOptions) {
const logs = await this._web3Wrapper.sendRawPayloadAsync({
method: 'eth_getLogs',
params: [filterOptions],
});
return logs;
}
_stopBlockAndLogStream() {
if (this._blockAndLogStreamerIfExists === undefined) {
throw new Error(SubscriptionErrors.SubscriptionNotFound);
}
this._blockAndLogStreamerIfExists.unsubscribeFromOnLogsAdded(this._onLogAddedSubscriptionToken);
this._blockAndLogStreamerIfExists.unsubscribeFromOnLogsRemoved(this._onLogRemovedSubscriptionToken);
intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamIntervalIfExists);
delete this._blockAndLogStreamerIfExists;
}
async _reconcileBlockAsync() {
const latestBlockOrNull = await this._blockstreamGetLatestBlockOrNullAsync();
if (latestBlockOrNull === null) {
return;
}
if (this._blockAndLogStreamerIfExists !== undefined) {
await this._blockAndLogStreamerIfExists.reconcileNewBlock(latestBlockOrNull);
}
}
}