From 63acf7e1eb49b085e2efff1a529adf0a52e2f54b Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Mon, 2 Mar 2026 20:49:47 +0100 Subject: [PATCH 1/5] Support URL path in WebSocket connections Some Electrum providers require path-based API keys in the WebSocket URL (e.g. wss://host:port/api-key). The library previously constructed URLs as protocol://host:port, discarding any path component. Add an optional `path` parameter to the constructor chain (ElectrumClient -> SocketClient -> WebSocketClient) and append it to the WebSocket URL. The parameter is backwards-compatible and defaults to an empty string. TCP/SSL clients are unaffected since paths are only meaningful for WebSocket connections. Update integration tests to use mainnet servers and support path-based authentication via the ELECTRUMX_API_KEY env variable. --- src/electrum/client.js | 4 +- src/socket/socket_client.js | 5 +- src/socket/socket_client_ws.js | 5 +- test/config.js | 41 +++++++------ test/integration_test.js | 105 ++++++++++++++++----------------- test/tx.json | 4 +- 6 files changed, 84 insertions(+), 80 deletions(-) diff --git a/src/electrum/client.js b/src/electrum/client.js index 88ff218..0d1d81f 100644 --- a/src/electrum/client.js +++ b/src/electrum/client.js @@ -4,8 +4,8 @@ const util = require('./util') const keepAliveInterval = 450 * 1000 // 7.5 minutes as recommended by ElectrumX SESSION_TIMEOUT class ElectrumClient extends SocketClient { - constructor(host, port, protocol, options) { - super(host, port, protocol, options) + constructor(host, port, protocol, options, path) { + super(host, port, protocol, options, path) } async connect(clientName, electrumProtocolVersion, persistencePolicy = {maxRetry: 10, callback: null}) { diff --git a/src/socket/socket_client.js b/src/socket/socket_client.js index a21be7b..8eef058 100644 --- a/src/socket/socket_client.js +++ b/src/socket/socket_client.js @@ -6,12 +6,13 @@ const TCPSocketClient = require('./socket_client_tcp') const WebSocketClient = require('./socket_client_ws') class SocketClient { - constructor(host, port, protocol, options) { + constructor(host, port, protocol, options, path) { this.id = 0 this.host = host this.port = port this.protocol = protocol this.options = options + this.path = path || '' this.status = 0 this.callback_message_queue = {} this.events = new EventEmitter() @@ -27,7 +28,7 @@ class SocketClient { break case 'ws': case 'wss': - this.client = new WebSocketClient(this, host, port, protocol, options) + this.client = new WebSocketClient(this, host, port, protocol, options, path) break default: throw new Error(`invalid protocol: [${protocol}]`) diff --git a/src/socket/socket_client_ws.js b/src/socket/socket_client_ws.js index df56529..d474cc8 100644 --- a/src/socket/socket_client_ws.js +++ b/src/socket/socket_client_ws.js @@ -2,17 +2,18 @@ const W3CWebSocket = require('websocket').w3cwebsocket class WebSocketClient { - constructor(self, host, port, protocol, options) { + constructor(self, host, port, protocol, options, path) { this.self = self this.host = host this.port = port this.protocol = protocol this.options = options + this.path = path || '' this.client = null } async connect() { - const url = `${this.protocol}://${this.host}:${this.port}` + const url = `${this.protocol}://${this.host}:${this.port}${this.path}` // TODO: Add docs // https://github.com/theturtle32/WebSocket-Node/blob/master/docs/W3CWebSocket.md#constructor diff --git a/test/config.js b/test/config.js index 06866ff..77b1bc5 100644 --- a/test/config.js +++ b/test/config.js @@ -1,27 +1,32 @@ -const servers = { - tcp: { - protocol: 'tcp', port: '50001', host: 'electrum.bitaroo.net', +const ELECTRUMX_API_KEY = process.env.ELECTRUMX_API_KEY + +const servers = [ + { + protocol: 'tcp', + port: 50001, + host: 'electrum.bitaroo.net', }, - ssl: { - protocol: 'ssl', port: '50002', host: 'electrum.bitaroo.net', + { + protocol: 'ssl', + port: 50002, + host: 'electrum.bitaroo.net', }, - ws: { - protocol: 'ws', port: '50003', host: 'electrumx-server.tbtc.svc.cluster.local', + // no server available to test ws:// + { + protocol: 'wss', + port: 8443, + host: 'electrumx-server.tbtc.network', + // FIXME: It's a temporary workaround to get the connection working. + options: {rejectUnauthorized: false}, }, - wss: { - protocol: 'wss', port: '50004', host: 'electrumx-server.tbtc.svc.cluster.local', + { + protocol: 'wss', + port: 443, + host: 'electrum.mainnet.boar.network', + path: `/${ELECTRUMX_API_KEY}`, }, -} - -const serversArray = [ - servers.tcp, - servers.ssl, - // FIXME: WebSocket is commented out for CI, until we find public servers for this protocol. - // electrumServers.ws, - // electrumServers.wss, ] module.exports = { servers, - serversArray, } diff --git a/test/integration_test.js b/test/integration_test.js index 9983596..113a272 100644 --- a/test/integration_test.js +++ b/test/integration_test.js @@ -5,76 +5,73 @@ const assert = chai.assert const fs = require('fs') -const config = require('./config') +const {servers} = require('./config') describe('ElectrumClient', async () => { let txData before(async () => { - txData = JSON.parse(await fs.readFileSync('./test/tx.json', 'utf8')) + txData = JSON.parse(fs.readFileSync('./test/tx.json', 'utf8')) }) - context('when connected', async () => { - config.serversArray.forEach((server) => { - describe(`for ${server.protocol} protocol`, async () => { - let client + describe('for all protocols', async () => { + servers.forEach((server) => { + const label = `${server.protocol}://${server.host}:${server.port}${server.path || ''}` + describe(label, async () => { + describe('when connected', async () => { + let client - before(async () => { - client = new ElectrumClient( - server.host, - server.port, - server.protocol, - server.options - ) + before(async () => { + client = new ElectrumClient( + server.host, + server.port, + server.protocol, + server.options, + server.path, + ) - await client - .connect('test_client' + server.protocol, '1.4.2') - .catch((err) => { - console.error( - `failed to connect with config [${JSON.stringify( - server - )}]: [${err}]` - ) - }) - }) + await client.connect('test_client' + server.protocol, '1.4.2') + }) - after(async () => { - await client.close() - }) + after(async () => { + await client.close() + }) - it('request returns result', async () => { - const expectedResult = txData.hex - const result = await client.blockchain_transaction_get(txData.hash) + it('request returns result', async () => { + const expectedResult = txData.hex + const result = await client.blockchain_transaction_get(txData.hash) - assert.equal(result, expectedResult, 'unexpected result') + assert.equal(result, expectedResult, 'unexpected result') + }) }) - }) - }) - }) - context('when not connected', async () => { - before(async () => { - const server = config.servers.tcp + describe('when not connected', async () => { + let client - client = new ElectrumClient( - server.host, - server.port, - server.protocol, - server.options - ) - }) + before(async () => { + client = new ElectrumClient( + server.host, + server.port, + server.protocol, + server.options, + server.path, + ) + }) - it('request throws error', async () => { - await client.blockchain_transaction_get(txData.hash).then( - (value) => { - // onFulfilled - assert.fail('not failed as expected') - }, - (reason) => { - // onRejected - assert.include(reason.toString(), `connection not established`) - } - ) + it('request throws error', async () => { + await client.blockchain_transaction_get(txData.hash).then( + (value) => { + // onFulfilled + assert.fail('not failed as expected') + }, + (reason) => { + // onRejected + assert.include(reason.toString(), `connection not established`) + }, + ) + }) + }) + }) }) }) // TODO: Add tests diff --git a/test/tx.json b/test/tx.json index 6997a67..29e2a78 100644 --- a/test/tx.json +++ b/test/tx.json @@ -1,4 +1,4 @@ { - "hash": "d60033c5cf5c199208a9c656a29967810c4e428c22efb492fdd816e6a0a1e548", - "hex": "010000000001011746bd867400f3494b8f44c24b83e1aa58c4f0ff25b4a61cffeffd4bc0f9ba300000000000ffffffff024897070000000000220020a4333e5612ab1a1043b25755c89b16d55184a42f81799e623e6bc39db8539c180000000000000000166a14edb1b5c2f39af0fec151732585b1049b07895211024730440220276e0ec78028582054d86614c65bc4bf85ff5710b9d3a248ca28dd311eb2fa6802202ec950dd2a8c9435ff2d400cc45d7a4854ae085f49e05cc3f503834546d410de012103732783eef3af7e04d3af444430a629b16a9261e4025f52bf4d6d026299c37c7400000000" + "hash": "a000b3082c3e177a5ba2064b74a87ef830f8107f3fb9f9274dde4ed79bb93008", + "hex": "02000000000103570412f893ca49eb1c42b6d22e385e8e281220eeb4b9526a77e3394a524a2bb80000000000ffffffffc7bf20bcf7643a91cb26410477a1066f1552c63b0f74175bb2a3169b2388cd2e0000000000ffffffffce1a14ab30f6e28ea6ee79469fed744b6b50c6e54583679da8ed00580bbe069f0300000000ffffffff0240420f0000000000220020f531e77ae36368687c262a7da1a89d7f22c48ab709cd83afe08e34c6db6952b90f4900000000000016001409da2b5bbfbe73460db900e09ec6745b65877c0b0248304502210095d6517ecaf5278c692ec72298088ac50f0e9f5afebd7418b4d8ad7cc59fa04202204bbb7c509344cee31982da546f01ab2cf2436425637769b1d4d9a4687ea4ff0601210387947b950113f035a5ffc732912670ebb34db4a97f6e3dd4ada2205b3994a2340248304502210090100e56b3c97b496fd9f9e83b31c26bd957be254826ff9bead8a95081e1851f02200c9d15825c4fa8ceb56828e8338b008791cb6d241f7f597e6587038aefb0aedb01210387947b950113f035a5ffc732912670ebb34db4a97f6e3dd4ada2205b3994a2340247304402204d1f7ba9de4889c9af06988de1142079f0f59ee3296b7acc5bb17cdda415260702200599898d448ee0798e9ced9eef059fc41d2a3c34b8526597d92655f64d6deacb01210387947b950113f035a5ffc732912670ebb34db4a97f6e3dd4ada2205b3994a23400000000" } From cf5c88ebd9a755022848c4499b727340783d5af7 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Tue, 3 Mar 2026 07:57:12 +0100 Subject: [PATCH 2/5] Update CI to Node 20 and actions v4 Node 14 is EOL and actions/setup-node@v2 cache protocol is no longer supported by GitHub, causing "Cache service responded with 400" errors. --- .github/workflows/node.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 857cd18..3185f45 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -11,11 +11,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version: "20.x" cache: "npm" - name: Install dependencies @@ -27,11 +27,11 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version: "20.x" cache: "npm" - name: Install dependencies From 95572fc2eaa6dbb94050342e22da547973a02c1f Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Tue, 3 Mar 2026 07:57:52 +0100 Subject: [PATCH 3/5] Linting files No changes to the logic, just linting. --- example/example.js | 4 ++-- src/electrum/client.js | 2 +- src/socket/socket_client_ws.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/example.js b/example/example.js index 527911c..8f0fec3 100644 --- a/example/example.js +++ b/example/example.js @@ -4,13 +4,13 @@ async function main() { const client = new ElectrumClient( 'electrum.bitaroo.net', 50002, - 'ssl' + 'ssl', ) try { await client.connect( 'electrum-client-js', // optional client name - '1.4.2' // optional protocol version + '1.4.2', // optional protocol version ) const header = await client.blockchain_headers_subscribe() diff --git a/src/electrum/client.js b/src/electrum/client.js index 0d1d81f..d1d094c 100644 --- a/src/electrum/client.js +++ b/src/electrum/client.js @@ -75,7 +75,7 @@ class ElectrumClient extends SocketClient { } }, keepAliveInterval, - this // pass this context as an argument to function + this, // pass this context as an argument to function ) } } diff --git a/src/socket/socket_client_ws.js b/src/socket/socket_client_ws.js index d474cc8..90d2ddd 100644 --- a/src/socket/socket_client_ws.js +++ b/src/socket/socket_client_ws.js @@ -22,7 +22,7 @@ class WebSocketClient { undefined, undefined, undefined, - this.options + this.options, ) this.client = client From 07abc8cc69f9cfff04f983ae16ee9654a8f0970d Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Tue, 3 Mar 2026 08:19:32 +0100 Subject: [PATCH 4/5] Renamed ELECTRUMX_API_KEY to ELECTRUM_API_KEY Not all Electrum servers will be ElectrumX. Renaming for clarity. --- test/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/config.js b/test/config.js index 77b1bc5..3407a86 100644 --- a/test/config.js +++ b/test/config.js @@ -1,4 +1,4 @@ -const ELECTRUMX_API_KEY = process.env.ELECTRUMX_API_KEY +const ELECTRUM_API_KEY = process.env.ELECTRUM_API_KEY const servers = [ { @@ -23,7 +23,7 @@ const servers = [ protocol: 'wss', port: 443, host: 'electrum.mainnet.boar.network', - path: `/${ELECTRUMX_API_KEY}`, + path: `/${ELECTRUM_API_KEY}`, }, ] From d07d089e15278b7cb6d97e6cd35f1b9caa9a8ae9 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Tue, 3 Mar 2026 08:31:09 +0100 Subject: [PATCH 5/5] Map ELECTRUM_API_KEY secret as env for the test job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repository secrets need to be explicitly mapped to environment variables via env: — they aren't injected into process.env automatically. The ${{ secrets.ELECTRUM_API_KEY }} expression reads the secret and exposes it as the ELECTRUM_API_KEY env var for the test step. --- .github/workflows/node.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 3185f45..8890d5a 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -39,3 +39,5 @@ jobs: - name: Run tests run: npm test + env: + ELECTRUM_API_KEY: ${{ secrets.ELECTRUM_API_KEY }}