Skip to content

Commit 388ee7f

Browse files
committed
Improve ElectrumNetworkProvider reliability
closes #74 - Keep counter of concurrentConnections, and only open a connection if the counter is at 0, and only close the connection when the counter is at 1. - Update tests to prevent concurrency issues.
1 parent 2a017b3 commit 388ee7f

5 files changed

Lines changed: 89 additions & 60 deletions

File tree

jest.setup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
jest.setTimeout(25000);
1+
jest.setTimeout(15000);

packages/cashscript/src/network/ElectrumNetworkProvider.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { addressToLockScript, sha256 } from '../util';
1111

1212
export default class ElectrumNetworkProvider implements NetworkProvider {
1313
private electrum: ElectrumCluster;
14+
private concurrentRequests: number = 0;
1415

1516
constructor(
1617
public network: Network = Network.MAINNET,
@@ -82,34 +83,51 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
8283
}
8384
}
8485

85-
disconnectCluster(): boolean[] {
86+
async disconnectCluster(): Promise<boolean[]> {
8687
return this.electrum.shutdown();
8788
}
8889

8990
private async performRequest(
9091
name: string,
9192
...parameters: (string | number | boolean)[]
9293
): Promise<RequestResponse> {
93-
if (!this.manualConnectionManagement) {
94+
// Only connect the cluster when no concurrent requests are running
95+
if (this.shouldConnect()) {
9496
this.connectCluster();
9597
}
9698

99+
this.concurrentRequests += 1;
100+
97101
await this.electrum.ready();
98102

99103
let result;
100104
try {
101105
result = await this.electrum.request(name, ...parameters);
102106
} finally {
103107
// Always disconnect the cluster, also if the request fails
104-
if (!this.manualConnectionManagement) {
105-
this.disconnectCluster();
108+
if (this.shouldDisconnect()) {
109+
await this.disconnectCluster();
106110
}
107111
}
108112

113+
this.concurrentRequests -= 1;
114+
109115
if (result instanceof Error) throw result;
110116

111117
return result;
112118
}
119+
120+
private shouldConnect(): boolean {
121+
if (this.manualConnectionManagement) return false;
122+
if (this.concurrentRequests !== 0) return false;
123+
return true;
124+
}
125+
126+
private shouldDisconnect(): boolean {
127+
if (this.manualConnectionManagement) return false;
128+
if (this.concurrentRequests !== 1) return false;
129+
return true;
130+
}
113131
}
114132

115133
interface ElectrumUtxo {

packages/cashscript/test/e2e/FullStack.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Contract, SignatureTemplate, FullStackNetworkProvider } from '../../src';
22
import {
3-
alicePkh,
4-
alicePk,
53
alice,
64
bob,
5+
bobPkh,
6+
bobPk,
77
} from '../fixture/vars';
88
import { getTxOutputs } from '../test-util';
99
import { FailedSigCheckError, Reason } from '../../src/Errors';
@@ -17,7 +17,7 @@ describe('P2PKH (using FullStackNetworkProvider)', () => {
1717
// eslint-disable-next-line global-require
1818
const artifact = require('../fixture/p2pkh.json');
1919
const provider = new FullStackNetworkProvider('mainnet', new BCHJS({ restURL: 'https://free-main.fullstack.cash/v3/' }));
20-
p2pkhInstance = new Contract(artifact, [alicePkh], provider);
20+
p2pkhInstance = new Contract(artifact, [bobPkh], provider);
2121
console.log(p2pkhInstance.address);
2222
});
2323

@@ -29,7 +29,7 @@ describe('P2PKH (using FullStackNetworkProvider)', () => {
2929

3030
// when
3131
const txPromise = p2pkhInstance.functions
32-
.spend(alicePk, new SignatureTemplate(bob))
32+
.spend(bobPk, new SignatureTemplate(alice))
3333
.to(to, amount)
3434
.send();
3535

@@ -45,7 +45,7 @@ describe('P2PKH (using FullStackNetworkProvider)', () => {
4545

4646
// when
4747
const tx = await p2pkhInstance.functions
48-
.spend(alicePk, new SignatureTemplate(alice))
48+
.spend(bobPk, new SignatureTemplate(bob))
4949
.to(to, amount)
5050
.send();
5151

packages/cashscript/test/e2e/P2PKH.test.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,30 +102,41 @@ describe('P2PKH', () => {
102102
});
103103
});
104104

105-
it('can send to multiple recipients', async () => {
105+
it('can call to() multiple times', async () => {
106106
// given
107107
const outputs = [
108108
{ to: p2pkhInstance.address, amount: 10000 },
109109
{ to: p2pkhInstance.address, amount: 20000 },
110110
];
111111

112112
// when
113-
const tx1 = await p2pkhInstance.functions
113+
const tx = await p2pkhInstance.functions
114114
.spend(alicePk, new SignatureTemplate(alice))
115-
.to(outputs)
115+
.to(outputs[0].to, outputs[0].amount)
116+
.to(outputs[1].to, outputs[1].amount)
116117
.send();
117118

118-
const tx2 = await p2pkhInstance.functions
119+
// then
120+
const txOutputs = getTxOutputs(tx);
121+
expect(txOutputs).toEqual(expect.arrayContaining(outputs));
122+
});
123+
124+
it('can send to list of recipients', async () => {
125+
// given
126+
const outputs = [
127+
{ to: p2pkhInstance.address, amount: 10000 },
128+
{ to: p2pkhInstance.address, amount: 20000 },
129+
];
130+
131+
// when
132+
const tx = await p2pkhInstance.functions
119133
.spend(alicePk, new SignatureTemplate(alice))
120-
.to(outputs[0].to, outputs[0].amount)
121-
.to(outputs[1].to, outputs[1].amount)
134+
.to(outputs)
122135
.send();
123136

124137
// then
125-
const txOutputs1 = getTxOutputs(tx1);
126-
const txOutputs2 = getTxOutputs(tx2);
127-
expect(txOutputs1).toEqual(expect.arrayContaining(outputs));
128-
expect(txOutputs2).toEqual(expect.arrayContaining(outputs));
138+
const txOutputs = getTxOutputs(tx);
139+
expect(txOutputs).toEqual(expect.arrayContaining(outputs));
129140
});
130141

131142
it('can include OP_RETURN data as an output', async () => {

packages/cashscript/test/e2e/TransferWithTimeout.test.ts

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('TransferWithTimeout', () => {
2323
});
2424

2525
describe('send', () => {
26-
it('should fail when using incorrect function arguments', async () => {
26+
it('should fail when using incorrect function arguments to transfer', async () => {
2727
// given
2828
const to = twtInstancePast.address;
2929
const amount = 10000;
@@ -34,19 +34,28 @@ describe('TransferWithTimeout', () => {
3434
.to(to, amount)
3535
.send();
3636

37-
const txPromise2 = twtInstancePast.functions
37+
// then
38+
await expect(txPromise).rejects.toThrow(FailedSigCheckError);
39+
await expect(txPromise).rejects.toThrow(Reason.SIG_NULLFAIL);
40+
});
41+
42+
it('should fail when using incorrect function arguments to timeout', async () => {
43+
// given
44+
const to = twtInstancePast.address;
45+
const amount = 10000;
46+
47+
// when
48+
const txPromise = twtInstancePast.functions
3849
.timeout(new SignatureTemplate(bob))
3950
.to(to, amount)
4051
.send();
4152

4253
// then
4354
await expect(txPromise).rejects.toThrow(FailedSigCheckError);
4455
await expect(txPromise).rejects.toThrow(Reason.SIG_NULLFAIL);
45-
await expect(txPromise2).rejects.toThrow(FailedSigCheckError);
46-
await expect(txPromise2).rejects.toThrow(Reason.SIG_NULLFAIL);
4756
});
4857

49-
it('should fail when called before timeout', async () => {
58+
it('should fail when timeout is called before timeout block', async () => {
5059
// given
5160
const to = twtInstanceFuture.address;
5261
const amount = 10000;
@@ -62,61 +71,52 @@ describe('TransferWithTimeout', () => {
6271
await expect(txPromise).rejects.toThrow(Reason.UNSATISFIED_LOCKTIME);
6372
});
6473

65-
it('should succeed when using correct function arguments', async () => {
74+
it('should succeed when transfer is called after timeout block', async () => {
6675
// given
67-
const toFuture = twtInstanceFuture.address;
68-
const toPast = twtInstancePast.address;
76+
const to = twtInstancePast.address;
6977
const amount = 10000;
7078

7179
// when
72-
const tx1 = await twtInstancePast.functions
80+
const tx = await twtInstancePast.functions
7381
.transfer(new SignatureTemplate(bob))
74-
.to(toPast, amount)
75-
.send();
76-
77-
const tx2 = await twtInstanceFuture.functions
78-
.transfer(new SignatureTemplate(bob))
79-
.to(toFuture, amount)
80-
.send();
81-
82-
const tx3 = await twtInstancePast.functions
83-
.timeout(new SignatureTemplate(alice))
84-
.to(toPast, amount)
82+
.to(to, amount)
8583
.send();
8684

8785
// then
88-
const tx1Outputs = getTxOutputs(tx1);
89-
const tx2Outputs = getTxOutputs(tx2);
90-
const tx3Outputs = getTxOutputs(tx3);
91-
expect(tx1Outputs).toEqual(expect.arrayContaining([{ to: toPast, amount }]));
92-
expect(tx2Outputs).toEqual(expect.arrayContaining([{ to: toFuture, amount }]));
93-
expect(tx3Outputs).toEqual(expect.arrayContaining([{ to: toPast, amount }]));
86+
const txOutputs = getTxOutputs(tx);
87+
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
9488
});
9589

96-
it('can send to multiple recipients', async () => {
90+
it('should succeed when transfer is called before timeout block', async () => {
9791
// given
98-
const outputs = [
99-
{ to: twtInstancePast.address, amount: 10000 },
100-
{ to: twtInstancePast.address, amount: 20000 },
101-
];
92+
const to = twtInstanceFuture.address;
93+
const amount = 10000;
10294

10395
// when
104-
const tx1 = await twtInstancePast.functions
96+
const tx = await twtInstanceFuture.functions
10597
.transfer(new SignatureTemplate(bob))
106-
.to(outputs)
98+
.to(to, amount)
10799
.send();
108100

109-
const tx2 = await twtInstancePast.functions
101+
// then
102+
const txOutputs = getTxOutputs(tx);
103+
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
104+
});
105+
106+
it('should succeed when timeout is called after timeout block', async () => {
107+
// given
108+
const to = twtInstancePast.address;
109+
const amount = 10000;
110+
111+
// when
112+
const tx = await twtInstancePast.functions
110113
.timeout(new SignatureTemplate(alice))
111-
.to(outputs[0].to, outputs[0].amount)
112-
.to(outputs[1].to, outputs[1].amount)
114+
.to(to, amount)
113115
.send();
114116

115117
// then
116-
const txOutputs1 = getTxOutputs(tx1);
117-
const txOutputs2 = getTxOutputs(tx2);
118-
expect(txOutputs1).toEqual(expect.arrayContaining(outputs));
119-
expect(txOutputs2).toEqual(expect.arrayContaining(outputs));
118+
const txOutputs = getTxOutputs(tx);
119+
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
120120
});
121121
});
122122
});

0 commit comments

Comments
 (0)