Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83420b861b | ||
|
|
d9f511d97f | ||
|
|
1bfe3cc424 | ||
|
|
a810b22e80 | ||
|
|
567e23d30e | ||
|
|
47acb51149 | ||
|
|
b0bcbe3def | ||
|
|
76c0ea35e1 | ||
|
|
414d5fb1f4 | ||
|
|
fbc8597960 | ||
|
|
99ebcc649d | ||
|
|
f9a827e724 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -57,3 +57,6 @@ typings/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
#editors
|
||||
.idea/
|
||||
|
||||
|
||||
30
README.md
30
README.md
@ -12,22 +12,40 @@ Electrum Protocol Client for React Native
|
||||
* persistence (ping strategy and reconnection)
|
||||
* batch requests
|
||||
* works in RN and nodejs
|
||||
* both clearnet TCP and TLS
|
||||
* zero dependencies
|
||||
|
||||
## protocol spec
|
||||
|
||||
* https://electrumx.readthedocs.io/en/latest/PROTOCOL.html
|
||||
* https://electrumx-spesmilo.readthedocs.io/en/latest/protocol.html
|
||||
|
||||
## usage
|
||||
|
||||
Relies on `react-native-tcp` so it should be already installed and linked in RN project. `net` should be provided from outside, this library wont do `require('net')`.
|
||||
For RN it should be in `shim.js`:
|
||||
For Nodejs you can just provide standard modules `net` & `tls` to constructor explicitly, this
|
||||
library won't do `require('net')`.
|
||||
|
||||
```javascript
|
||||
global.net = require('react-native-tcp');
|
||||
const net = require('net');
|
||||
```
|
||||
|
||||
For nodejs it should be provided before usage:
|
||||
and then
|
||||
|
||||
```javascript
|
||||
global.net = require('net');
|
||||
const client = new ElectrumClient(net, false, 50001, 'electrum1.bluewallet.io', 'tcp');
|
||||
const ver = await client.initElectrum({ client: 'bluewallet', version: '1.4' });
|
||||
const balance = await client.blockchainScripthash_getBalance('716decbe1660861c3d93906cb1d98ee68b154fd4d23aed9783859c1271b52a9c');
|
||||
```
|
||||
|
||||
For React Native luckily we have `react-native-tcp-socket` which mimics `net` & `tls` pretty closely,
|
||||
one of the ways to shim it is via `package.json`:
|
||||
|
||||
```json
|
||||
"react-native": {
|
||||
"net": "react-native-tcp-socket",
|
||||
"tls": "react-native-tcp-socket"
|
||||
}
|
||||
```
|
||||
|
||||
# license
|
||||
|
||||
MIT
|
||||
|
||||
7
index.js
7
index.js
@ -3,8 +3,8 @@
|
||||
const Client = require('./lib/client');
|
||||
|
||||
class ElectrumClient extends Client {
|
||||
constructor(port, host, protocol, options) {
|
||||
super(port, host, protocol, options);
|
||||
constructor(net, tls, port, host, protocol, options) {
|
||||
super(net, tls, port, host, protocol, options);
|
||||
this.timeLastCall = 0;
|
||||
}
|
||||
|
||||
@ -134,6 +134,9 @@ class ElectrumClient extends Client {
|
||||
blockchainScripthash_subscribe(scripthash) {
|
||||
return this.request('blockchain.scripthash.subscribe', [scripthash]);
|
||||
}
|
||||
blockchainBlock_header(height) {
|
||||
return this.request('blockchain.block.header', [height]);
|
||||
}
|
||||
blockchainBlock_headers(start_height, count) {
|
||||
return this.request('blockchain.block.headeres', [start_height, count]);
|
||||
}
|
||||
|
||||
@ -1,126 +0,0 @@
|
||||
/**
|
||||
* Simple wrapper to mimick Socket class from NET package, since TLS package has slightly different API.
|
||||
* We implement several methods that TCP sockets are expected to have. We will proxy call them as soon as
|
||||
* real TLS socket will be created (TLS socket created after connection).
|
||||
*/
|
||||
class TlsSocketWrapper {
|
||||
constructor(tls) {
|
||||
this._tls = tls; // dependency injection lol
|
||||
this._socket = false;
|
||||
// defaults:
|
||||
this._timeout = 5000;
|
||||
this._encoding = 'utf8';
|
||||
this._keepAliveEneblad = true;
|
||||
this._keepAliveinitialDelay = 0;
|
||||
this._noDelay = true;
|
||||
this._listeners = {};
|
||||
}
|
||||
|
||||
setTimeout(timeout) {
|
||||
if (this._socket) this._socket.setTimeout(timeout);
|
||||
this._timeout = timeout;
|
||||
}
|
||||
|
||||
setEncoding(encoding) {
|
||||
if (this._socket) this._socket.setEncoding(encoding);
|
||||
this._encoding = encoding;
|
||||
}
|
||||
|
||||
setKeepAlive(enabled, initialDelay) {
|
||||
if (this._socket) this._socket.setKeepAlive(enabled, initialDelay);
|
||||
this._keepAliveEneblad = enabled;
|
||||
this._keepAliveinitialDelay = initialDelay;
|
||||
}
|
||||
|
||||
setNoDelay(noDelay) {
|
||||
if (this._socket) this._socket.setNoDelay(noDelay);
|
||||
this._noDelay = noDelay;
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
this._listeners[event] = this._listeners[event] || [];
|
||||
this._listeners[event].push(listener);
|
||||
}
|
||||
|
||||
removeListener(event, listener) {
|
||||
this._listeners[event] = this._listeners[event] || [];
|
||||
let newListeners = [];
|
||||
|
||||
let found = false;
|
||||
for (let savedListener of this._listeners[event]) {
|
||||
if (savedListener == listener) {
|
||||
// found our listener
|
||||
found = true;
|
||||
// we just skip it
|
||||
} else {
|
||||
// other listeners should go back to original array
|
||||
newListeners.push(savedListener);
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
this._listeners[event] = newListeners;
|
||||
} else {
|
||||
// something went wrong, lets just cleanup all listeners
|
||||
this._listeners[event] = [];
|
||||
}
|
||||
}
|
||||
|
||||
connect(port, host, callback) {
|
||||
// resulting TLSSocket extends <net.Socket>
|
||||
this._socket = this._tls.connect({ port: port, host: host, rejectUnauthorized: false }, () => {
|
||||
console.log('TLS Connected to ', host, port);
|
||||
return callback();
|
||||
});
|
||||
|
||||
// setting everything that was set to this proxy class
|
||||
|
||||
this._socket.setTimeout(this._timeout);
|
||||
this._socket.setEncoding(this._encoding);
|
||||
this._socket.setKeepAlive(this._keepAliveEneblad, this._keepAliveinitialDelay);
|
||||
this._socket.setNoDelay(this._noDelay);
|
||||
|
||||
// resubscribing to events on newly created socket so we could proxy them to already established listeners
|
||||
|
||||
this._socket.on('data', data => {
|
||||
this._passOnEvent('data', data);
|
||||
});
|
||||
this._socket.on('error', data => {
|
||||
this._passOnEvent('error', data);
|
||||
});
|
||||
this._socket.on('close', data => {
|
||||
this._passOnEvent('close', data);
|
||||
});
|
||||
this._socket.on('connect', data => {
|
||||
this._passOnEvent('connect', data);
|
||||
});
|
||||
this._socket.on('connection', data => {
|
||||
this._passOnEvent('connection', data);
|
||||
});
|
||||
}
|
||||
|
||||
_passOnEvent(event, data) {
|
||||
this._listeners[event] = this._listeners[event] || [];
|
||||
for (let savedListener of this._listeners[event]) {
|
||||
savedListener(data);
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
this._socket.emit(event, data);
|
||||
}
|
||||
|
||||
end() {
|
||||
this._socket.end();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._socket.destroy();
|
||||
}
|
||||
|
||||
write(data) {
|
||||
this._socket.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TlsSocketWrapper;
|
||||
@ -1,24 +1,21 @@
|
||||
'use strict';
|
||||
/**
|
||||
* expecting NET & TLS to be injected from outside:
|
||||
* for RN it should be in shim.js:
|
||||
* global.net = require('react-native-tcp');
|
||||
* global.tls = require('react-native-tcp/tls');
|
||||
* NET & TLS dependencies should be injected via constructor
|
||||
* for RN you can use react-native-tcp-socket
|
||||
*
|
||||
* for nodejs tests it should be provided before tests:
|
||||
* global.net = require('net');
|
||||
* global.tls = require('tls');
|
||||
* for nodejs it should be regular node's net & tls:
|
||||
* const net = require('net');
|
||||
* const tls = require('tls');
|
||||
* */
|
||||
let net = global.net;
|
||||
let tls = global.tls;
|
||||
const TIMEOUT = 5000;
|
||||
|
||||
const TlsSocketWrapper = require('./TlsSocketWrapper.js');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const util = require('./util');
|
||||
|
||||
class Client {
|
||||
constructor(port, host, protocol, options) {
|
||||
constructor(net, tls, port, host, protocol, options) {
|
||||
this.net = net;
|
||||
this.tls = tls;
|
||||
this.id = 0;
|
||||
this.port = port;
|
||||
this.host = host;
|
||||
@ -37,14 +34,15 @@ class Client {
|
||||
options = options || this._options;
|
||||
switch (protocol) {
|
||||
case 'tcp':
|
||||
this.conn = new net.Socket();
|
||||
this.conn = new this.net.Socket();
|
||||
break;
|
||||
case 'tls':
|
||||
case 'ssl':
|
||||
if (!tls) {
|
||||
if (!this.tls) {
|
||||
throw new Error('tls package could not be loaded');
|
||||
}
|
||||
this.conn = new TlsSocketWrapper(tls);
|
||||
this.connUnsecure = new this.net.Socket();
|
||||
this.conn = new this.tls.TLSSocket(this.connUnsecure, { rejectUnauthorized: false });
|
||||
break;
|
||||
default:
|
||||
throw new Error('unknown protocol');
|
||||
@ -76,13 +74,14 @@ class Client {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.status = 1;
|
||||
return this.connectSocket(this.conn, this.port, this.host);
|
||||
return this.connectSocket(this.connUnsecure || this.conn, this.port, this.host);
|
||||
}
|
||||
|
||||
connectSocket(conn, port, host) {
|
||||
port = +port;
|
||||
return new Promise((resolve, reject) => {
|
||||
const errorHandler = e => reject(e);
|
||||
conn.connect(port, host, () => {
|
||||
conn.connect({port, host}, () => {
|
||||
conn.removeListener('error', errorHandler);
|
||||
resolve();
|
||||
});
|
||||
@ -156,20 +155,34 @@ class Client {
|
||||
callback(null, msg.result || msg);
|
||||
}
|
||||
} else {
|
||||
console.log("Can't get callback");
|
||||
console.log("Electrum: can't get callback. Msg = " + JSON.stringify(msg));
|
||||
if (JSON.stringify(msg).includes("Batch limit exceeded")) {
|
||||
// since we dont know which batch failed (id is not provided), we cancel all requests.
|
||||
// this is better than hanging indefinitely
|
||||
Object.keys(this.callback_message_queue).forEach(key => {
|
||||
this.callback_message_queue[key](new Error('Batch limit exceeded'));
|
||||
delete this.callback_message_queue[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(body, n) {
|
||||
const msg = JSON.parse(body);
|
||||
if (msg instanceof Array) {
|
||||
this.response(msg);
|
||||
} else {
|
||||
if (msg.id !== void 0) {
|
||||
try {
|
||||
const msg = JSON.parse(body);
|
||||
if (msg instanceof Array) {
|
||||
this.response(msg);
|
||||
} else {
|
||||
this.subscribe.emit(msg.method, msg.params);
|
||||
if (msg.id !== void 0) {
|
||||
this.response(msg);
|
||||
} else {
|
||||
this.subscribe.emit(msg.method, msg.params);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.conn.end();
|
||||
this.conn.destroy();
|
||||
this.onClose(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
48
lib/util.js
48
lib/util.js
@ -9,26 +9,6 @@ const makeRequest = (exports.makeRequest = (method, params, id) => {
|
||||
});
|
||||
});
|
||||
|
||||
const createRecuesiveParser = (exports.createRecuesiveParser = (max_depth, delimiter) => {
|
||||
const MAX_DEPTH = max_depth;
|
||||
const DELIMITER = delimiter;
|
||||
const recursiveParser = (n, buffer, callback) => {
|
||||
if (buffer.length === 0) {
|
||||
return { code: 0, buffer: buffer };
|
||||
}
|
||||
if (n > MAX_DEPTH) {
|
||||
return { code: 1, buffer: buffer };
|
||||
}
|
||||
const xs = buffer.split(DELIMITER);
|
||||
if (xs.length === 1) {
|
||||
return { code: 0, buffer: buffer };
|
||||
}
|
||||
callback(xs.shift(), n);
|
||||
return recursiveParser(n + 1, xs.join(DELIMITER), callback);
|
||||
};
|
||||
return recursiveParser;
|
||||
});
|
||||
|
||||
const createPromiseResult = (exports.createPromiseResult = (resolve, reject) => {
|
||||
return (err, result) => {
|
||||
if (err) reject(err);
|
||||
@ -51,18 +31,30 @@ const createPromiseResultBatch = (exports.createPromiseResultBatch = (resolve, r
|
||||
|
||||
class MessageParser {
|
||||
constructor(callback) {
|
||||
this.buffer = '';
|
||||
this.parts = [];
|
||||
this.partsLen = 0;
|
||||
this.callback = callback;
|
||||
this.recursiveParser = createRecuesiveParser(20, '\n');
|
||||
}
|
||||
run(chunk) {
|
||||
this.buffer += chunk;
|
||||
let s = chunk;
|
||||
if (this.partsLen > 0) {
|
||||
this.parts.push(chunk);
|
||||
s = this.parts.join('');
|
||||
this.parts = [];
|
||||
this.partsLen = 0;
|
||||
}
|
||||
let start = 0;
|
||||
let n = 0;
|
||||
while (true) {
|
||||
const res = this.recursiveParser(0, this.buffer, this.callback);
|
||||
this.buffer = res.buffer;
|
||||
if (res.code === 0) {
|
||||
break;
|
||||
}
|
||||
const idx = s.indexOf('\n', start);
|
||||
if (idx === -1) break;
|
||||
this.callback(s.slice(start, idx), n++);
|
||||
start = idx + 1;
|
||||
}
|
||||
if (start < s.length) {
|
||||
const tail = start === 0 ? s : s.slice(start);
|
||||
this.parts.push(tail);
|
||||
this.partsLen += tail.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "electrum-client",
|
||||
"version": "1.2.0",
|
||||
"version": "3.1.1",
|
||||
"description": "Electrum protocol client for React Native & Node.js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "node test.js"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
@ -21,7 +21,7 @@
|
||||
"bitcoin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"contributors": [
|
||||
{ "name": "Yuki Akiyama" },
|
||||
|
||||
23
test.js
Normal file
23
test.js
Normal file
@ -0,0 +1,23 @@
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const assert = require('assert');
|
||||
|
||||
const ElectrumClient = require("./index");
|
||||
|
||||
(async () => {
|
||||
let client = new ElectrumClient(net, false, 50001, 'electrum1.bluewallet.io', 'tcp');
|
||||
let ver = await client.initElectrum({ client: 'bluewallet', version: '1.4' });
|
||||
let balance = await client.blockchainScripthash_getBalance('716decbe1660861c3d93906cb1d98ee68b154fd4d23aed9783859c1271b52a9c');
|
||||
assert.ok(balance)
|
||||
assert.ok(ver[0].toLowerCase().includes('electrum'));
|
||||
assert.strictEqual(balance.confirmed, 51432);
|
||||
|
||||
client = new ElectrumClient(net, tls, 443, 'electrum1.bluewallet.io', 'ssl');
|
||||
ver = await client.initElectrum({ client: 'bluewallet', version: '1.4' });
|
||||
balance = await client.blockchainScripthash_getBalance('716decbe1660861c3d93906cb1d98ee68b154fd4d23aed9783859c1271b52a9c');
|
||||
assert.ok(balance)
|
||||
assert.ok(ver[0].toLowerCase().includes('electrum'));
|
||||
assert.strictEqual(balance.confirmed, 51432);
|
||||
|
||||
process.exit(0);
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user