UNPKG

libbwt

Version:

A library for programmatically controlling the Bitcoin Wallet Tracker daemons

166 lines (137 loc) 4.98 kB
const ffi = require('ffi-napi') , ref = require('ref-napi') , path = require('path') , EventEmitter = require('events') , debug = require('debug')('libbwt') const LIB_PATH = process.env.BWT_LIB || path.join(__dirname, 'libbwt') // Low-level private API const OK = 0 const shutdownPtr = ref.refType('void') const libbwt = ffi.Library(LIB_PATH, { bwt_start: [ 'int', [ 'string', 'pointer', 'pointer' ] ] , bwt_shutdown: [ 'int', [ shutdownPtr ] ] }) function bwt_start(options, init_cb, notify_cb, done) { const opt_json = JSON.stringify(options) , init_cb_ffi = ffi.Callback('void', [ shutdownPtr ], init_cb) , notify_cb_ffi = ffi.Callback('void', [ 'string', 'float', 'uint32', 'string' ], notify_cb) libbwt.bwt_start.async(opt_json, init_cb_ffi, notify_cb_ffi, function(err, code) { debug('stopped with', { err, code }) if (err) return done(err) if (code != OK) return done(new Error(`bwt failed with code ${code}`)) done(null) }) } function bwt_shutdown(shutdown_ptr) { const code = libbwt.bwt_shutdown(shutdown_ptr) shutdown_ptr.deref() if (code != OK) throw new Error(`bwt shutdown failed with code ${code}`) } // High-level public API class BwtDaemon extends EventEmitter { constructor(options) { super() if (options.sync_progress) this.on('progress:sync', take_prop(options, 'sync_progress')) if (options.scan_progress) this.on('progress:scan', take_prop(options, 'scan_progress')) this.options = normalize_options(options) this.ready = new Promise((resolve, reject) => { this.on('ready', _ => resolve(this)) this.on('error', reject) this.on('exit', err => { if (err) return this.emit('error', err) // this `reject` will be ignored if the daemon already started up successfully // and the promise was resolved, which can happen following a shutdown() call. // imagine this is wrapped in an `if (!promise.resolved)`. reject(new Error('daemon stopped while starting up')) }) }) } start() { if (this._started) throw new Error('daemon already started') this._started = true debug('starting with %O', { ...this.options, bitcoind_auth: '**SCRUBBED**' }); bwt_start(this.options, this._init.bind(this), this._notify.bind(this) , err => this.emit('exit', err /*null for successful exit*/)) return this.ready } shutdown() { debug('shutdown', this._shutdown_ptr != null) // we cannot shut down yet, mark for later termination if (!this._shutdown_ptr) return this._terminate = true bwt_shutdown(take_prop(this, '_shutdown_ptr')) } _init(shutdown_ptr) { this._shutdown_ptr = shutdown_ptr if (take_prop(this, '_terminate')) this.shutdown() } _notify(msg_type, progress_n, detail_n, detail_s) { debug('notify %s %s %s', msg_type, progress_n, detail_n, detail_s) switch (msg_type) { case 'error': this.emit('error', detail_s) break case 'progress:sync': const tip_time = new Date(detail_n*1000) this.emit('progress:sync', progress_n, tip_time) break case 'progress:scan': this.emit('progress:scan', progress_n, detail_n /*ETA in seconds*/) break case 'access_token': this.access_token = detail_s break case 'ready:http': this.http_addr = detail_s this.http_url = `http://${this.http_addr}/` break case 'ready:electrum': this.electrum_addr = detail_s break case 'ready': this.emit('ready') break default: debug('unknown msg', msg_type) } } } // optional 'new' exports.BwtDaemon = module.exports = function(opt) { return new BwtDaemon(opt) } exports._BwtDaemon = BwtDaemon exports.start = opt => new BwtDaemon(opt).start() // Utility function normalize_options(options) { if (options.rescan_since) { options.rescan_since = parse_timestamp(options.rescan_since) } if (take_prop(options, 'electrum') && !options.electrum_addr) { options.electrum_addr = '127.0.0.1:0' } if (take_prop(options, 'http') && !options.http_addr) { options.http_addr = '127.0.0.1:0' } if (!options.electrum_addr && !options.http_addr) { throw new Error('None of the bwt services are enabled') } // Delete nully options so that they get their default value Object.entries(options) .filter(([ _, val ]) => val == null) .forEach(([ key, _ ]) => delete options[key]) return options } function take_prop(obj, key) { const val = obj[key] delete obj[key] return val } function parse_timestamp(ts) { // Pass 'now' as is if (ts == 'now') return ts // Date objects if (ts.getTime) return ts.getTime()/1000|0 // Unix timestamp if (!isNaN(ts)) return +ts // Date string (e.g. YYYY-MM-DD) const dt = new Date(ts) if (!isNaN(dt.getTime())) return dt.getTime()/1000|0 throw new Error(`Invalid rescan since value: ${ts}`) }