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
|
// Use the shared axios instance if provided, otherwise fall back to axios
|
||||||
this.axios = config.axios || 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') {
|
if (typeof hex === 'string') {
|
||||||
const response = await this.axios.get(
|
const response = await this.axios.get(
|
||||||
`${this.restURL}full-node/rawtransactions/sendRawTransaction/${hex}`,
|
`${this.restURL}full-node/rawtransactions/sendRawTransaction/${hex}`,
|
||||||
this.axiosOptions
|
{
|
||||||
|
...this.axiosOptions,
|
||||||
|
headers: {
|
||||||
|
...this.axiosOptions.headers,
|
||||||
|
Connection: 'close'
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.data === '66: insufficient priority') {
|
if (response.data === '66: insufficient priority') {
|
||||||
@@ -463,17 +509,25 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Array input
|
// Array input
|
||||||
} else if (Array.isArray(hex)) {
|
} else if (Array.isArray(hex)) {
|
||||||
const options = {
|
let lastErr
|
||||||
method: 'POST',
|
|
||||||
url: `${this.restURL}full-node/rawtransactions/sendRawTransaction`,
|
|
||||||
data: {
|
|
||||||
hexes: hex
|
|
||||||
},
|
|
||||||
headers: this.axiosOptions.headers
|
|
||||||
}
|
|
||||||
const response = await this.axios(options)
|
|
||||||
|
|
||||||
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.')
|
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