mirror of
https://github.com/Permissionless-Software-Foundation/bch-js.git
synced 2026-09-22 17:12: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).
|
[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
|
### 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)
|
- [npm Library](https://www.npmjs.com/package/@psf/bch-js)
|
||||||
- [Documentation](https://bchjs.fullstack.cash/)
|
- [API Reference](https://bchjs.fullstack.cash/)
|
||||||
- [Examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples)
|
|
||||||
- [x402-bch.fullstack.cash](https://x402-bch.fullstack.cash) - The REST API this library talks to by default.
|
- [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.
|
- [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.
|
- [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
|
### 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.
|
[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:
|
## Documentation:
|
||||||
|
|
||||||
Full documentation for this library can be found here:
|
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
|
bch-js uses [APIDOC](http://apidocjs.com/) so that documentation and working code
|
||||||
live in the same repository. To generate the documentation:
|
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>
|
<p>bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr</p>
|
||||||
</div>
|
</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
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE.md)
|
[MIT](LICENSE.md)
|
||||||
|
|||||||
+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