Skip to content

Commit e3c604d

Browse files
authored
Merge pull request #20 from bitcoinerlab/feat/api-refactor-and-reconnection-fixes
feat: API improvements and reconnection fixes for Electrum explorer
2 parents 70126f8 + b6e1232 commit e3c604d

File tree

7 files changed

+60
-41
lines changed

7 files changed

+60
-41
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ import { EsploraExplorer } from '@bitcoinerlab/explorer';
123123
const feeEstimates = await explorer.fetchFeeEstimates();
124124

125125
// Close the connection
126-
await explorer.close();
126+
explorer.close();
127127
})();
128128
```
129129

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@bitcoinerlab/explorer",
33
"description": "Bitcoin Blockchain Explorer: Client Interface featuring Esplora and Electrum Implementations.",
44
"homepage": "https://github.com/bitcoinerlab/explorer",
5-
"version": "0.3.9",
5+
"version": "0.4.0",
66
"author": "Jose-Luis Landabaso",
77
"license": "MIT",
88
"prettier": "@bitcoinerlab/configs/prettierConfig.json",

src/electrum.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,12 @@ export class ElectrumExplorer implements Explorer {
143143
* Implements {@link Explorer#connect}.
144144
*/
145145
async connect(): Promise<void> {
146-
if (await this.isConnected()) throw new Error('Client already connected.');
146+
if (this.#pingInterval)
147+
throw new Error(
148+
'Client was not successfully closed. Prev connection is still pinging.'
149+
);
150+
if (!this.isClosed())
151+
throw new Error('Client previously connected and never closed.');
147152
this.#client = new ElectrumClient(
148153
netModule,
149154
this.#protocol === 'ssl' ? tlsModule : false,
@@ -179,20 +184,22 @@ export class ElectrumExplorer implements Explorer {
179184
//socket has been init. Here we get an error if electrum server cannot
180185
//be found in that port, so close the socket
181186
try {
182-
await this.close(); //hide possible socket errors
187+
if (!this.isClosed()) this.close();
183188
} catch (err) {
184189
console.warn('Error while closing connection:', getErrorMsg(error));
185190
}
186191
throw new Error(
187192
`ElectrumClient failed to connect: ${getErrorMsg(error)}`
188193
);
189194
}
195+
if (!this.#client) throw new Error('Client should exist at this point');
190196

191197
// Ping every few seconds to keep connection alive.
192198
// This function will never throw since it cannot be handled
193199
// In case of connection errors, users will get them on any next function
194200
// call
195201
this.#pingInterval = setInterval(async () => {
202+
const pingInterval = this.#pingInterval;
196203
this.#getClientOrThrow();
197204
let shouldReconnect = false;
198205
try {
@@ -204,11 +211,15 @@ export class ElectrumExplorer implements Explorer {
204211
getErrorMsg(error)
205212
);
206213
}
207-
if (shouldReconnect) {
214+
//Dont allow 2 instances of #pingInterval. #pingInterval is set on connection
215+
if (shouldReconnect && pingInterval === this.#pingInterval) {
208216
try {
209-
if (await this.isConnected(false)) await this.close(); //hide possible socket errors
217+
if (this.isClosed()) throw new Error('Pinging a closed connection');
218+
this.close();
210219
await new Promise(resolve => setTimeout(resolve, 1000));
211-
await this.connect();
220+
//connect may have been set externally while sleeping, check first
221+
if (this.isClosed() && pingInterval === this.#pingInterval)
222+
await this.connect();
212223
} catch (error) {
213224
console.warn(
214225
'Error while reconnecting during interval pinging:',
@@ -242,31 +253,32 @@ export class ElectrumExplorer implements Explorer {
242253
* Checks server connectivity by sending a ping. Returns `true` if the ping
243254
* is successful, otherwise `false`.
244255
*/
245-
async isConnected(
246-
requestNetworkConfirmation: boolean = true
247-
): Promise<boolean> {
248-
if (this.#client === undefined) return false;
256+
async isConnected(): Promise<boolean> {
257+
if (!this.#client) return false;
249258
else {
250-
if (requestNetworkConfirmation) {
251-
try {
252-
await this.#client.server_ping();
253-
return true;
254-
} catch {}
255-
return false;
256-
} else return true;
259+
try {
260+
await this.#client.server_ping();
261+
return true;
262+
} catch {}
263+
return false;
257264
}
258265
}
266+
isClosed(): boolean {
267+
return !this.#client;
268+
}
259269

260270
/**
261271
* Implements {@link Explorer#close}.
262272
*/
263-
async close(): Promise<void> {
273+
close(): void {
264274
if (this.#pingInterval) {
265275
clearInterval(this.#pingInterval);
266276
this.#pingInterval = undefined;
267277
}
268278
if (!this.#client) console.warn('Client was already closed');
269-
else this.#client.close();
279+
else {
280+
this.#client.close();
281+
}
270282
this.#client = undefined;
271283
}
272284

src/esplora.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function isValidHttpUrl(string: string): boolean {
2626
* Implements an {@link Explorer} Interface for an Esplora server.
2727
*/
2828
export class EsploraExplorer implements Explorer {
29+
#closed: boolean = true;
2930
#timeout: number;
3031
#irrevConfThresh: number;
3132
#BLOCK_HEIGHT_CACHE_TIME: number = 3; //cache for 3 seconds at most
@@ -64,6 +65,7 @@ export class EsploraExplorer implements Explorer {
6465
this.#url = url;
6566
this.#irrevConfThresh = irrevConfThresh;
6667
this.#maxTxPerScriptPubKey = maxTxPerScriptPubKey;
68+
this.#closed = true;
6769
}
6870

6971
async #esploraFetch(...args: Parameters<typeof fetch>): Promise<Response> {
@@ -123,25 +125,26 @@ export class EsploraExplorer implements Explorer {
123125
}
124126

125127
async connect() {
128+
this.#closed = false;
126129
return;
127130
}
128131
/**
129132
* Implements {@link Explorer#isConnected}.
130133
* Checks server connectivity by attempting to fetch the current block height.
131134
* Returns `true` if successful, otherwise `false`.
132135
*/
133-
async isConnected(
134-
requestNetworkConfirmation: boolean = true
135-
): Promise<boolean> {
136-
if (requestNetworkConfirmation) {
137-
try {
138-
await this.fetchBlockHeight();
139-
return true;
140-
} catch {}
141-
return false;
142-
} else return true;
136+
async isConnected(): Promise<boolean> {
137+
try {
138+
await this.fetchBlockHeight();
139+
return true;
140+
} catch {}
141+
return false;
142+
}
143+
isClosed(): boolean {
144+
return this.#closed;
143145
}
144-
async close() {
146+
close() {
147+
this.#closed = true;
145148
return;
146149
}
147150

src/interface.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,21 @@ export interface Explorer {
4040
* respond to requests.
4141
*
4242
* @async
43-
* @param {boolean} [requestNetworkConfirmation=true] When `true`, this checks the network to confirm if the connection is active. When `false`, it only verifies that all required initializations, like calling `connect()` for Electrum clients, have been completed.
4443
* @returns {Promise<boolean>} Promise resolving to `true` if the server is
4544
* reachable and responding; otherwise, `false`.
4645
*/
47-
isConnected(requestNetworkConfirmation?: boolean): Promise<boolean>;
46+
isConnected(): Promise<boolean>;
47+
48+
/**
49+
* Returns true if connect has never been called
50+
* or if close() was called after a connect().
51+
*/
52+
isClosed(): boolean;
4853

4954
/**
5055
* Close the connection.
51-
* @async
5256
*/
53-
close(): Promise<void>;
57+
close(): void;
5458

5559
/**
5660
* Get the balance of an address and find out whether the address ever

test/explorer.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ for (const regtestExplorer of regtestExplorers) {
221221
) //Push a problematic tx that has "Missing inputs"
222222
).rejects.toThrow(/bad-txns-inputs-missingorspent/);
223223
});
224-
test('close', async () => {
225-
await explorer.close();
224+
test('close', () => {
225+
explorer.close();
226226
});
227227
});
228228
}
@@ -300,8 +300,8 @@ describe('Explorer: Tests with public servers', () => {
300300
const blockStatus2 = await explorer.fetchBlockStatus(847612);
301301
expect(blockStatus2).toBe(blockStatus); // Checks reference equality
302302
}, 30000);
303-
test(`close ${explorerName}`, async () => {
304-
await explorer.close();
303+
test(`close ${explorerName}`, () => {
304+
explorer.close();
305305
//await new Promise(r => setTimeout(r, 9000));
306306
}, 10000);
307307
}

0 commit comments

Comments
 (0)