UNPKG

@cityofzion/neo-js

Version:

Running NEO blockchain full node with Node.js and MongoDB.

239 lines (199 loc) 7.45 kB
import { EventEmitter } from 'events' import { Logger, LoggerOptions } from 'node-log-it' import { merge } from 'lodash' import { Mesh } from './mesh' import { NodeMeta } from './node' import { MemoryStorage } from '../storages/memory-storage' import { MongodbStorage } from '../storages/mongodb-storage' import C from '../common/constants' import { NeoValidator } from '../validators/neo-validator' const MODULE_NAME = 'Api' const DEFAULT_OPTIONS: ApiOptions = { insertToStorage: true, checkReadyIntervalMs: 200, loggerOptions: {}, } export interface ApiOptions { insertToStorage?: boolean checkReadyIntervalMs?: number loggerOptions?: LoggerOptions } interface StorageInsertPayload { method: string nodeMeta: NodeMeta | undefined result: any } export class Api extends EventEmitter { private mesh: Mesh private storage?: MemoryStorage | MongodbStorage private options: ApiOptions private logger: Logger private checkReadyIntervalId?: NodeJS.Timer constructor(mesh: Mesh, storage?: MemoryStorage | MongodbStorage, options: ApiOptions = {}) { super() // Associate required properties this.mesh = mesh this.storage = storage // Associate optional properties this.options = merge({}, DEFAULT_OPTIONS, options) this.validateOptionalParameters() // Bootstrapping this.logger = new Logger(MODULE_NAME, this.options.loggerOptions) this.checkMeshAndStorageReady() // Event handlers this.on('storage:insert', this.storageInsertHandler.bind(this)) this.logger.debug('constructor completes.') } async getBlockCount(): Promise<number> { this.logger.debug('getBlockCount triggered.') if (!this.storage) { this.logger.debug('No storage delegate detected.') return this.getBlockCountFromMesh() } let blockHeight: number | undefined try { blockHeight = await this.storage!.getHighestBlockHeight() return blockHeight } catch (err) { // Suppress error and continue } // Failed to fetch from storage, try mesh instead this.logger.debug('Cannot find result from storage delegate, attempt to fetch from mesh instead...') blockHeight = await this.getBlockCountFromMesh() this.logger.debug('Successfully fetch result from mesh.') this.emit('storage:insert', { method: C.rpc.getblockcount, result: blockHeight }) return blockHeight } async getBlock(height: number): Promise<object> { this.logger.debug('getBlock triggered. height:', height) NeoValidator.validateHeight(height) if (!this.storage) { this.logger.debug('No storage delegate detected.') return this.getBlockFromMesh(height) } let block: object | undefined try { block = await this.storage!.getBlock(height) return block } catch (err) { // Suppress error and continue this.logger.debug('Cannot find result from storage delegate. Error:', err.message) } // Failed to fetch from storage, try mesh instead this.logger.debug('Attempt to fetch from mesh instead...') const blockResponse: any = await this.getBlockAndNodeMetaFromMesh(height) this.logger.debug('Successfully fetch result from mesh.') block = blockResponse.block const nodeMeta = blockResponse.nodeMeta this.emit('storage:insert', { method: C.rpc.getblock, result: { height, block }, nodeMeta }) return block! } async getTransaction(transactionId: string): Promise<object> { this.logger.debug('getBlock triggered. transactionId:', transactionId) NeoValidator.validateTransactionId(transactionId) if (!this.storage) { this.logger.debug('No storage delegate detected.') return this.getTransactionFromMesh(transactionId) } let transaction: object | undefined try { transaction = await this.storage!.getTransaction(transactionId) return transaction } catch (err) { // Suppress error and continue this.logger.debug('Cannot find result from storage delegate. Error:', err.message) } // Failed to fetch from storage, try mesh instead this.logger.debug('Attempt to fetch from mesh instead...') transaction = await this.getTransactionFromMesh(transactionId) return transaction } close() { clearInterval(this.checkReadyIntervalId!) } private storageInsertHandler(payload: StorageInsertPayload) { if (!this.options.insertToStorage) { return } this.logger.debug('storageInsertHandler triggered.') if (payload.method === C.rpc.getblockcount) { this.storeBlockCount(payload) } else if (payload.method === C.rpc.getblock) { this.storeBlock(payload) } else { // TODO throw new Error('Not implemented.') } } private validateOptionalParameters() { // TODO } private checkMeshAndStorageReady() { this.logger.debug('checkMeshAndStorageReady triggered.') /** * The easiest implementation to asynchronously detects readiness * of multiple components, is to just periodically ping them until * all are stated to be ready. */ this.checkReadyIntervalId = setInterval(() => { const meshReady = this.mesh.isReady() const storageReady = this.storage ? this.storage.isReady() : true if (meshReady && storageReady) { this.emit('ready') clearInterval(this.checkReadyIntervalId!) } }, this.options.checkReadyIntervalMs!) } private storeBlockCount(payload: StorageInsertPayload) { if (this.storage) { const blockHeight = payload.result as number this.storage.setBlockCount(blockHeight) } } private storeBlock(payload: StorageInsertPayload) { if (this.storage) { const height = payload.result.height as number const block = payload.result.block as object const source = payload.nodeMeta ? payload.nodeMeta.endpoint : 'api:storeBlock' this.storage.setBlock(height, block, { source }) } } private async getBlockCountFromMesh(): Promise<number> { this.logger.debug('getBlockCountFromMesh triggered.') const highestNode = this.mesh.getHighestNode() if (highestNode && highestNode.blockHeight) { return highestNode.blockHeight } else { // TODO throw new Error('Edge case not implemented.') } } private async getBlockFromMesh(height: number): Promise<object> { this.logger.debug('getBlockFromMesh triggered.') const blockResponse: any = await this.getBlockAndNodeMetaFromMesh(height) return blockResponse.block } private async getBlockAndNodeMetaFromMesh(height: number): Promise<object> { this.logger.debug('getBlockAndNodeMetaFromMesh triggered.') const highestNode = this.mesh.getHighestNode() if (highestNode && highestNode.blockHeight) { const nodeMeta = highestNode.getNodeMeta() const block = await highestNode.getBlock(height) return { block, nodeMeta } } else { // TODO throw new Error('Edge case not implemented.') } } private async getTransactionFromMesh(transactionId: string): Promise<object> { this.logger.debug('getTransactionFromMesh triggered.') const highestNode = this.mesh.getHighestNode() if (highestNode && highestNode.blockHeight) { const transaction = await highestNode.getTransaction(transactionId) return transaction } else { // TODO throw new Error('Edge case not implemented.') } } }