Compare commits

...

46 Commits

Author SHA1 Message Date
softsimon
d533c8b216
Bump version 2023-03-23 15:50:09 +09:00
softsimon
d8547ee1a1
Merge pull request #4 from mempool/nymkappa/increase-timeout
Increase timeout to 60 seconds
2023-03-23 15:49:49 +09:00
nymkappa
691d4ceec9 Increase timeout to 60 seconds 2023-03-23 06:47:28 +00:00
softsimon
41872e548b
Merge pull request #3 from tiero/master
Remove references to React Native in readme & pkg.json
2022-11-26 19:03:50 +09:00
Marco Argentieri
47b4b11dae
Update package.json 2022-11-25 09:52:31 -06:00
Marco Argentieri
afafe337fe
Update README.md 2022-11-25 09:50:57 -06:00
softsimon
0dedfdb1b5 Make lib rely on tls/net internally. 2020-12-29 14:13:55 +07:00
softsimon
be9541b0cb Tagging package.json with @mempool namespace. 2020-12-29 00:38:21 +07:00
Dan Janosik
d98e929028
configurable ping period with sensible default (2m instead of 5s) 2020-07-11 22:17:53 -04:00
Dan Janosik
344ac93946
version bump 2020-07-03 15:08:42 -04:00
Dan Janosik
a5b158a954
improvements/simplifications to callbacks/logging 2020-07-03 15:08:11 -04:00
Dan Janosik
53dc21ca13
backtrack broken logging change 2020-07-03 11:47:49 -04:00
Dan Janosik
595475e8d2
minor error handling/logging tweaks 2020-07-03 11:45:55 -04:00
Dan Janosik
0dec5c9cdd
Remove "debug" dependency; add simple/configurable log indirection 2020-07-02 14:44:07 -04:00
Dan Janosik
6fefbd88f1
Merge branch 'master' of github.com:janoside/electrum-client 2020-07-02 14:13:54 -04:00
Dan Janosik
bedf539fdf
typo 2020-07-02 14:13:45 -04:00
Dan Janosik
b3a6fecea1
Update README.md
rename. add "based on" for BlueWallet.
2020-07-02 18:06:32 +00:00
Dan Janosik
d263d2627b
spaces -> tabs 2020-07-02 14:05:13 -04:00
Dan Janosik
65a717ddef
package.json cleanup, version bump 2020-07-02 14:02:25 -04:00
Dan Janosik
5408cc301b
Externally configurable callbacks: afterConnect, afterClose 2020-07-02 12:05:23 -04:00
Dan Janosik
bd4d5f491e
Carry persistence policy across reconnects 2020-07-02 12:04:11 -04:00
Dan Janosik
5b3a2b728d
Configurable reconnect attempt period 2020-07-02 12:00:28 -04:00
Dan Janosik
59583ae7f7
Handle connection errors 2020-07-01 10:25:43 -04:00
Dan Janosik
2f8121346a
log message cleanup 2020-06-25 14:26:25 -04:00
Dan Janosik
3ba95c3737
Typo 2020-06-25 14:23:52 -04:00
Dan Janosik
138d2b8bf0
use "debug" for logging instead of console 2020-06-25 14:10:38 -04:00
Overtorment
cc018effaf REF: reconnect improvements; remove unused code 2020-06-12 15:47:10 +01:00
Overtorment
d1baea7ecb FIX: minor 2020-06-11 18:03:04 +01:00
Overtorment
2a5bb11dd9 FIX: minor improvements 2020-03-27 19:02:44 +00:00
Overtorment
4bbd13f3c2 FIX: initSocket() without arguments 2020-03-27 13:47:58 +00:00
Overtorment
aa73cd8bb3 added TLS support 2020-03-26 15:04:02 +00:00
Overtorment
609ca3bd58 refactoring before adding TLS support 2020-03-26 10:36:09 +00:00
Overtorment
d194ff6919 ADD: Scripthash_getHistoryBatch & Transaction_getBatch 2019-06-09 00:31:49 +01:00
Overtorment
3052638dcf DOC 2019-05-23 13:31:14 +01:00
Overtorment
5e413bf471 REF: cleanup 2019-05-23 13:11:04 +01:00
Overtorment
720c70ef99 ADD: batch fetch UTXO 2019-05-23 12:52:36 +01:00
Overtorment
989d785b23
Update README.md 2019-05-12 21:36:35 +01:00
Overtorment
7017952513 ADD: basic batching support and batched getBalance 2019-05-12 21:35:15 +01:00
Overtorment
59712b3b7d made it fork for RN/nodejs test env 2019-01-05 13:08:32 +00:00
Overtorment
232e113b35 Add electrum config for server version, ping strategy and reconnection 2019-01-05 12:07:35 +00:00
Yuki Akiyama
d54b794583
Merge pull request #7 from you21979/addmethod
Addmethod
2018-03-12 16:37:50 +09:00
you21979
17586c9723 fix example 2018-03-12 16:35:42 +09:00
Yuki Akiyama
0af5bab669
Update README.md 2018-03-09 00:12:55 +09:00
you21979
e612c206ae inc ver 2018-03-08 20:40:42 +09:00
you21979
5fb46ad687 update protocol ver 1.2 2018-03-08 20:39:54 +09:00
you21979
e57244914c update document 2018-02-09 19:41:41 +09:00
16 changed files with 687 additions and 568 deletions

View File

@ -1,46 +1,23 @@
# node-electrum-client
# electrum-client
Electrum Protocol Client for Node.js
Electrum Protocol Client for node.js.
## what is this
# based on
https://electrum.org/
* https://github.com/you21979/node-electrum-client
* https://github.com/7kharov/node-electrum-client
* https://github.com/BlueWallet/rn-electrum-client
electrum is bitcoin wallet service.
This is a library of Node.js that can communicate with the electrum(x) server.
# features
## install
* persistence (ping strategy and reconnection)
* batch requests
* works in nodejs
```
npm i electrum-client
```
## protocol spec
## spec
* http://docs.electrum.org/en/latest/protocol.html
* TCP / TLS
* JSON-RPC
* Subscribe Message
* High Performance Message
* no dependency for other library
* https://electrumx.readthedocs.io/en/latest/PROTOCOL.html
## usage
```
const ElectrumCli = require('electrum-client')
const main = async () => {
const ecl = new ElectrumCli(995, 'btc.smsys.me', 'tls') // tcp or tls
await ecl.connect() // connect(promise)
ecl.subscribe.on('blockchain.headers.subscribe', (v) => console.log(v)) // subscribe message(EventEmitter)
try{
const ver = await ecl.server_version("2.7.11", "1.0") // json-rpc(promise)
console.log(ver)
}catch(e){
console.log(e)
}
await ecl.close() // disconnect(promise)
}
main()
```
Relies on `net` so will only run in NodeJS environment.

View File

@ -1,23 +0,0 @@
const ElectrumClient = require('..')
const peers = require('electrum-host-parse').getDefaultPeers("BitcoinSegwit").filter(v => v.ssl)
const getRandomPeer = () => peers[peers.length * Math.random() | 0]
const main = async () => {
const peer = getRandomPeer()
console.log('begin connection: ' + JSON.stringify(peer))
const ecl = new ElectrumClient(peer.ssl, peer.host, 'ssl')
await ecl.connect()
try{
const ver = await ecl.server_version("2.7.11", "1.0")
console.log(ver)
const balance = await ecl.blockchainAddress_getBalance("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
console.log(balance)
const unspent = await ecl.blockchainAddress_listunspent("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
console.log(unspent)
}catch(e){
console.log(e)
}
await ecl.close()
}
main().catch(console.log)

View File

@ -1,25 +0,0 @@
const Client = require("../lib/electrum_cli")
const proc = async(cl) => {
try{
const version = await cl.server_version("2.7.11", "1.0")
console.log(version)
const balance = await cl.blockchainAddress_getBalance("MS43dMzRKfEs99Q931zFECfUhdvtWmbsPt")
console.log(balance)
const utxo = await cl.blockchainAddress_listunspent("MS43dMzRKfEs99Q931zFECfUhdvtWmbsPt")
console.log(utxo)
}catch(e){
console.log(e)
}
}
const main = async(port, host) => {
const cl = new Client(port, host);
await cl.connect()
for(let i = 0; i<100; ++i){
await proc(cl)
}
await cl.close()
}
main(4444, "localhost")

View File

@ -1,40 +0,0 @@
'use strict';
const ElectrumClient = require('..');
const createRaiiClient = (port, host, protocol, options) => {
return (params, promise) => {
const name = params.join(':')
const client = new ElectrumClient(port, host, protocol, options)
console.time(name)
return client.connect().then( () => {
return promise(client)
}).catch( e => {
client.close()
console.timeEnd(name)
throw e
}).then( res => {
client.close()
console.timeEnd(name)
return res
})
}
}
const main = async(hex) => {
const hosts = ['electrum-mona.bitbank.cc', 'electrumx.tamami-foundation.org']
const host = hosts[Math.floor(Math.random() * hosts.length)]
const connect = createRaiiClient(50001, host, 'tcp')
await connect(['blockchainTransaction_broadcast', hex], async(client) => {
const ver = await client.server_version('2.7.11', '1.0')
console.log(ver)
const result = await client.blockchainTransaction_broadcast(hex)
console.log(result)
})
}
const getopt = () => {
return process.argv.slice(2)[0]
}
main(getopt()).catch(console.log)

View File

@ -1,40 +0,0 @@
'use strict';
const ElectrumClient = require('..');
const createRaiiClient = (port, host, protocol, options) => {
return (params, promise) => {
const name = params.join(':')
const client = new ElectrumClient(port, host, protocol, options)
console.time(name)
return client.connect().then( () => {
return promise(client)
}).catch( e => {
client.close()
console.timeEnd(name)
throw e
}).then( res => {
client.close()
console.timeEnd(name)
return res
})
}
}
const main = async(hex) => {
const hosts = ['electrum-mona.bitbank.cc', 'electrumx.tamami-foundation.org']
const host = hosts[Math.floor(Math.random() * hosts.length)]
const connect = createRaiiClient(50000, host, 'tcp')
await connect(['blockchainTransaction_broadcast', hex], async(client) => {
const ver = await client.server_version('2.7.11', '1.0')
console.log(ver)
const result = await client.blockchainTransaction_broadcast(hex)
console.log(result)
})
}
const getopt = () => {
return process.argv.slice(2)[0]
}
main(getopt()).catch(console.log)

View File

@ -1,22 +0,0 @@
const ElectrumClient = require('..')
const main = async () => {
const ecl = new ElectrumClient(50002, 'bitcoins.sk', 'tls')
await ecl.connect()
try{
const ver = await ecl.server_version("3.0.5", "1.1")
console.log(ver)
const balance = await ecl.blockchainScripthash_getBalance("676ca8550e249787290b987e12cebdb2e9b26d88c003d836ffb1cb03ffcbea7c")
console.log(balance)
const unspent = await ecl.blockchainScripthash_listunspent("676ca8550e249787290b987e12cebdb2e9b26d88c003d836ffb1cb03ffcbea7c")
console.log(unspent)
const history = await ecl.blockchainScripthash_getHistory("676ca8550e249787290b987e12cebdb2e9b26d88c003d836ffb1cb03ffcbea7c")
console.log(history)
const mempool = await ecl.blockchainScripthash_getMempool("676ca8550e249787290b987e12cebdb2e9b26d88c003d836ffb1cb03ffcbea7c")
console.log(mempool)
}catch(e){
console.log(e)
}
await ecl.close()
}
main().catch(console.log)

View File

@ -1,18 +0,0 @@
const ElectrumClient = require('..')
const main = async () => {
const ecl = new ElectrumClient(995, 'btc.smsys.me', 'tls')
await ecl.connect()
try{
const ver = await ecl.server_version("2.7.11", "1.0")
console.log(ver)
const balance = await ecl.blockchainAddress_getBalance("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
console.log(balance)
const unspent = await ecl.blockchainAddress_listunspent("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
console.log(unspent)
}catch(e){
console.log(e)
}
await ecl.close()
}
main().catch(console.log)

View File

@ -1,30 +0,0 @@
const ElectrumClient = require('..')
const sleep = (ms) => new Promise((resolve,_) => setTimeout(() => resolve(), ms))
const main = async () => {
try{
const ecl = new ElectrumClient(50002, 'bitcoins.sk', 'tls')
ecl.subscribe.on('server.peers.subscribe', console.log)
ecl.subscribe.on('blockchain.numblocks.subscribe', console.log)
ecl.subscribe.on('blockchain.headers.subscribe', console.log)
ecl.subscribe.on('blockchain.address.subscribe', console.log)
ecl.subscribe.on('blockchain.scripthash.subscribe', console.log)
await ecl.connect()
await ecl.server_version("3.0.5", "1.1")
const p1 = await ecl.serverPeers_subscribe()
const p2 = await ecl.blockchainHeaders_subscribe()
// Note: blockchain.numblocks.subscribe is deprecated in protocol version 1.1
const p3 = await ecl.blockchainAddress_subscribe('1BK45iaPrrd26gKagrXytvz6anrj3hQ2pQ')
// Subscribe to corresponding scripthash for the above address
const p4 = await ecl.blockchainScripthash_subscribe('f3aa57a41424146327e5c88c25db8953dd16c6ab6273cdb74a4404ed4d0f5714')
while(true){
await sleep(1000)
const ver = await ecl.server_version("3.0.5", "1.1")
}
await ecl.close()
}catch(e){
console.log("error")
console.log(e)
}
}
main()

267
index.js
View File

@ -1 +1,266 @@
module.exports = require('./lib/electrum_client');
'use strict';
const Client = require('./lib/client');
class ElectrumClient extends Client {
constructor(port, host, protocol, options, callbacks) {
super(port, host, protocol, options, callbacks);
this.onConnectCallback = (callbacks && callbacks.onConnect) ? callbacks.onConnect : null;
this.onCloseCallback = (callbacks && callbacks.onClose) ? callbacks.onClose : null;
this.onLogCallback = (callbacks && callbacks.onLog) ? callbacks.onLog : function(str) {
console.log(str);
};
this.timeLastCall = 0;
}
initElectrum(electrumConfig, persistencePolicy = { retryPeriod: 10000, maxRetry: 1000, pingPeriod: 120000, callback: null }) {
this.persistencePolicy = persistencePolicy;
this.electrumConfig = electrumConfig;
this.timeLastCall = 0;
return new Promise((resolve, reject) => {
this.connect().then(() => {
this.server_version(this.electrumConfig.client, this.electrumConfig.version).then((versionInfo) => {
this.versionInfo = versionInfo;
if (this.onConnectCallback != null) {
this.onConnectCallback(this, this.versionInfo);
}
resolve(this);
}).catch((err) => {
reject(err);
});
}).catch((err) => {
reject(err);
});
});
}
// Override parent
request(method, params) {
this.timeLastCall = new Date().getTime();
const parentPromise = super.request(method, params);
return parentPromise.then(response => {
this.keepAlive();
return response;
});
}
requestBatch(method, params, secondParam) {
this.timeLastCall = new Date().getTime();
const parentPromise = super.requestBatch(method, params, secondParam);
return parentPromise.then(response => {
this.keepAlive();
return response;
});
}
onClose() {
super.onClose();
const list = [
'server.peers.subscribe',
'blockchain.numblocks.subscribe',
'blockchain.headers.subscribe',
'blockchain.address.subscribe',
];
list.forEach(event => this.subscribe.removeAllListeners(event));
var retryPeriod = 10000;
if (this.persistencePolicy != null && this.persistencePolicy.retryPeriod > 0) {
retryPeriod = this.persistencePolicy.retryPeriod;
}
if (this.onCloseCallback != null) {
this.onCloseCallback(this);
}
setTimeout(() => {
if (this.persistencePolicy != null && this.persistencePolicy.maxRetry > 0) {
this.reconnect().catch((err) => {
this.onError(err);
});
this.persistencePolicy.maxRetry -= 1;
} else if (this.persistencePolicy != null && this.persistencePolicy.callback != null) {
this.persistencePolicy.callback();
} else if (this.persistencePolicy == null) {
this.reconnect().catch((err) => {
this.onError(err);
});
}
}, retryPeriod);
}
// ElectrumX persistancy
keepAlive() {
if (this.timeout != null) {
clearTimeout(this.timeout);
}
var pingPeriod = 120000;
if (this.persistencePolicy != null && this.persistencePolicy.pingPeriod > 0) {
pingPeriod = this.persistencePolicy.pingPeriod;
}
this.timeout = setTimeout(() => {
if (this.timeLastCall !== 0 && new Date().getTime() > this.timeLastCall + pingPeriod) {
this.server_ping().catch((reason) => {
this.log('Keep-Alive ping failed: ', reason);
});
}
}, pingPeriod);
}
close() {
super.close();
if (this.timeout != null) {
clearTimeout(this.timeout);
}
this.reconnect = this.reconnect = this.onClose = this.keepAlive = () => {}; // dirty hack to make it stop reconnecting
}
reconnect() {
this.log("Electrum attempting reconnect...");
this.initSocket();
if (this.persistencePolicy != null) {
return this.initElectrum(this.electrumConfig, this.persistencePolicy);
} else {
return this.initElectrum(this.electrumConfig);
}
}
log(str) {
this.onLogCallback(str);
}
// ElectrumX API
server_version(client_name, protocol_version) {
return this.request('server.version', [client_name, protocol_version]);
}
server_banner() {
return this.request('server.banner', []);
}
server_features() {
return this.request('server.features', []);
}
server_ping() {
return this.request('server.ping', []);
}
server_addPeer(features) {
return this.request('server.add_peer', [features]);
}
serverDonation_address() {
return this.request('server.donation_address', []);
}
serverPeers_subscribe() {
return this.request('server.peers.subscribe', []);
}
blockchainAddress_getProof(address) {
return this.request('blockchain.address.get_proof', [address]);
}
blockchainScripthash_getBalance(scripthash) {
return this.request('blockchain.scripthash.get_balance', [scripthash]);
}
blockchainScripthash_getBalanceBatch(scripthash) {
return this.requestBatch('blockchain.scripthash.get_balance', scripthash);
}
blockchainScripthash_listunspentBatch(scripthash) {
return this.requestBatch('blockchain.scripthash.listunspent', scripthash);
}
blockchainScripthash_getHistory(scripthash) {
return this.request('blockchain.scripthash.get_history', [scripthash]);
}
blockchainScripthash_getHistoryBatch(scripthash) {
return this.requestBatch('blockchain.scripthash.get_history', scripthash);
}
blockchainScripthash_getMempool(scripthash) {
return this.request('blockchain.scripthash.get_mempool', [scripthash]);
}
blockchainScripthash_listunspent(scripthash) {
return this.request('blockchain.scripthash.listunspent', [scripthash]);
}
blockchainScripthash_subscribe(scripthash) {
return this.request('blockchain.scripthash.subscribe', [scripthash]);
}
blockchainBlock_getHeader(height) {
return this.request('blockchain.block.get_header', [height]);
}
blockchainBlock_headers(start_height, count) {
return this.request('blockchain.block.headeres', [start_height, count]);
}
blockchainEstimatefee(number) {
return this.request('blockchain.estimatefee', [number]);
}
blockchainHeaders_subscribe(raw) {
return this.request('blockchain.headers.subscribe', [raw || false]);
}
blockchain_relayfee() {
return this.request('blockchain.relayfee', []);
}
blockchainTransaction_broadcast(rawtx) {
return this.request('blockchain.transaction.broadcast', [rawtx]);
}
blockchainTransaction_get(tx_hash, verbose) {
return this.request('blockchain.transaction.get', [tx_hash, verbose || false]);
}
blockchainTransaction_getBatch(tx_hash, verbose) {
return this.requestBatch('blockchain.transaction.get', tx_hash, verbose);
}
blockchainTransaction_getMerkle(tx_hash, height) {
return this.request('blockchain.transaction.get_merkle', [tx_hash, height]);
}
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;

125
lib/TlsSocketWrapper.js Normal file
View File

@ -0,0 +1,125 @@
/**
* 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 }, () => {
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,99 +1,198 @@
'use strict'
const EventEmitter = require('events').EventEmitter
const util = require('./util')
const initSocket = require('./init_socket')
const connectSocket = require('./connect_socket')
'use strict';
let net = require('net');
let tls = require('tls');
const TIMEOUT = 60000;
class Client{
constructor(port, host, protocol = 'tcp', options = void 0){
this.id = 0;
this.port = port
this.host = host
this.callback_message_queue = {}
this.subscribe = new EventEmitter()
this.conn = initSocket(this, protocol, options)
this.mp = new util.MessageParser((body, n) => {
this.onMessage(body, n)
})
this.status = 0
}
const TlsSocketWrapper = require('./TlsSocketWrapper.js');
const EventEmitter = require('events').EventEmitter;
const util = require('./util');
connect(){
if(this.status) {
return Promise.resolve()
}
this.status = 1
return connectSocket(this.conn, this.port, this.host)
}
class Client {
close(){
if(!this.status) {
return
}
this.conn.end()
this.conn.destroy()
this.status = 0
}
constructor(port, host, protocol, options, callbacks) {
this.id = 0;
this.port = port;
this.host = host;
this.callback_message_queue = {};
this.subscribe = new EventEmitter();
this.mp = new util.MessageParser((body, n) => {
this.onMessage(body, n);
});
this._protocol = protocol; // saving defaults
this._options = options;
request(method, params){
if(!this.status) {
return Promise.reject(new Error('ESOCKET'))
}
return new Promise((resolve, reject) => {
const id = ++this.id;
const content = util.makeRequest(method, params, id);
this.callback_message_queue[id] = util.createPromiseResult(resolve, reject);
this.conn.write(content + '\n');
})
}
this.onErrorCallback = (callbacks && callbacks.onError) ? callbacks.onError : null;
response(msg){
const callback = this.callback_message_queue[msg.id]
if(callback){
delete this.callback_message_queue[msg.id]
if(msg.error){
callback(msg.error)
}else{
callback(null, msg.result)
}
}else{
; // can't get callback
}
}
this.initSocket(protocol, options);
}
onMessage(body, n){
const msg = JSON.parse(body)
if(msg instanceof Array){
; // don't support batch request
} else {
if(msg.id !== void 0){
this.response(msg)
}else{
this.subscribe.emit(msg.method, msg.params)
}
}
}
initSocket(protocol, options) {
protocol = protocol || this._protocol;
options = options || this._options;
switch (protocol) {
case 'tcp':
this.conn = new net.Socket();
break;
case 'tls':
case 'ssl':
if (!tls) {
throw new Error("Package 'tls' not available");
}
onConnect(){
}
this.conn = new TlsSocketWrapper(tls);
onClose(){
Object.keys(this.callback_message_queue).forEach((key) => {
this.callback_message_queue[key](new Error('close connect'))
delete this.callback_message_queue[key]
})
}
break;
default:
throw new Error('unknown protocol');
}
onRecv(chunk){
this.mp.run(chunk)
}
this.conn.setTimeout(TIMEOUT);
this.conn.setEncoding('utf8');
this.conn.setKeepAlive(true, 0);
this.conn.setNoDelay(true);
this.conn.on('connect', () => {
this.conn.setTimeout(0);
this.onConnect();
});
this.conn.on('close', e => {
this.onClose(e);
});
this.conn.on('data', chunk => {
this.conn.setTimeout(0);
this.onRecv(chunk);
});
this.conn.on('error', e => {
this.onError(e);
});
this.status = 0;
}
onEnd(){
}
connect() {
if (this.status === 1) {
return Promise.resolve();
}
this.status = 1;
return this.connectSocket(this.conn, this.port, this.host);
}
onError(e){
}
connectSocket(conn, port, host) {
return new Promise((resolve, reject) => {
const errorHandler = e => reject(e);
conn.on('error', errorHandler);
conn.connect(port, host, () => {
conn.removeListener('error', errorHandler);
resolve();
});
});
}
close() {
if (this.status === 0) {
return;
}
this.conn.end();
this.conn.destroy();
this.status = 0;
}
request(method, params) {
if (this.status === 0) {
return Promise.reject(new Error('Connection to server lost, please retry'));
}
return new Promise((resolve, reject) => {
const id = ++this.id;
const content = util.makeRequest(method, params, id);
this.callback_message_queue[id] = util.createPromiseResult(resolve, reject);
this.conn.write(content + '\n');
});
}
requestBatch(method, params, secondParam) {
if (this.status === 0) {
return Promise.reject(new Error('Connection to server lost, please retry'));
}
return new Promise((resolve, reject) => {
let arguments_far_calls = {};
let contents = [];
for (let param of params) {
const id = ++this.id;
if (secondParam !== undefined) {
contents.push(util.makeRequest(method, [param, secondParam], id));
} else {
contents.push(util.makeRequest(method, [param], id));
}
arguments_far_calls[id] = param;
}
const content = '[' + contents.join(',') + ']';
this.callback_message_queue[this.id] = util.createPromiseResultBatch(resolve, reject, arguments_far_calls);
// callback will exist only for max id
this.conn.write(content + '\n');
});
}
response(msg) {
let callback;
if (!msg.id && msg[0] && msg[0].id) {
// this is a response from batch request
for (let m of msg) {
if (m.id && this.callback_message_queue[m.id]) {
callback = this.callback_message_queue[m.id];
delete this.callback_message_queue[m.id];
}
}
} else {
callback = this.callback_message_queue[msg.id];
}
if (callback) {
delete this.callback_message_queue[msg.id];
if (msg.error) {
callback(msg.error);
} else {
callback(null, msg.result || msg);
}
} else {
throw new Error("Error getting callback while handling response");
}
}
onMessage(body, n) {
const msg = JSON.parse(body);
if (msg instanceof Array) {
this.response(msg);
} else {
if (msg.id !== void 0) {
this.response(msg);
} else {
this.subscribe.emit(msg.method, msg.params);
}
}
}
onConnect() {
}
onClose(e) {
this.status = 0;
Object.keys(this.callback_message_queue).forEach(key => {
this.callback_message_queue[key](new Error('close connect'));
delete this.callback_message_queue[key];
});
}
onRecv(chunk) {
this.mp.run(chunk);
}
onError(e) {
if (this.onErrorCallback != null) {
this.onErrorCallback(e);
}
}
}
module.exports = Client
module.exports = Client;

View File

@ -1,14 +0,0 @@
'use strict';
const connectSocket = (conn, port, host) => {
return new Promise((resolve, reject) => {
const errorHandler = (e) => reject(e)
conn.connect(port, host, () => {
conn.removeListener('error', errorHandler)
resolve()
})
conn.on('error', errorHandler)
})
}
module.exports = connectSocket

View File

@ -1,93 +0,0 @@
const Client = require("./client")
class ElectrumClient extends Client{
constructor(port, host, protocol, options){
super(port, host, protocol, options)
}
onClose(){
super.onClose()
const list = [
'server.peers.subscribe',
'blockchain.numblocks.subscribe',
'blockchain.headers.subscribe',
'blockchain.address.subscribe'
]
list.forEach(event => this.subscribe.removeAllListeners(event))
}
server_version(client_name, protocol_version){
return this.request('server.version', [client_name, protocol_version])
}
server_banner(){
return this.request('server.banner', [])
}
serverDonation_address(){
return this.request('server.donation_address', [])
}
serverPeers_subscribe(){
return this.request('server.peers.subscribe', [])
}
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_getProof(address){
return this.request('blockchain.address.get_proof', [address])
}
blockchainAddress_listunspent(address){
return this.request('blockchain.address.listunspent', [address])
}
blockchainAddress_subscribe(address){
return this.request('blockchain.address.subscribe', [address])
}
blockchainScripthash_getBalance(scripthash){
return this.request('blockchain.scripthash.get_balance', [scripthash])
}
blockchainScripthash_getHistory(scripthash){
return this.request('blockchain.scripthash.get_history', [scripthash])
}
blockchainScripthash_getMempool(scripthash){
return this.request('blockchain.scripthash.get_mempool', [scripthash])
}
blockchainScripthash_listunspent(scripthash){
return this.request('blockchain.scripthash.listunspent', [scripthash])
}
blockchainScripthash_subscribe(scripthash){
return this.request('blockchain.scripthash.subscribe', [scripthash])
}
blockchainBlock_getHeader(height){
return this.request('blockchain.block.get_header', [height])
}
blockchainBlock_getChunk(index){
return this.request('blockchain.block.get_chunk', [index])
}
blockchainEstimatefee(number){
return this.request('blockchain.estimatefee', [number])
}
blockchainHeaders_subscribe(){
return this.request('blockchain.headers.subscribe', [])
}
blockchainNumblocks_subscribe(){
return this.request('blockchain.numblocks.subscribe', [])
}
blockchain_relayfee(){
return this.request('blockchain.relayfee', [])
}
blockchainTransaction_broadcast(rawtx){
return this.request('blockchain.transaction.broadcast', [rawtx])
}
blockchainTransaction_get(tx_hash, height){
return this.request('blockchain.transaction.get', [tx_hash])
}
blockchainTransaction_getMerkle(tx_hash, height){
return this.request('blockchain.transaction.get_merkle', [tx_hash, height])
}
blockchainUtxo_getAddress(tx_hash, index){
return this.request('blockchain.utxo.get_address', [tx_hash, index])
}
}
module.exports = ElectrumClient

View File

@ -1,57 +0,0 @@
'use strict'
const net = require('net');
const TIMEOUT = 10000
const getSocket = (protocol, options) => {
switch(protocol){
case 'tcp':
return new net.Socket();
case 'tls':
case 'ssl':
let tls;
try {
tls = require('tls');
} catch (e) {
throw new Error('tls package could not be loaded');
}
return new tls.TLSSocket(options);
}
throw new Error('unknown protocol')
}
const initSocket = (self, protocol, options) => {
const conn = getSocket(protocol, options);
conn.setTimeout(TIMEOUT)
conn.setEncoding('utf8')
conn.setKeepAlive(true, 0)
conn.setNoDelay(true)
conn.on('connect', () => {
conn.setTimeout(0)
self.onConnect()
})
conn.on('close', (e) => {
self.onClose(e)
})
conn.on('timeout', () => {
const e = new Error('ETIMEDOUT')
e.errorno = 'ETIMEDOUT'
e.code = 'ETIMEDOUT'
e.connect = false
conn.emit('error', e)
})
conn.on('data', (chunk) => {
conn.setTimeout(0)
self.onRecv(chunk)
})
conn.on('end', (e) => {
conn.setTimeout(0)
self.onEnd(e)
})
conn.on('error', (e) => {
self.onError(e)
})
return conn
}
module.exports = initSocket

View File

@ -1,58 +1,69 @@
'use strict'
'use strict';
const makeRequest = exports.makeRequest = (method, params, id) => {
return JSON.stringify({
jsonrpc : "2.0",
method : method,
params : params,
id : id,
})
const makeRequest = (exports.makeRequest = (method, params, id) => {
return JSON.stringify({
jsonrpc: '2.0',
method: method,
params: params,
id: 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);
else resolve(result);
};
});
const createPromiseResultBatch = (exports.createPromiseResultBatch = (resolve, reject, argz) => {
return (err, result) => {
if (result && result[0] && result[0].id) {
// this is a batch request response
for (let r of result) {
r.param = argz[r.id];
}
}
if (err) reject(err);
else resolve(result);
};
});
class MessageParser {
constructor(callback) {
this.buffer = '';
this.callback = callback;
this.recursiveParser = createRecuesiveParser(20, '\n');
}
run(chunk) {
this.buffer += chunk;
while (true) {
const res = this.recursiveParser(0, this.buffer, this.callback);
this.buffer = res.buffer;
if (res.code === 0) {
break;
}
}
}
}
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)
else resolve(result)
}
}
class MessageParser{
constructor(callback){
this.buffer = ''
this.callback = callback
this.recursiveParser = createRecuesiveParser(20, '\n')
}
run(chunk){
this.buffer += chunk
while(true){
const res = this.recursiveParser(0, this.buffer, this.callback)
this.buffer = res.buffer
if(res.code === 0){
break;
}
}
}
}
exports.MessageParser = MessageParser
exports.MessageParser = MessageParser;

View File

@ -1,30 +1,34 @@
{
"name": "electrum-client",
"version": "0.0.6",
"description": "Electrum protocol client for node.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {},
"devDependencies": {
"electrum-host-parse": "*"
},
"repository": {
"type": "git",
"url": "git://github.com/you21979/node-electrum-client.git"
},
"bugs": {
"url": "https://github.com/you21979/node-electrum-client/issues"
},
"keywords": [
"client",
"electrum",
"bitcoin"
],
"engines": {
"node": ">=6"
},
"author": "Yuki Akiyama",
"license": "MIT"
"name": "@mempool/electrum-client",
"version": "1.1.9",
"description": "Electrum protocol client for Node.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {},
"devDependencies": {},
"repository": {
"type": "git",
"url": "git://github.com/mempool/electrum-client.git"
},
"bugs": {
"url": "https://github.com/mempool/electrum-client/issues"
},
"keywords": [
"bitcoin",
"electrum",
"electrumx"
],
"engines": {
"node": ">=6"
},
"contributors": [
{ "name": "Yuki Akiyama" },
{ "name": "7kharov" },
{ "name": "overtorment" },
{ "name": "janoside" },
{ "name": "softsimon" }
],
"license": "MIT"
}