mirror of
https://github.com/Permissionless-Software-Foundation/bch-js.git
synced 2026-09-22 00:52:01 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7649eeffec | ||
|
|
f7d79dea28 | ||
|
|
2f1d6995a6 | ||
|
|
462885e97b | ||
|
|
24774a706e |
@@ -7,21 +7,16 @@
|
||||
|
||||
[bch-js](https://www.npmjs.com/package/@psf/bch-js) is a JavaScript npm library for creating web and mobile apps that can interact with the Bitcoin Cash (BCH) blockchain. bch-js contains a toolbox of handy tools, and an easy API for talking with [psf-bch-api REST API](https://github.com/Permissionless-Software-Foundation/psf-bch-api). [FullStack.cash](https://fullstack.cash) offers paid cloud access to psf-bch-api. You can run your own infrastructure by following documentation on [CashStack.info](https://cashstack.info).
|
||||
|
||||
### Quick Start Videos:
|
||||
|
||||
YouTube walk-through videos to help you get started:
|
||||
|
||||
- [Introduction to bch-js and the bch-js-examples repository](https://youtu.be/GD2i1ZUiyrk)
|
||||
|
||||
### Quick Links
|
||||
|
||||
- [Code Examples](https://github.com/Permissionless-Software-Foundation/psf-js-examples) using bch-js
|
||||
- [npm Library](https://www.npmjs.com/package/@psf/bch-js)
|
||||
- [Documentation](https://bchjs.fullstack.cash/)
|
||||
- [Examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples)
|
||||
- [API Reference](https://bchjs.fullstack.cash/)
|
||||
- [x402-bch.fullstack.cash](https://x402-bch.fullstack.cash) - The REST API this library talks to by default.
|
||||
- [FullStack.cash](https://fullstack.cash) - cloud-based infrastructure for application developers.
|
||||
- [Permissionless Software Foundation](https://psfoundation.cash) - The organization that maintains this library.
|
||||
- [CashStack.info](https://cashstack.info) - bch-js is part of the Cash Stack, a JavaScript framework for writing web 2 and web 3 business applications.
|
||||
- [Permissionless Software Foundation](https://psfoundation.info) - The organization that maintains this library.
|
||||
|
||||
|
||||
### Quick Notes
|
||||
|
||||
@@ -115,24 +110,11 @@ const bchjs2 = new BCHJS({
|
||||
|
||||
[bch-wallet-web3-spa](https://github.com/Permissionless-Software-Foundation/bch-wallet-web3-spa) is a React web app template using bch-js and minimal-slp-wallet.
|
||||
|
||||
## Features
|
||||
|
||||
- [ECMAScript 2017 standard JavaScript](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017) used instead of TypeScript. Works
|
||||
natively with node.js v10 or higher.
|
||||
|
||||
- Full SLP tokens support: bch-js has full support for all SLP token functionality, including send, mint, and genesis transactions. It also fully supports all aspects of [non-fugible tokans (NFTs)](https://www.youtube.com/watch?v=vvlpYUx6HRs).
|
||||
|
||||
- [Semantic Release](https://github.com/semantic-release/semantic-release) for
|
||||
continuous delivery using semantic versioning.
|
||||
|
||||
- [IPFS](https://ipfs.io) and [Radicle](https://radicle.xyz) uploads of all files and dependencies, to backup
|
||||
dependencies in case they are ever inaccessible from GitHub or npm.
|
||||
|
||||
## Documentation:
|
||||
|
||||
Full documentation for this library can be found here:
|
||||
|
||||
- [Documentation](https://bchjs.fullstack.cash/)
|
||||
- [API Reference](https://bchjs.fullstack.cash/)
|
||||
|
||||
bch-js uses [APIDOC](http://apidocjs.com/) so that documentation and working code
|
||||
live in the same repository. To generate the documentation:
|
||||
@@ -154,17 +136,6 @@ This open source software is developed and maintained by the [Permissionless Sof
|
||||
<p>bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr</p>
|
||||
</div>
|
||||
|
||||
|
||||
## IPFS & Radicle Releases
|
||||
|
||||
Copies of this repository are also published on [IPFS](https://ipfs.io).
|
||||
|
||||
- v6.2.10: `bafybeifsioj3ba77u2763nsyuzq53gtbdxsnqpoipvdl4immj6ytznjaoy`
|
||||
- (with dependencies, node v14.18.2 and npm v8.8.0): `bafybeihfendd4oj6uxvvecm7sluobwwhpb5wdcxhvhmx56e667nxdncd4a`
|
||||
|
||||
They are also posted to the Radicle:
|
||||
- v6.2.10: `rad:git:hnrkkroqnbfwj6uxpfjuhspoxnfm4i8e6oqwy`
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE.md)
|
||||
|
||||
+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