Skip to content

Commit bec9b6b

Browse files
committed
Initial commit, migrated out of main rpeo
0 parents  commit bec9b6b

8 files changed

Lines changed: 572 additions & 0 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.so
2+
*.dll
3+
*.dylib
4+
node_modules
5+
dist

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2020-2021 Nadav Ivgi
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# bwt-daemon
2+
3+
A nodejs library for programmatically managing the Bitcoin Wallet Tracker Electrum RPC and HTTP API servers
4+
using the [`libbwt` C FFI interface](https://github.com/bwt-dev/libbwt).
5+
6+
> ⚠️ WARNING: This is an alpha preview, released to gather developers' feedback. It is not ready for general use.
7+
8+
### Install
9+
10+
```
11+
$ npm install bwt-daemon
12+
```
13+
14+
This will download the `libbwt` library for your platform.
15+
The currently supported platforms are Linux, Mac, Windows and ARMv7/8.
16+
17+
The hash of the downloaded library is verified against the
18+
[`SHA256SUMS`](https://github.com/shesek/bwt/blob/master/contrib/nodejs-bwt-daemon/SHA256SUMS)
19+
file that ships with the npm package.
20+
21+
The library comes with the electrum and http servers by default.
22+
If you're only interested in the Electrum server, you can install with `BWT_VARIANT=electrum_only npm install bwt-daemon`.
23+
This reduces the download size by ~1.6MB.
24+
25+
> Note: `bwt-daemon` uses [`ffi-napi`](https://github.com/node-ffi-napi/node-ffi-napi), which requires
26+
> a recent nodejs version. If you're running into errors during installation or segmentation faults,
27+
> try updating to a newer version.
28+
29+
### Use
30+
31+
Below is a minimally viable setup. If bitcoind is running locally on the default port, at the default datadir location
32+
and with cookie auth enabled (the default), this should Just Work™ \o/
33+
34+
```js
35+
import BwtDaemon from 'bwt-daemon'
36+
37+
const bwtd = await BwtDaemon({
38+
xpubs: [ 'xpub66...' ],
39+
electrum: true,
40+
}).start()
41+
42+
console.log('bwt electrum server ready on', bwtd.electrum_addr)
43+
```
44+
45+
With some more advanced options:
46+
47+
```js
48+
const bwtd = await BwtDaemon({
49+
// Network and Bitcoin Core RPC settings
50+
network: 'regtest',
51+
bitcoind_dir: '/home/satoshi/.bitcoin',
52+
bitcoind_url: 'http://127.0.0.1:9008/',
53+
bitcoind_wallet: 'bwt',
54+
55+
// Descriptors and xpubs to track
56+
descriptors: [ 'wpkh(tpub61.../0/*)' ],
57+
xpubs: [ 'tpub66...' ],
58+
59+
// Rescan since timestamp. Accepts unix timestamps, date strings, Date objects, or 'now' to look for new transactions only
60+
rescan_since: '2020-01-01',
61+
62+
// Enable HTTP and Electrum servers
63+
http: true,
64+
electrum: true,
65+
66+
// Bind on port 0 to use any available port (the default)
67+
electrum_addr: '127.0.0.1:0',
68+
http_addr: '127.0.0.1:0',
69+
70+
// Set the gap limit of watched unused addresses
71+
gap_limit: 100,
72+
73+
// Progress notifications for history scanning (a full rescan from genesis can take 20-30 minutes)
74+
progress: (type, progress, detail) => console.log('bwt %s progress %f%%', type, progress*100, detail),
75+
}).start()
76+
77+
// Get the assigned address/port for the Electrum/HTTP servers
78+
console.log('bwt electrum server ready on', bwtd.electrum_addr)
79+
console.log('bwt http server ready on', bwtd.http_url)
80+
81+
// Shutdown
82+
bwtd.shutdown()
83+
```
84+
85+
See [`example.js`](https://github.com/shesek/bwt/blob/master/contrib/nodejs-bwt-daemon/example.js) for an even more complete
86+
example, including connecting to the HTTP API.
87+
88+
The full list of options is available in the [libbwt documentation](https://github.com/bwt-dev/libbwt#config-options).
89+
The nodejs wrapper also provides the following additional options:
90+
91+
- `progress` - callback for progress update notifications, invoked with `(type, progress, detail)` (optional)
92+
- `electrum` - setting to `true` is an alias for `electrum_addr=127.0.0.1:0`
93+
- `http` - setting to `true` is an alias for `http_addr=127.0.0.1:0`
94+
95+
### License
96+
MIT

example.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const BwtDaemon = require('bwt-daemon')
2+
3+
;(async function(){
4+
const my_desc = 'wpkh(tpubD6NzVbkrYhZ4Ya1aR2od7JTGK6b44cwKhWzrvrTeTWFrzGokdAGHrZLK6BdYwpx9K7EoY38LzHva3SWwF8yRrXM9x9DQ3jCGKZKt1nQEz7n/0/*)';
5+
6+
const bwtd = await BwtDaemon({
7+
network: 'regtest',
8+
bitcoind_dir: '/tmp/bd1',
9+
bitcoind_wallet: 'bwt',
10+
descriptors: [ my_desc ],
11+
electrum: true,
12+
http: true,
13+
verbose: 2,
14+
progress: (type, progress, detail) => console.log('bwt progress %s %f%%', type, progress*100, detail),
15+
}).start()
16+
17+
console.log('bwt running', bwtd.electrum_addr, bwtd.http_addr)
18+
19+
// Connect to the HTTP API. Requires `npm install node-fetch`
20+
const fetch = require('node-fetch')
21+
const bwt = (...path) => fetch(bwtd.http_url + path.join('/')).then(r => r.json())
22+
23+
console.log('wallets:', await bwt('wallets'))
24+
console.log('address:', await bwt('wallet/qufmgwfu/10'))
25+
console.log('transactions:', await bwt('txs'))
26+
27+
setTimeout(_ => bwtd.shutdown(), 5000)
28+
})()
29+
.catch(console.error)

index.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const ffi = require('ffi-napi')
2+
, ref = require('ref-napi')
3+
, path = require('path')
4+
, EventEmitter = require('events')
5+
, debug = require('debug')('bwt-daemon')
6+
7+
const LIB_PATH = process.env.BWT_LIB || path.join(__dirname, 'libbwt')
8+
9+
// Low-level private API
10+
11+
const OK = 0
12+
13+
const shutdownPtr = ref.refType('void')
14+
15+
const libbwt = ffi.Library(LIB_PATH, {
16+
bwt_start: [ 'int', [ 'string', 'pointer', 'pointer' ] ]
17+
, bwt_shutdown: [ 'int', [ shutdownPtr ] ]
18+
})
19+
20+
function bwt_start(options, init_cb, notify_cb, done) {
21+
const opt_json = JSON.stringify(options)
22+
, init_cb_ffi = ffi.Callback('void', [ shutdownPtr ], init_cb)
23+
, notify_cb_ffi = ffi.Callback('void', [ 'string', 'float', 'uint32', 'string' ], notify_cb)
24+
25+
libbwt.bwt_start.async(opt_json, init_cb_ffi, notify_cb_ffi, function(err, code) {
26+
debug('stopped with', { err, code })
27+
if (err) return done(err)
28+
if (code != OK) return done(new Error(`bwt failed with code ${code}`))
29+
done(null)
30+
})
31+
}
32+
33+
function bwt_shutdown(shutdown_ptr) {
34+
const code = libbwt.bwt_shutdown(shutdown_ptr)
35+
shutdown_ptr.deref()
36+
if (code != OK) throw new Error(`bwt shutdown failed with code ${code}`)
37+
}
38+
39+
// High-level public API
40+
41+
class BwtDaemon extends EventEmitter {
42+
constructor(options) {
43+
super()
44+
45+
if (options.progress) this.on('progress', take_prop(options, 'progress'))
46+
this.options = normalize_options(options)
47+
48+
this.ready = new Promise((resolve, reject) => {
49+
this.on('ready', _ => resolve(this))
50+
this.on('error', reject)
51+
this.on('exit', err => {
52+
if (err) return this.emit('error', err)
53+
// this `reject` will be ignored if the daemon already started up successfully
54+
// and the promise was resolved, which can happen following a shutdown() call.
55+
// imagine this is wrapped in an `if (!promise.resolved)`.
56+
reject(new Error('daemon stopped while starting up'))
57+
})
58+
})
59+
}
60+
61+
start() {
62+
if (this._started) throw new Error('daemon already started')
63+
this._started = true
64+
65+
debug('starting with %O', { ...this.options, bitcoind_auth: '**SCRUBBED**' });
66+
bwt_start(this.options, this._init.bind(this), this._notify.bind(this)
67+
, err => this.emit('exit', err /*null for successful exit*/))
68+
return this.ready
69+
}
70+
71+
shutdown() {
72+
debug('shutdown', this._shutdown_ptr != null)
73+
// we cannot shut down yet, mark for later termination
74+
if (!this._shutdown_ptr) return this._terminate = true
75+
76+
bwt_shutdown(take_prop(this, '_shutdown_ptr'))
77+
}
78+
79+
_init(shutdown_ptr) {
80+
this._shutdown_ptr = shutdown_ptr
81+
if (take_prop(this, '_terminate')) this.shutdown()
82+
}
83+
84+
_notify(msg_type, progress_n, detail_n, detail_s) {
85+
debug('notify %s %s %s', msg_type, progress_n, detail_n, detail_s)
86+
switch (msg_type) {
87+
case 'error':
88+
this.emit('error', detail_s)
89+
break
90+
case 'progress:sync':
91+
this.emit('progress', 'sync', progress_n, { tip_time: detail_n })
92+
break
93+
case 'progress:scan':
94+
this.emit('progress', 'scan', progress_n, { eta: detail_n })
95+
break
96+
case 'ready:http':
97+
this.http_addr = detail_s
98+
this.http_url = `http://${this.http_addr}/`
99+
break
100+
case 'ready:electrum':
101+
this.electrum_addr = detail_s
102+
break
103+
case 'ready':
104+
this.emit('ready')
105+
break
106+
default: debug('unknown msg', msg_type)
107+
}
108+
}
109+
}
110+
111+
// optional 'new'
112+
exports = module.exports = function(opt) { return new BwtDaemon(opt) }
113+
exports.BwtDaemon = BwtDaemon
114+
exports.start = BwtDaemon.start = opt => new BwtDaemon(opt).start()
115+
116+
// Utility
117+
118+
function normalize_options(options) {
119+
if (options.rescan_since) {
120+
options.rescan_since = parse_timestamp(options.rescan_since)
121+
}
122+
if (take_prop(options, 'electrum') && !options.electrum_addr) {
123+
options.electrum_addr = '127.0.0.1:0'
124+
}
125+
if (take_prop(options, 'http') && !options.http_addr) {
126+
options.http_addr = '127.0.0.1:0'
127+
}
128+
129+
if (!options.electrum_addr && !options.http_addr) {
130+
throw new Error('None of the bwt services are enabled')
131+
}
132+
133+
// Delete nully options so that they get their default value
134+
Object.entries(options)
135+
.filter(([ _, val ]) => val == null)
136+
.forEach(([ key, _ ]) => delete options[key])
137+
138+
return options
139+
}
140+
141+
function take_prop(obj, key) {
142+
const val = obj[key]
143+
delete obj[key]
144+
return val
145+
}
146+
147+
function parse_timestamp(ts) {
148+
// Pass 'now' as is
149+
if (ts == 'now') return ts
150+
// Date objects
151+
if (ts.getTime) return ts.getTime()/1000|0
152+
// Unix timestamp
153+
if (!isNaN(ts)) return +ts
154+
// Date string (e.g. YYYY-MM-DD)
155+
const dt = new Date(ts)
156+
if (!isNaN(dt.getTime())) return dt.getTime()/1000|0
157+
158+
throw new Error(`Invalid rescan since value: ${ts}`)
159+
}

0 commit comments

Comments
 (0)