@samouraiwallet/electrum-client
Version:
Electrum protocol client for Node.js
167 lines • 5.57 kB
JavaScript
import { EventEmitter } from "node:events";
import net from "node:net";
import tls from "node:tls";
import * as util from "./util.js";
const TIMEOUT = 60000;
export class Client {
constructor(port, host, protocol, callbacks) {
this.id = 0;
this.host = host;
this.port = port;
this.status = 0;
this.protocol = protocol;
this.callback_message_queue = new Map();
this.subscribe = new EventEmitter();
this.mp = new util.MessageParser((body, n) => {
this.onMessage(body, n);
});
if (protocol !== "tcp" && protocol !== "tls" && protocol !== "ssl") {
throw new Error("unknown protocol");
}
this.onErrorCallback = callbacks?.onError ?? null;
this.initSocket();
}
initSocket() {
this.conn =
this.protocol === "tls" || this.protocol === "ssl"
? // @ts-expect-error
new tls.TLSSocket(null, { rejectUnauthorized: false })
: new net.Socket();
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", () => {
this.onClose();
});
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.conn) {
if (this.status === 1) {
return Promise.resolve();
}
this.status = 1;
return this.connectSocket(this.conn, this.port, this.host);
}
return Promise.reject(new Error("There is no socket to initialize connection on."));
}
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();
}
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.set(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) => {
const arguments_far_calls = {};
const contents = [];
for (const param of params) {
const id = ++this.id;
if (secondParam == null) {
contents.push(util.makeRequest(method, [param], id));
}
else {
contents.push(util.makeRequest(method, [param, secondParam], id));
}
arguments_far_calls[id] = param;
}
const content = `[${contents.join(",")}]`;
this.callback_message_queue.set(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 (const m of msg) {
if (m.id && this.callback_message_queue.has(m.id)) {
callback = this.callback_message_queue.get(m.id);
this.callback_message_queue.delete(m.id);
}
}
}
else {
callback = this.callback_message_queue.get(msg.id);
}
if (callback) {
this.callback_message_queue.delete(msg.id);
if (msg.error) {
callback(msg.error);
}
else {
callback(null, msg.result || msg);
}
}
else {
console.log(msg);
throw new Error("Error getting callback while handling response");
}
}
onMessage(body, n) {
const msg = JSON.parse(body || "");
if (Array.isArray(msg)) {
this.response(msg);
}
else if (msg.id == null) {
this.subscribe.emit(msg.method, msg.params);
}
else {
this.response(msg);
}
}
onConnect() { }
onClose() {
for (const [key, fn] of this.callback_message_queue.entries()) {
fn(new Error("close connect"));
this.callback_message_queue.delete(key);
}
}
onRecv(chunk) {
this.mp.run(chunk);
}
onError(e) {
if (this.onErrorCallback != null) {
this.onErrorCallback(e);
}
}
}
//# sourceMappingURL=client.js.map