Compare commits
59 Commits
parameter-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d533c8b216 | ||
|
|
d8547ee1a1 | ||
|
|
691d4ceec9 | ||
|
|
41872e548b | ||
|
|
47b4b11dae | ||
|
|
afafe337fe | ||
|
|
0dedfdb1b5 | ||
|
|
be9541b0cb | ||
|
|
d98e929028 | ||
|
|
344ac93946 | ||
|
|
a5b158a954 | ||
|
|
53dc21ca13 | ||
|
|
595475e8d2 | ||
|
|
0dec5c9cdd | ||
|
|
6fefbd88f1 | ||
|
|
bedf539fdf | ||
|
|
b3a6fecea1 | ||
|
|
d263d2627b | ||
|
|
65a717ddef | ||
|
|
5408cc301b | ||
|
|
bd4d5f491e | ||
|
|
5b3a2b728d | ||
|
|
59583ae7f7 | ||
|
|
2f8121346a | ||
|
|
3ba95c3737 | ||
|
|
138d2b8bf0 | ||
|
|
cc018effaf | ||
|
|
d1baea7ecb | ||
|
|
2a5bb11dd9 | ||
|
|
4bbd13f3c2 | ||
|
|
aa73cd8bb3 | ||
|
|
609ca3bd58 | ||
|
|
d194ff6919 | ||
|
|
3052638dcf | ||
|
|
5e413bf471 | ||
|
|
720c70ef99 | ||
|
|
989d785b23 | ||
|
|
7017952513 | ||
|
|
59712b3b7d | ||
|
|
232e113b35 | ||
|
|
d54b794583 | ||
|
|
17586c9723 | ||
|
|
0af5bab669 | ||
|
|
e612c206ae | ||
|
|
5fb46ad687 | ||
|
|
e57244914c | ||
|
|
96911d3263 | ||
|
|
0a0d6a74cb | ||
|
|
7f022e9f51 | ||
|
|
50fd4e1d26 | ||
|
|
41f93d827d | ||
|
|
8ed71b1e49 | ||
|
|
25234a6648 | ||
|
|
57ab7bf0aa | ||
|
|
9277fda4e8 | ||
|
|
b49886885b | ||
|
|
296e0bb9fe | ||
|
|
66bd1bbf1c | ||
|
|
71e7dc3cdf |
43
README.md
43
README.md
@ -1,36 +1,23 @@
|
|||||||
# node-electrum-client
|
# electrum-client
|
||||||
|
|
||||||
Electrum Protocol Client for Node.js
|
Electrum Protocol Client for node.js.
|
||||||
|
|
||||||
## install
|
# based on
|
||||||
|
|
||||||
```
|
* https://github.com/you21979/node-electrum-client
|
||||||
npm i electrum-client
|
* https://github.com/7kharov/node-electrum-client
|
||||||
```
|
* https://github.com/BlueWallet/rn-electrum-client
|
||||||
|
|
||||||
## spec
|
# features
|
||||||
|
|
||||||
* http://docs.electrum.org/en/latest/protocol.html
|
* persistence (ping strategy and reconnection)
|
||||||
* TCP / TLS
|
* batch requests
|
||||||
* JSON-RPC
|
* works in nodejs
|
||||||
* Subscribe Message
|
|
||||||
|
## protocol spec
|
||||||
|
|
||||||
|
* https://electrumx.readthedocs.io/en/latest/PROTOCOL.html
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
|
||||||
```
|
Relies on `net` so will only run in NodeJS environment.
|
||||||
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()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
|
||||||
@ -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()
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
const ElectrumClient = require('..')
|
|
||||||
const sleep = (ms) => new Promise((resolve,_) => setTimeout(() => resolve(), ms))
|
|
||||||
|
|
||||||
const main = async () => {
|
|
||||||
try{
|
|
||||||
const ecl = new ElectrumClient(995, 'btc.smsys.me', '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)
|
|
||||||
await ecl.connect()
|
|
||||||
const p1 = await ecl.serverPeers_subscribe()
|
|
||||||
const p2 = await ecl.blockchainHeaders_subscribe()
|
|
||||||
const p3 = await ecl.blockchainNumblocks_subscribe()
|
|
||||||
while(true){
|
|
||||||
await sleep(1000)
|
|
||||||
let version = await ecl.server_version("2.7.11", "1.0")
|
|
||||||
}
|
|
||||||
await ecl.close()
|
|
||||||
}catch(e){
|
|
||||||
console.log("error")
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
main()
|
|
||||||
267
index.js
267
index.js
@ -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
125
lib/TlsSocketWrapper.js
Normal 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;
|
||||||
275
lib/client.js
275
lib/client.js
@ -1,105 +1,198 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const EventEmitter = require('events').EventEmitter
|
let net = require('net');
|
||||||
const util = require('./util')
|
let tls = require('tls');
|
||||||
const initSocket = require('./init_socket')
|
const TIMEOUT = 60000;
|
||||||
|
|
||||||
class Client{
|
const TlsSocketWrapper = require('./TlsSocketWrapper.js');
|
||||||
constructor(port, host, protocol = 'tcp', options = void 0){
|
const EventEmitter = require('events').EventEmitter;
|
||||||
this.id = 0;
|
const util = require('./util');
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(){
|
class Client {
|
||||||
if(this.status) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
this.status = 1
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const errorHandler = (e) => reject(e)
|
|
||||||
this.conn.connect(this.port, this.host, () => {
|
|
||||||
this.conn.removeListener('error', errorHandler)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
this.conn.on('error', errorHandler)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
close(){
|
constructor(port, host, protocol, options, callbacks) {
|
||||||
if(!this.status) {
|
this.id = 0;
|
||||||
return
|
this.port = port;
|
||||||
}
|
this.host = host;
|
||||||
this.conn.end()
|
this.callback_message_queue = {};
|
||||||
this.conn.destroy()
|
this.subscribe = new EventEmitter();
|
||||||
this.status = 0
|
this.mp = new util.MessageParser((body, n) => {
|
||||||
}
|
this.onMessage(body, n);
|
||||||
|
});
|
||||||
|
this._protocol = protocol; // saving defaults
|
||||||
|
this._options = options;
|
||||||
|
|
||||||
request(method, params){
|
this.onErrorCallback = (callbacks && callbacks.onError) ? callbacks.onError : null;
|
||||||
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');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
response(msg){
|
this.initSocket(protocol, options);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMessage(body, n){
|
initSocket(protocol, options) {
|
||||||
const msg = JSON.parse(body)
|
protocol = protocol || this._protocol;
|
||||||
if(msg instanceof Array){
|
options = options || this._options;
|
||||||
; // don't support batch request
|
switch (protocol) {
|
||||||
} else {
|
case 'tcp':
|
||||||
if(msg.id !== void 0){
|
this.conn = new net.Socket();
|
||||||
this.response(msg)
|
break;
|
||||||
}else{
|
case 'tls':
|
||||||
this.subscribe.emit(msg.method, msg.params)
|
case 'ssl':
|
||||||
}
|
if (!tls) {
|
||||||
}
|
throw new Error("Package 'tls' not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect(){
|
this.conn = new TlsSocketWrapper(tls);
|
||||||
}
|
|
||||||
|
|
||||||
onClose(){
|
break;
|
||||||
Object.keys(this.callback_message_queue).forEach((key) => {
|
default:
|
||||||
this.callback_message_queue[key](new Error('close connect'))
|
throw new Error('unknown protocol');
|
||||||
delete this.callback_message_queue[key]
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onRecv(chunk){
|
this.conn.setTimeout(TIMEOUT);
|
||||||
this.mp.run(chunk)
|
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;
|
||||||
|
|||||||
@ -1,75 +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])
|
|
||||||
}
|
|
||||||
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, height])
|
|
||||||
}
|
|
||||||
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
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const tls = require('tls');
|
|
||||||
const net = require('net');
|
|
||||||
|
|
||||||
const getSocket = (protocol, options) => {
|
|
||||||
switch(protocol){
|
|
||||||
case 'tcp':
|
|
||||||
return new net.Socket();
|
|
||||||
case 'tls':
|
|
||||||
case 'ssl':
|
|
||||||
return new tls.TLSSocket(options);
|
|
||||||
}
|
|
||||||
throw new Error('unknown protocol')
|
|
||||||
}
|
|
||||||
|
|
||||||
const initSocket = (self, protocol, options) => {
|
|
||||||
const conn = getSocket(protocol, options);
|
|
||||||
conn.setEncoding('utf8')
|
|
||||||
conn.setKeepAlive(true, 0)
|
|
||||||
conn.setNoDelay(true)
|
|
||||||
conn.on('connect', () => {
|
|
||||||
self.onConnect()
|
|
||||||
})
|
|
||||||
conn.on('close', (e) => {
|
|
||||||
self.onClose(e)
|
|
||||||
})
|
|
||||||
conn.on('data', (chunk) => {
|
|
||||||
self.onRecv(chunk)
|
|
||||||
})
|
|
||||||
conn.on('end', (e) => {
|
|
||||||
self.onEnd(e)
|
|
||||||
})
|
|
||||||
conn.on('error', (e) => {
|
|
||||||
self.onError(e)
|
|
||||||
})
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = initSocket
|
|
||||||
123
lib/util.js
123
lib/util.js
@ -1,58 +1,69 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
|
|
||||||
const makeRequest = exports.makeRequest = (method, params, id) => {
|
const makeRequest = (exports.makeRequest = (method, params, id) => {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
jsonrpc : "2.0",
|
jsonrpc: '2.0',
|
||||||
method : method,
|
method: method,
|
||||||
params : params,
|
params: params,
|
||||||
id : id,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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
|
|
||||||
|
|
||||||
|
|||||||
55
package.json
55
package.json
@ -1,25 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "electrum-client",
|
"name": "@mempool/electrum-client",
|
||||||
"version": "0.0.4",
|
"version": "1.1.9",
|
||||||
"description": "Electrum protocol client for node.js",
|
"description": "Electrum protocol client for Node.js",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
},
|
"devDependencies": {},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/you21979/node-electrum-client.git"
|
"url": "git://github.com/mempool/electrum-client.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/you21979/node-electrum-client/issues"
|
"url": "https://github.com/mempool/electrum-client/issues"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"client",
|
"bitcoin",
|
||||||
"electrum",
|
"electrum",
|
||||||
"bitcoin"
|
"electrumx"
|
||||||
],
|
],
|
||||||
"author": "Yuki Akiyama",
|
"engines": {
|
||||||
"license": "MIT"
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{ "name": "Yuki Akiyama" },
|
||||||
|
{ "name": "7kharov" },
|
||||||
|
{ "name": "overtorment" },
|
||||||
|
{ "name": "janoside" },
|
||||||
|
{ "name": "softsimon" }
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user