Compare commits
No commits in common. "master" and "v0.0.x" have entirely different histories.
3
.gitignore
vendored
3
.gitignore
vendored
@ -57,6 +57,3 @@ typings/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
#editors
|
||||
.idea/
|
||||
|
||||
|
||||
68
README.md
68
README.md
@ -1,51 +1,49 @@
|
||||
# rn-electrum-client
|
||||
# node-electrum-client
|
||||
|
||||
Electrum Protocol Client for React Native
|
||||
Electrum Protocol Client for Node.js
|
||||
|
||||
# based on
|
||||
## what is this
|
||||
|
||||
* https://github.com/you21979/node-electrum-client
|
||||
* https://github.com/7kharov/node-electrum-client
|
||||
https://electrum.org/
|
||||
|
||||
# features
|
||||
electrum is bitcoin wallet service.
|
||||
This is a library of Node.js that can communicate with the electrum(x) server.
|
||||
|
||||
* persistence (ping strategy and reconnection)
|
||||
* batch requests
|
||||
* works in RN and nodejs
|
||||
* both clearnet TCP and TLS
|
||||
* zero dependencies
|
||||
## install
|
||||
|
||||
```
|
||||
npm i electrum-client
|
||||
```
|
||||
|
||||
## spec
|
||||
|
||||
* TCP / TLS
|
||||
* JSON-RPC
|
||||
* Subscribe Message
|
||||
* High Performance Message
|
||||
* no dependency for other library
|
||||
|
||||
## protocol spec
|
||||
|
||||
* https://electrumx-spesmilo.readthedocs.io/en/latest/protocol.html
|
||||
* https://electrumx.readthedocs.io/en/latest/PROTOCOL.html
|
||||
|
||||
## usage
|
||||
|
||||
For Nodejs you can just provide standard modules `net` & `tls` to constructor explicitly, this
|
||||
library won't do `require('net')`.
|
||||
|
||||
```javascript
|
||||
const net = require('net');
|
||||
```
|
||||
|
||||
and then
|
||||
|
||||
```javascript
|
||||
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"
|
||||
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()
|
||||
```
|
||||
|
||||
# license
|
||||
|
||||
MIT
|
||||
|
||||
24
example/bitcoin.js
Normal file
24
example/bitcoin.js
Normal file
@ -0,0 +1,24 @@
|
||||
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)
|
||||
25
example/ex.js
Normal file
25
example/ex.js
Normal file
@ -0,0 +1,25 @@
|
||||
const Client = require("..")
|
||||
|
||||
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")
|
||||
40
example/raii_client_transaction_send.js
Normal file
40
example/raii_client_transaction_send.js
Normal file
@ -0,0 +1,40 @@
|
||||
'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)
|
||||
40
example/raii_timeout.js
Normal file
40
example/raii_timeout.js
Normal file
@ -0,0 +1,40 @@
|
||||
'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)
|
||||
22
example/scripthash.js
Normal file
22
example/scripthash.js
Normal file
@ -0,0 +1,22 @@
|
||||
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)
|
||||
22
example/simple_v09.js
Normal file
22
example/simple_v09.js
Normal file
@ -0,0 +1,22 @@
|
||||
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("0", "0.9")
|
||||
console.log(ver)
|
||||
const balance = await ecl.blockchainAddress_getBalance("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
|
||||
console.log(balance)
|
||||
const unspent = await ecl.blockchainAddress_listunspent("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
|
||||
console.log(unspent)
|
||||
const tx1 = await ecl.blockchainTransaction_get("f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd", false)
|
||||
console.log(tx1)
|
||||
const tx2 = await ecl.blockchainTransaction_get("f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd", true)
|
||||
console.log(tx2)
|
||||
}catch(e){
|
||||
console.log(e)
|
||||
}
|
||||
await ecl.close()
|
||||
}
|
||||
main().catch(console.log)
|
||||
22
example/simple_v12.js
Normal file
22
example/simple_v12.js
Normal file
@ -0,0 +1,22 @@
|
||||
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("0", "1.2")
|
||||
console.log(ver)
|
||||
const balance = await ecl.blockchainAddress_getBalance("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
|
||||
console.log(balance)
|
||||
const unspent = await ecl.blockchainAddress_listunspent("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
|
||||
console.log(unspent)
|
||||
const tx1 = await ecl.blockchainTransaction_get("f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd", false)
|
||||
console.log(tx1)
|
||||
const tx2 = await ecl.blockchainTransaction_get("f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd", true)
|
||||
console.log(JSON.stringify(tx2, null, 2))
|
||||
}catch(e){
|
||||
console.log(e)
|
||||
}
|
||||
await ecl.close()
|
||||
}
|
||||
main().catch(console.log)
|
||||
30
example/subscribe.js
Normal file
30
example/subscribe.js
Normal file
@ -0,0 +1,30 @@
|
||||
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()
|
||||
170
index.js
170
index.js
@ -1,169 +1 @@
|
||||
'use strict';
|
||||
|
||||
const Client = require('./lib/client');
|
||||
|
||||
class ElectrumClient extends Client {
|
||||
constructor(net, tls, port, host, protocol, options) {
|
||||
super(net, tls, port, host, protocol, options);
|
||||
this.timeLastCall = 0;
|
||||
}
|
||||
|
||||
initElectrum(electrumConfig, persistencePolicy = { maxRetry: 1000, callback: null }) {
|
||||
this.persistencePolicy = persistencePolicy;
|
||||
this.electrumConfig = electrumConfig;
|
||||
this.timeLastCall = 0;
|
||||
return this.connect().then(() => this.server_version(this.electrumConfig.client, this.electrumConfig.version));
|
||||
}
|
||||
|
||||
// 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 = [
|
||||
'blockchain.headers.subscribe',
|
||||
'blockchain.scripthash.subscribe',
|
||||
];
|
||||
list.forEach(event => this.subscribe.removeAllListeners(event));
|
||||
setTimeout(() => {
|
||||
if (this.persistencePolicy != null && this.persistencePolicy.maxRetry > 0) {
|
||||
this.reconnect();
|
||||
this.persistencePolicy.maxRetry -= 1;
|
||||
} else if (this.persistencePolicy != null && this.persistencePolicy.callback != null) {
|
||||
this.persistencePolicy.callback();
|
||||
} else if (this.persistencePolicy == null) {
|
||||
this.reconnect();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ElectrumX persistancy
|
||||
keepAlive() {
|
||||
if (this.timeout != null) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
this.timeout = setTimeout(() => {
|
||||
if (this.timeLastCall !== 0 && new Date().getTime() > this.timeLastCall + 5000) {
|
||||
const pingTimer = setTimeout(() => {
|
||||
this.onError(new Error('keepalive ping timeout'));
|
||||
}, 9000);
|
||||
this.server_ping().catch((reason) => {
|
||||
console.log('keepalive ping failed because of', reason);
|
||||
clearTimeout(pingTimer);
|
||||
}).then(() => clearTimeout(pingTimer));
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
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() {
|
||||
console.log('electrum reconnect');
|
||||
this.initSocket();
|
||||
return this.initElectrum(this.electrumConfig);
|
||||
}
|
||||
|
||||
// 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_header(height) {
|
||||
return this.request('blockchain.block.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() {
|
||||
return this.request('blockchain.headers.subscribe', []);
|
||||
}
|
||||
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', []);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ElectrumClient;
|
||||
module.exports = require('./lib/electrum_client');
|
||||
|
||||
259
lib/client.js
259
lib/client.js
@ -1,208 +1,99 @@
|
||||
'use strict';
|
||||
/**
|
||||
* NET & TLS dependencies should be injected via constructor
|
||||
* for RN you can use react-native-tcp-socket
|
||||
*
|
||||
* for nodejs it should be regular node's net & tls:
|
||||
* const net = require('net');
|
||||
* const tls = require('tls');
|
||||
* */
|
||||
const TIMEOUT = 5000;
|
||||
'use strict'
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const util = require('./util')
|
||||
const initSocket = require('./init_socket')
|
||||
const connectSocket = require('./connect_socket')
|
||||
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const util = require('./util');
|
||||
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
|
||||
}
|
||||
|
||||
class Client {
|
||||
constructor(net, tls, port, host, protocol, options) {
|
||||
this.net = net;
|
||||
this.tls = tls;
|
||||
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;
|
||||
this.initSocket(protocol, options);
|
||||
}
|
||||
|
||||
initSocket(protocol, options) {
|
||||
protocol = protocol || this._protocol;
|
||||
options = options || this._options;
|
||||
switch (protocol) {
|
||||
case 'tcp':
|
||||
this.conn = new this.net.Socket();
|
||||
break;
|
||||
case 'tls':
|
||||
case 'ssl':
|
||||
if (!this.tls) {
|
||||
throw new Error('tls package could not be loaded');
|
||||
connect(){
|
||||
if(this.status) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
this.connUnsecure = new this.net.Socket();
|
||||
this.conn = new this.tls.TLSSocket(this.connUnsecure, { rejectUnauthorized: false });
|
||||
break;
|
||||
default:
|
||||
throw new Error('unknown protocol');
|
||||
this.status = 1
|
||||
return connectSocket(this.conn, this.port, this.host)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.status === 1) {
|
||||
return Promise.resolve();
|
||||
close(){
|
||||
if(!this.status) {
|
||||
return
|
||||
}
|
||||
this.conn.end()
|
||||
this.conn.destroy()
|
||||
this.status = 0
|
||||
}
|
||||
this.status = 1;
|
||||
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.removeListener('error', errorHandler);
|
||||
resolve();
|
||||
});
|
||||
conn.on('error', errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.status === 0) {
|
||||
return;
|
||||
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.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'));
|
||||
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
|
||||
}
|
||||
}
|
||||
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));
|
||||
onMessage(body, n){
|
||||
const msg = JSON.parse(body)
|
||||
if(msg instanceof Array){
|
||||
; // don't support batch request
|
||||
} else {
|
||||
contents.push(util.makeRequest(method, [param], id));
|
||||
if(msg.id !== void 0){
|
||||
this.response(msg)
|
||||
}else{
|
||||
this.subscribe.emit(msg.method, msg.params)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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];
|
||||
});
|
||||
}
|
||||
onConnect(){
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(body, n) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.conn.end();
|
||||
this.conn.destroy();
|
||||
this.onClose(error);
|
||||
onClose(){
|
||||
Object.keys(this.callback_message_queue).forEach((key) => {
|
||||
this.callback_message_queue[key](new Error('close connect'))
|
||||
delete this.callback_message_queue[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onConnect() {}
|
||||
onRecv(chunk){
|
||||
this.mp.run(chunk)
|
||||
}
|
||||
|
||||
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];
|
||||
});
|
||||
}
|
||||
onEnd(){
|
||||
}
|
||||
|
||||
onRecv(chunk) {
|
||||
this.mp.run(chunk);
|
||||
}
|
||||
onError(e){
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
console.log('OnError:' + e);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
module.exports = Client
|
||||
|
||||
14
lib/connect_socket.js
Normal file
14
lib/connect_socket.js
Normal file
@ -0,0 +1,14 @@
|
||||
'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
|
||||
111
lib/electrum_client.js
Normal file
111
lib/electrum_client.js
Normal file
@ -0,0 +1,111 @@
|
||||
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', [])
|
||||
}
|
||||
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_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_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 ? 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 ? verbose : false])
|
||||
}
|
||||
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
|
||||
57
lib/init_socket.js
Normal file
57
lib/init_socket.js
Normal file
@ -0,0 +1,57 @@
|
||||
'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
|
||||
115
lib/util.js
115
lib/util.js
@ -1,61 +1,58 @@
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
const makeRequest = (exports.makeRequest = (method, params, id) => {
|
||||
return JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: method,
|
||||
params: params,
|
||||
id: id,
|
||||
});
|
||||
});
|
||||
|
||||
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.parts = [];
|
||||
this.partsLen = 0;
|
||||
this.callback = callback;
|
||||
}
|
||||
run(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 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;
|
||||
}
|
||||
}
|
||||
const makeRequest = exports.makeRequest = (method, params, id) => {
|
||||
return JSON.stringify({
|
||||
jsonrpc : "2.0",
|
||||
method : method,
|
||||
params : params,
|
||||
id : id,
|
||||
})
|
||||
}
|
||||
exports.MessageParser = MessageParser;
|
||||
|
||||
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
|
||||
|
||||
|
||||
22
package.json
22
package.json
@ -1,19 +1,21 @@
|
||||
{
|
||||
"name": "electrum-client",
|
||||
"version": "3.1.1",
|
||||
"description": "Electrum protocol client for React Native & Node.js",
|
||||
"version": "0.0.7",
|
||||
"description": "Electrum protocol client for node.js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node test.js"
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"devDependencies": {
|
||||
"electrum-host-parse": "*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/BlueWallet/rn-electrum-client.git"
|
||||
"url": "git://github.com/you21979/node-electrum-client.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/BlueWallet/rn-electrum-client/issues"
|
||||
"url": "https://github.com/you21979/node-electrum-client/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"client",
|
||||
@ -21,12 +23,8 @@
|
||||
"bitcoin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=6"
|
||||
},
|
||||
"contributors": [
|
||||
{ "name": "Yuki Akiyama" },
|
||||
{ "name": "7kharov" },
|
||||
{ "name": "overtorment" }
|
||||
],
|
||||
"author": "Yuki Akiyama",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
23
test.js
23
test.js
@ -1,23 +0,0 @@
|
||||
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