mirror of
https://github.com/Permissionless-Software-Foundation/bch-js.git
synced 2026-09-21 16:51:59 -07:00
Merge pull request #290 from Permissionless-Software-Foundation/ct-unstable
fix(sendRawTransaction): Adding retry logic
This commit is contained in:
+65
-11
@@ -15,6 +15,46 @@ class RawTransactions {
|
||||
|
||||
// Use the shared axios instance if provided, otherwise fall back to axios
|
||||
this.axios = config.axios || axios
|
||||
|
||||
// Retry configuration for transient network failures during broadcast.
|
||||
this.maxBroadcastRetries = 2
|
||||
this.broadcastRetryDelayMs = 250
|
||||
}
|
||||
|
||||
_sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
_isTransientNetworkError (error) {
|
||||
if (!error) return false
|
||||
|
||||
const msg = String(error.message || '').toLowerCase()
|
||||
const causeMsg = String(error?.cause?.message || '').toLowerCase()
|
||||
const stack = String(error.stack || '').toLowerCase()
|
||||
const code = String(error.code || error?.cause?.code || '').toUpperCase()
|
||||
|
||||
if (['ECONNRESET', 'EPIPE', 'ETIMEDOUT'].includes(code)) return true
|
||||
if (msg.includes('socket hang up')) return true
|
||||
if (causeMsg.includes('socket hang up')) return true
|
||||
if (stack.includes('socket hang up')) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async _postSendRawTransaction (hexes) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${this.restURL}full-node/rawtransactions/sendRawTransaction`,
|
||||
data: {
|
||||
hexes
|
||||
},
|
||||
headers: {
|
||||
...this.axiosOptions.headers,
|
||||
Connection: 'close'
|
||||
}
|
||||
}
|
||||
|
||||
return this.axios(options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,7 +488,13 @@ class RawTransactions {
|
||||
if (typeof hex === 'string') {
|
||||
const response = await this.axios.get(
|
||||
`${this.restURL}full-node/rawtransactions/sendRawTransaction/${hex}`,
|
||||
this.axiosOptions
|
||||
{
|
||||
...this.axiosOptions,
|
||||
headers: {
|
||||
...this.axiosOptions.headers,
|
||||
Connection: 'close'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (response.data === '66: insufficient priority') {
|
||||
@@ -463,17 +509,25 @@ class RawTransactions {
|
||||
|
||||
// Array input
|
||||
} else if (Array.isArray(hex)) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${this.restURL}full-node/rawtransactions/sendRawTransaction`,
|
||||
data: {
|
||||
hexes: hex
|
||||
},
|
||||
headers: this.axiosOptions.headers
|
||||
}
|
||||
const response = await this.axios(options)
|
||||
let lastErr
|
||||
|
||||
return response.data
|
||||
for (let attempt = 0; attempt <= this.maxBroadcastRetries; attempt++) {
|
||||
try {
|
||||
const response = await this._postSendRawTransaction(hex)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
lastErr = err
|
||||
|
||||
const isLastAttempt = attempt >= this.maxBroadcastRetries
|
||||
const shouldRetry = this._isTransientNetworkError(err) && !isLastAttempt
|
||||
if (!shouldRetry) throw err
|
||||
|
||||
const delay = this.broadcastRetryDelayMs * Math.pow(2, attempt)
|
||||
await this._sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
throw lastErr
|
||||
}
|
||||
|
||||
throw new Error('Input hex must be a string or array of strings.')
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Focused unit tests for retry behavior in raw-transactions.js.
|
||||
*/
|
||||
|
||||
import assert from 'assert'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import RawTransactions from '../../src/raw-transactions.js'
|
||||
|
||||
describe('#RawTransactions Retry Logic', () => {
|
||||
afterEach(() => sinon.restore())
|
||||
|
||||
it('retries once on ECONNRESET and succeeds', async () => {
|
||||
const axiosStub = sinon.stub()
|
||||
axiosStub.onCall(0).rejects(Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' }))
|
||||
axiosStub.onCall(1).resolves({ data: ['txid-123'] })
|
||||
|
||||
const uut = new RawTransactions({
|
||||
restURL: 'http://localhost:5942/v6/',
|
||||
authToken: '',
|
||||
axios: axiosStub
|
||||
})
|
||||
uut.broadcastRetryDelayMs = 0
|
||||
|
||||
const result = await uut.sendRawTransaction(['abcd'])
|
||||
|
||||
assert.deepStrictEqual(result, ['txid-123'])
|
||||
assert.equal(axiosStub.callCount, 2)
|
||||
assert.equal(axiosStub.getCall(0).args[0].headers.Connection, 'close')
|
||||
assert.equal(axiosStub.getCall(1).args[0].headers.Connection, 'close')
|
||||
})
|
||||
|
||||
it('does not retry on non-transient errors', async () => {
|
||||
const axiosStub = sinon.stub()
|
||||
axiosStub.rejects(new Error('RPC validation error'))
|
||||
|
||||
const uut = new RawTransactions({
|
||||
restURL: 'http://localhost:5942/v6/',
|
||||
authToken: '',
|
||||
axios: axiosStub
|
||||
})
|
||||
uut.broadcastRetryDelayMs = 0
|
||||
|
||||
await assert.rejects(
|
||||
uut.sendRawTransaction(['abcd']),
|
||||
/RPC validation error/
|
||||
)
|
||||
assert.equal(axiosStub.callCount, 1)
|
||||
})
|
||||
|
||||
it('enforces retry cap for repeated transient failures', async () => {
|
||||
const axiosStub = sinon.stub()
|
||||
axiosStub.rejects(Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' }))
|
||||
|
||||
const uut = new RawTransactions({
|
||||
restURL: 'http://localhost:5942/v6/',
|
||||
authToken: '',
|
||||
axios: axiosStub
|
||||
})
|
||||
uut.broadcastRetryDelayMs = 0
|
||||
uut.maxBroadcastRetries = 3
|
||||
|
||||
await assert.rejects(
|
||||
uut.sendRawTransaction(['abcd']),
|
||||
/socket hang up/
|
||||
)
|
||||
|
||||
// 1 initial attempt + 2 retries.
|
||||
assert.equal(axiosStub.callCount, 4)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user