Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: test

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
31 changes: 22 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,43 @@
"description": "Electrum protocol client for React Native & Node.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test test/*.test.js"
},
"dependencies": {},
"devDependencies": {},
"repository": {
"type": "git",
"url": "git://github.com/janoside/electrum-client.git"
"url": "git+https://github.com/2rdzy/electrum-client.git"
},
"bugs": {
"url": "https://github.com/janoside/electrum-client/issues"
"url": "https://github.com/2rdzy/electrum-client/issues"
},
"keywords": [
"bitcoin",
"electrum",
"electrumx"
],
"engines": {
"node": ">=6"
"node": ">=18"
},
"contributors": [
{ "name": "Yuki Akiyama" },
{ "name": "7kharov" },
{ "name": "overtorment" },
{ "name": "janoside" }
{
"name": "Yuki Akiyama"
},
{
"name": "7kharov"
},
{
"name": "overtorment"
},
{
"name": "janoside"
}
],
"license": "MIT"
"license": "MIT",
"homepage": "https://github.com/2rdzy/electrum-client#readme",
"files": [
"index.js",
"lib"
]
}
67 changes: 67 additions & 0 deletions test/client.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';

const assert = require('node:assert/strict');
const { after, before, describe, test } = require('node:test');

const { startServer } = require('./helpers/server.js');
const { withClient } = require('./helpers/client.js');

const handler = request => {
switch (request.method) {
case 'server.version': return ['test-server 1.0', '1.4'];
case 'echo': return request.params;
case 'blockchain.scripthash.get_balance': return { confirmed: request.params[0].length, unconfirmed: 0 };
case 'fail': throw { code: 2, message: 'nope' };
default: throw { code: -32601, message: `unknown method ${request.method}` };
}
};

for (const [protocol, useTls] of [['tcp', false], ['tls', true]]) {
describe(`over ${protocol}`, () => {
let server;

before(async () => { server = await startServer({ useTls, handler }); });
after(() => server.close());

test('negotiates the protocol version', () => withClient(server, protocol, client => {
assert.deepEqual(client.versionInfo, ['test-server 1.0', '1.4']);
}));

test('resolves a request with its result', () => withClient(server, protocol, async client => {
assert.deepEqual(await client.request('echo', ['a', 1]), ['a', 1]);
}));

test('rejects a request with the server error', () => withClient(server, protocol, async client => {
await assert.rejects(client.request('fail', []), { code: 2, message: 'nope' });
}));

test('keeps concurrent requests apart', () => withClient(server, protocol, async client => {
const results = await Promise.all([1, 2, 3, 4, 5].map(n => client.request('echo', [n])));

assert.deepEqual(results, [[1], [2], [3], [4], [5]]);
}));

test('resolves a batch with one entry per parameter, tagged with it', () => withClient(server, protocol, async client => {
const results = await client.blockchainScripthash_getBalanceBatch(['aa', 'bbbb', 'cccccc']);

assert.deepEqual(results.map(r => r.param), ['aa', 'bbbb', 'cccccc']);
assert.deepEqual(results.map(r => r.result.confirmed), [2, 4, 6]);
}));

test('emits subscription notifications', () => withClient(server, protocol, async client => {
const notified = new Promise(resolve => client.subscribe.once('blockchain.headers.subscribe', resolve));

server.broadcast(JSON.stringify({ jsonrpc: '2.0', method: 'blockchain.headers.subscribe', params: [{ height: 7 }] }));

assert.deepEqual(await notified, [{ height: 7 }]);
}));

test('refuses requests after close', async () => {
await withClient(server, protocol, async client => {
client.close();

await assert.rejects(client.request('echo', []), /Connection to server lost/);
});
});
});
}
19 changes: 19 additions & 0 deletions test/fixtures/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJzCCAg+gAwIBAgIUC8QQdK2UWC7Gzx1+oiCJs2ear6wwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAwNTIwMTMyNVoYDzIxMjYw
OTExMjAxMzI1WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCpRxHcBqjb/VShru2qOjUGM+B9owDhiwKzM47xQ/iF
G7k0mtbFv9lHTL9NWTTEHy7prg461Z2XCP3QeNYYjV4udy9G2U9YuJuHHT+gx7br
Cr1Q6WT6kqc7RXe0R79GggqHpi02s3g/qbVO/OalBSvVBFpxxw9uMdG9Q7V5y7Jn
WI9NmYB/WRkBQS4dygw+wjS4igUF3j6vwR40gXm3QA66RxdSDNTOLnWLp6On92wj
DpmGNPciXCxhXquwW0o7ylRnQYsXpafHEMHl1uGjJQpXBuvgV/q1VmLFNKbp00T0
YxiXV2gQA+IgnFMSQ7XKoKHPVJx1gPr97ERTiLYe/ci7AgMBAAGjbzBtMB0GA1Ud
DgQWBBT+7ktr7S5GLg39hICE95f36RZXzTAfBgNVHSMEGDAWgBT+7ktr7S5GLg39
hICE95f36RZXzTAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z
dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAhSP9Gxcxde1s8jfTStMn+VNmKGG3
cWIVnw1IYf8PCTUx5z2gi1MjBpEV4QyL0tWNvfk0yfDCZJ24kXqLbgo338T7A5pQ
SfJJ1XMP/INRWtcsrnQZA4MdflD+9LzDjSaTNw8rLTQqemrRGLcDXfwmk+rAsGSJ
N20d/Txy1Ixlu7bBE95vMS5nHMK3P6XtlbZPGdDyZnN7JoomUYghWUkmtM00w1Ck
WPqNBCVsTRJZgyhgzvIXEBD7XcN4vMR6wWoTUhRxpW9QqlSQyzVr/Ik9DBh+QZ/n
DVdsD2+EMFtg0jzOJ9+fn+dMYpNJOjis8D1hAJZiku500/R2xjlmVdwYeg==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions test/fixtures/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCpRxHcBqjb/VSh
ru2qOjUGM+B9owDhiwKzM47xQ/iFG7k0mtbFv9lHTL9NWTTEHy7prg461Z2XCP3Q
eNYYjV4udy9G2U9YuJuHHT+gx7brCr1Q6WT6kqc7RXe0R79GggqHpi02s3g/qbVO
/OalBSvVBFpxxw9uMdG9Q7V5y7JnWI9NmYB/WRkBQS4dygw+wjS4igUF3j6vwR40
gXm3QA66RxdSDNTOLnWLp6On92wjDpmGNPciXCxhXquwW0o7ylRnQYsXpafHEMHl
1uGjJQpXBuvgV/q1VmLFNKbp00T0YxiXV2gQA+IgnFMSQ7XKoKHPVJx1gPr97ERT
iLYe/ci7AgMBAAECggEABiIzx3aGT9JSFWRdQDnJChHUeeOaIzys5bQMjeQZ8S4b
+91QbZc8rD9fqdHS/k2S9EIA9KjLg67TuOYSpo5uHIjnsaLWI9oskyzAIsyZwtJ/
dvBotEZJa6BiF1Tc3WqA2dbE9KJ0Ik+9PhYegAQ787AAsaFg3Gv5tD3r1B9JYO00
I2nQ36zxw1YlOwHS5LBgYSePHlVP2BYo6M21C/JUs6oECL0DshNRkbE0qLv95trL
JHq0PRx/0ABwpcfgjEDftvA/kFsxDrP3HEnoHIdvGawO/wiURQQiIEEIwtGkMmTP
/7hXSUOf8XWrRk1+9ljxHpIhMEizbVgW0vjvQi9a4QKBgQDQSQhmzef9zM677qvs
xmjdP4S5XQMQXxCRXRYBVs15OURM+MRvWG+zhuhnMMI8tNe9jNCci3jpk5O5NKUN
Bn3P/SLGkr9odzD2wZ+9dw/KHpaCwuGqnngTGKgTaBWA0yyl6Kk59lUbABz0T9vN
87xREtUF2x752Vugpb7wZjt6RwKBgQDQDmr6fA/tUlrPxyb5QINXmFzNrv2WYCQn
L4mZi8fAaSUIb04+LGaKSEr8JhGKir5Vgy7OLQQT5AuPWeBkJcP324jddvJ03cdO
WSVGQ2r1pOjKz+N8egFVGCgi+YE21H2g6LbpXRH4vm/pLv1m7Agp+qy/mJkWe7kn
4GkVKStD7QKBgBhLLGuxQIndxY8t+2eWjMyhh/2G18ClFe2eid6GHVkyUMEZku/Z
xM2PwrOmgEFMZo15Syu3KNc1m1lSxp2KUEpeIBxBKYopvNBLLTKQHItanfuKd3/L
E/5GNfRtg3SPLPn89SAJl0ua68WjOuP+tcxPAgG42RgTPhiyvF6VzgIpAoGBAMJf
pewr+YlDNloFedDFR6+PRaxXPsy9NylwzQGpSE3nLVTq/9MwEgBAt+VH/31KT7V+
ID04ayXWGEKtueh2K9YkwIH+vVcXVztIJ320QHoLvfy0KZhCN3pzXhQGu4QpEtFA
eJoefErCR7U3VEBSZPIrIjbBbsxFfF7X1fSSCLylAoGBAKZ652nLK3Tctg5F1PVm
PMO0K4Vs4rcXBURFq6vLB5QEs0pSC3kjYhrbT8cW6L8zThueFnGrK9SopGing/e8
d6zhhkx5C+OzkDxbyirJKOzCtjrssVTEZId17dDPfr06YDXsD6+6pvFYxeDRxL5J
WhiL3lHrR0+npjrBWe9zJjpy
-----END PRIVATE KEY-----
23 changes: 23 additions & 0 deletions test/helpers/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

// The client expects net and tls to be injected as globals (see lib/client.js).
global.net = require('net');
global.tls = require('tls');

const ElectrumClient = require('../../index.js');

// Connect a client to `server`, run `fn(client)`, and always close the client.
async function withClient(server, protocol, fn, { options, callbacks } = {}) {
const client = new ElectrumClient(server.port, '127.0.0.1', protocol, options, callbacks);

await client.initElectrum({ client: 'electrum-client-test', version: '1.4' }, { retryPeriod: 100000, maxRetry: 0, callback: () => {} });

try {
return await fn(client);

} finally {
client.close();
}
}

module.exports = { ElectrumClient, withClient };
76 changes: 76 additions & 0 deletions test/helpers/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

// A minimal Electrum-protocol server for tests: newline-delimited JSON-RPC over
// TCP or TLS. `handler(request)` returns the result for a request, or throws
// `{code, message}` to send an error.

const fs = require('fs');
const net = require('net');
const path = require('path');
const tls = require('tls');

const fixtures = path.join(__dirname, '..', 'fixtures');

const tlsOptions = {
key: fs.readFileSync(path.join(fixtures, 'key.pem')),
cert: fs.readFileSync(path.join(fixtures, 'cert.pem')),
};

function reply(request, handler) {
try {
return { jsonrpc: '2.0', id: request.id, result: handler(request) };

} catch (err) {
return { jsonrpc: '2.0', id: request.id, error: { code: err.code || -1, message: err.message || String(err) } };
}
}

async function startServer({ useTls = false, handler = req => req.params } = {}) {
const sockets = new Set();
const received = [];

const onConnection = socket => {
sockets.add(socket);
socket.on('close', () => sockets.delete(socket));
socket.on('error', () => {});
socket.setEncoding('utf8');

let buffer = '';
socket.on('data', chunk => {
buffer += chunk;

let index;
while ((index = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, index);
buffer = buffer.slice(index + 1);

const message = JSON.parse(line);
received.push(message);

const out = Array.isArray(message)
? message.map(request => reply(request, handler))
: reply(message, handler);

socket.write(JSON.stringify(out) + '\n');
}
});
};

const server = useTls ? tls.createServer(tlsOptions, onConnection) : net.createServer(onConnection);

await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));

return {
port: server.address().port,
received,
sockets,
// send a raw line to every connected client (for notifications and malformed input)
broadcast: line => sockets.forEach(socket => socket.write(line + '\n')),
close: () => new Promise(resolve => {
sockets.forEach(socket => socket.destroy());
server.close(resolve);
}),
};
}

module.exports = { startServer, tlsOptions };
Loading