Compare commits

...

13 Commits

Author SHA1 Message Date
Carlos Marques
83420b861b perf: improve message parser 2026-04-27 14:54:50 +01:00
overtorment
d9f511d97f FIX: dont hang forever if electrum threw 'Batch limit exceeded' error 2025-05-16 16:53:49 +01:00
overtorment
1bfe3cc424 FIX: tls for nodejs 2024-02-05 09:07:06 +00:00
overtorment
a810b22e80 DOC 2024-02-04 14:42:32 +00:00
overtorment
567e23d30e REF: modernize, make it work with latest react-native-tcp-socket 2024-02-04 14:32:40 +00:00
Overtorment
47acb51149
Merge pull request #13 from limpbrains/header
ADD: add blockchainBlock_header
2023-07-29 16:14:03 +00:00
Ivan Vershigora
b0bcbe3def
fix: add blockchainBlock_header 2023-07-29 12:31:26 +01:00
Overtorment
76c0ea35e1
Merge pull request #8 from Zero-1729/patch-1
README: fixed broken url link
2021-07-26 13:30:43 +01:00
Overtorment
414d5fb1f4
Update README.md 2021-07-26 13:30:21 +01:00
Abubakar Nur Khalil
fbc8597960
README: fixed broken url link 2021-07-25 21:54:29 +01:00
Overtorment
99ebcc649d REF: net & tls dependency inject 2021-01-08 18:35:17 +00:00
Overtorment
f9a827e724 FIX: gracefully handling malformed server responses 2020-10-06 13:43:32 +01:00
Overtorment
99c75385f9 FIX: deprecated methods 2020-08-29 11:18:02 +01:00
8 changed files with 116 additions and 225 deletions

3
.gitignore vendored
View File

@ -57,3 +57,6 @@ typings/
# dotenv environment variables file
.env
#editors
.idea/

View File

@ -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

View File

@ -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;
}
@ -37,10 +37,8 @@ class ElectrumClient extends Client {
onClose() {
super.onClose();
const list = [
'server.peers.subscribe',
'blockchain.numblocks.subscribe',
'blockchain.headers.subscribe',
'blockchain.address.subscribe',
'blockchain.scripthash.subscribe',
];
list.forEach(event => this.subscribe.removeAllListeners(event));
setTimeout(() => {
@ -136,8 +134,8 @@ class ElectrumClient extends Client {
blockchainScripthash_subscribe(scripthash) {
return this.request('blockchain.scripthash.subscribe', [scripthash]);
}
blockchainBlock_getHeader(height) {
return this.request('blockchain.block.get_header', [height]);
blockchainBlock_header(height) {
return this.request('blockchain.block.header', [height]);
}
blockchainBlock_headers(start_height, count) {
return this.request('blockchain.block.headeres', [start_height, count]);
@ -145,8 +143,8 @@ class ElectrumClient extends Client {
blockchainEstimatefee(number) {
return this.request('blockchain.estimatefee', [number]);
}
blockchainHeaders_subscribe(raw) {
return this.request('blockchain.headers.subscribe', [raw || false]);
blockchainHeaders_subscribe() {
return this.request('blockchain.headers.subscribe', []);
}
blockchain_relayfee() {
return this.request('blockchain.relayfee', []);
@ -166,36 +164,6 @@ class ElectrumClient extends Client {
mempool_getFeeHistogram() {
return this.request('mempool.get_fee_histogram', []);
}
// ---------------------------------
// protocol 1.1 deprecated method
// ---------------------------------
blockchainUtxo_getAddress(tx_hash, index) {
return this.request('blockchain.utxo.get_address', [tx_hash, index]);
}
blockchainNumblocks_subscribe() {
return this.request('blockchain.numblocks.subscribe', []);
}
// ---------------------------------
// protocol 1.2 deprecated method
// ---------------------------------
blockchainBlock_getChunk(index) {
return this.request('blockchain.block.get_chunk', [index]);
}
blockchainAddress_getBalance(address) {
return this.request('blockchain.address.get_balance', [address]);
}
blockchainAddress_getHistory(address) {
return this.request('blockchain.address.get_history', [address]);
}
blockchainAddress_getMempool(address) {
return this.request('blockchain.address.get_mempool', [address]);
}
blockchainAddress_listunspent(address) {
return this.request('blockchain.address.listunspent', [address]);
}
blockchainAddress_subscribe(address) {
return this.request('blockchain.address.subscribe', [address]);
}
}
module.exports = ElectrumClient;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -1,10 +1,10 @@
{
"name": "electrum-client",
"version": "1.1.4",
"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
View 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);
})();