@psf/bch-js 7.1.13 → 7.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/raw-transactions.js
CHANGED
|
@@ -15,6 +15,46 @@ class RawTransactions {
|
|
|
15
15
|
|
|
16
16
|
// Use the shared axios instance if provided, otherwise fall back to axios
|
|
17
17
|
this.axios = config.axios || axios
|
|
18
|
+
|
|
19
|
+
// Retry configuration for transient network failures during broadcast.
|
|
20
|
+
this.maxBroadcastRetries = 2
|
|
21
|
+
this.broadcastRetryDelayMs = 250
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_sleep (ms) {
|
|
25
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_isTransientNetworkError (error) {
|
|
29
|
+
if (!error) return false
|
|
30
|
+
|
|
31
|
+
const msg = String(error.message || '').toLowerCase()
|
|
32
|
+
const causeMsg = String(error?.cause?.message || '').toLowerCase()
|
|
33
|
+
const stack = String(error.stack || '').toLowerCase()
|
|
34
|
+
const code = String(error.code || error?.cause?.code || '').toUpperCase()
|
|
35
|
+
|
|
36
|
+
if (['ECONNRESET', 'EPIPE', 'ETIMEDOUT'].includes(code)) return true
|
|
37
|
+
if (msg.includes('socket hang up')) return true
|
|
38
|
+
if (causeMsg.includes('socket hang up')) return true
|
|
39
|
+
if (stack.includes('socket hang up')) return true
|
|
40
|
+
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async _postSendRawTransaction (hexes) {
|
|
45
|
+
const options = {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
url: `${this.restURL}full-node/rawtransactions/sendRawTransaction`,
|
|
48
|
+
data: {
|
|
49
|
+
hexes
|
|
50
|
+
},
|
|
51
|
+
headers: {
|
|
52
|
+
...this.axiosOptions.headers,
|
|
53
|
+
Connection: 'close'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return this.axios(options)
|
|
18
58
|
}
|
|
19
59
|
|
|
20
60
|
/**
|
|
@@ -448,7 +488,13 @@ class RawTransactions {
|
|
|
448
488
|
if (typeof hex === 'string') {
|
|
449
489
|
const response = await this.axios.get(
|
|
450
490
|
`${this.restURL}full-node/rawtransactions/sendRawTransaction/${hex}`,
|
|
451
|
-
|
|
491
|
+
{
|
|
492
|
+
...this.axiosOptions,
|
|
493
|
+
headers: {
|
|
494
|
+
...this.axiosOptions.headers,
|
|
495
|
+
Connection: 'close'
|
|
496
|
+
}
|
|
497
|
+
}
|
|
452
498
|
)
|
|
453
499
|
|
|
454
500
|
if (response.data === '66: insufficient priority') {
|
|
@@ -463,17 +509,25 @@ class RawTransactions {
|
|
|
463
509
|
|
|
464
510
|
// Array input
|
|
465
511
|
} else if (Array.isArray(hex)) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
512
|
+
let lastErr
|
|
513
|
+
|
|
514
|
+
for (let attempt = 0; attempt <= this.maxBroadcastRetries; attempt++) {
|
|
515
|
+
try {
|
|
516
|
+
const response = await this._postSendRawTransaction(hex)
|
|
517
|
+
return response.data
|
|
518
|
+
} catch (err) {
|
|
519
|
+
lastErr = err
|
|
520
|
+
|
|
521
|
+
const isLastAttempt = attempt >= this.maxBroadcastRetries
|
|
522
|
+
const shouldRetry = this._isTransientNetworkError(err) && !isLastAttempt
|
|
523
|
+
if (!shouldRetry) throw err
|
|
524
|
+
|
|
525
|
+
const delay = this.broadcastRetryDelayMs * Math.pow(2, attempt)
|
|
526
|
+
await this._sleep(delay)
|
|
527
|
+
}
|
|
473
528
|
}
|
|
474
|
-
const response = await this.axios(options)
|
|
475
529
|
|
|
476
|
-
|
|
530
|
+
throw lastErr
|
|
477
531
|
}
|
|
478
532
|
|
|
479
533
|
throw new Error('Input hex must be a string or array of strings.')
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Focused unit tests for retry behavior in raw-transactions.js.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from 'assert'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import RawTransactions from '../../src/raw-transactions.js'
|
|
9
|
+
|
|
10
|
+
describe('#RawTransactions Retry Logic', () => {
|
|
11
|
+
afterEach(() => sinon.restore())
|
|
12
|
+
|
|
13
|
+
it('retries once on ECONNRESET and succeeds', async () => {
|
|
14
|
+
const axiosStub = sinon.stub()
|
|
15
|
+
axiosStub.onCall(0).rejects(Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' }))
|
|
16
|
+
axiosStub.onCall(1).resolves({ data: ['txid-123'] })
|
|
17
|
+
|
|
18
|
+
const uut = new RawTransactions({
|
|
19
|
+
restURL: 'http://localhost:5942/v6/',
|
|
20
|
+
authToken: '',
|
|
21
|
+
axios: axiosStub
|
|
22
|
+
})
|
|
23
|
+
uut.broadcastRetryDelayMs = 0
|
|
24
|
+
|
|
25
|
+
const result = await uut.sendRawTransaction(['abcd'])
|
|
26
|
+
|
|
27
|
+
assert.deepStrictEqual(result, ['txid-123'])
|
|
28
|
+
assert.equal(axiosStub.callCount, 2)
|
|
29
|
+
assert.equal(axiosStub.getCall(0).args[0].headers.Connection, 'close')
|
|
30
|
+
assert.equal(axiosStub.getCall(1).args[0].headers.Connection, 'close')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('does not retry on non-transient errors', async () => {
|
|
34
|
+
const axiosStub = sinon.stub()
|
|
35
|
+
axiosStub.rejects(new Error('RPC validation error'))
|
|
36
|
+
|
|
37
|
+
const uut = new RawTransactions({
|
|
38
|
+
restURL: 'http://localhost:5942/v6/',
|
|
39
|
+
authToken: '',
|
|
40
|
+
axios: axiosStub
|
|
41
|
+
})
|
|
42
|
+
uut.broadcastRetryDelayMs = 0
|
|
43
|
+
|
|
44
|
+
await assert.rejects(
|
|
45
|
+
uut.sendRawTransaction(['abcd']),
|
|
46
|
+
/RPC validation error/
|
|
47
|
+
)
|
|
48
|
+
assert.equal(axiosStub.callCount, 1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('enforces retry cap for repeated transient failures', async () => {
|
|
52
|
+
const axiosStub = sinon.stub()
|
|
53
|
+
axiosStub.rejects(Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' }))
|
|
54
|
+
|
|
55
|
+
const uut = new RawTransactions({
|
|
56
|
+
restURL: 'http://localhost:5942/v6/',
|
|
57
|
+
authToken: '',
|
|
58
|
+
axios: axiosStub
|
|
59
|
+
})
|
|
60
|
+
uut.broadcastRetryDelayMs = 0
|
|
61
|
+
uut.maxBroadcastRetries = 3
|
|
62
|
+
|
|
63
|
+
await assert.rejects(
|
|
64
|
+
uut.sendRawTransaction(['abcd']),
|
|
65
|
+
/socket hang up/
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// 1 initial attempt + 2 retries.
|
|
69
|
+
assert.equal(axiosStub.callCount, 4)
|
|
70
|
+
})
|
|
71
|
+
})
|