@psf/bch-js 7.1.12 → 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/README.md +5 -34
- package/package.json +1 -1
- package/src/raw-transactions.js +64 -10
- package/test/unit/raw-transactions.unit.js +71 -0
package/README.md
CHANGED
|
@@ -7,21 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
[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).
|
|
9
9
|
|
|
10
|
-
### Quick Start Videos:
|
|
11
|
-
|
|
12
|
-
YouTube walk-through videos to help you get started:
|
|
13
|
-
|
|
14
|
-
- [Introduction to bch-js and the bch-js-examples repository](https://youtu.be/GD2i1ZUiyrk)
|
|
15
|
-
|
|
16
10
|
### Quick Links
|
|
17
11
|
|
|
12
|
+
- [Code Examples](https://github.com/Permissionless-Software-Foundation/psf-js-examples) using bch-js
|
|
18
13
|
- [npm Library](https://www.npmjs.com/package/@psf/bch-js)
|
|
19
|
-
- [
|
|
20
|
-
- [Examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples)
|
|
14
|
+
- [API Reference](https://bchjs.fullstack.cash/)
|
|
21
15
|
- [x402-bch.fullstack.cash](https://x402-bch.fullstack.cash) - The REST API this library talks to by default.
|
|
22
16
|
- [FullStack.cash](https://fullstack.cash) - cloud-based infrastructure for application developers.
|
|
23
|
-
- [Permissionless Software Foundation](https://psfoundation.cash) - The organization that maintains this library.
|
|
24
17
|
- [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.
|
|
18
|
+
- [Permissionless Software Foundation](https://psfoundation.info) - The organization that maintains this library.
|
|
19
|
+
|
|
25
20
|
|
|
26
21
|
### Quick Notes
|
|
27
22
|
|
|
@@ -115,24 +110,11 @@ const bchjs2 = new BCHJS({
|
|
|
115
110
|
|
|
116
111
|
[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.
|
|
117
112
|
|
|
118
|
-
## Features
|
|
119
|
-
|
|
120
|
-
- [ECMAScript 2017 standard JavaScript](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017) used instead of TypeScript. Works
|
|
121
|
-
natively with node.js v10 or higher.
|
|
122
|
-
|
|
123
|
-
- 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).
|
|
124
|
-
|
|
125
|
-
- [Semantic Release](https://github.com/semantic-release/semantic-release) for
|
|
126
|
-
continuous delivery using semantic versioning.
|
|
127
|
-
|
|
128
|
-
- [IPFS](https://ipfs.io) and [Radicle](https://radicle.xyz) uploads of all files and dependencies, to backup
|
|
129
|
-
dependencies in case they are ever inaccessible from GitHub or npm.
|
|
130
|
-
|
|
131
113
|
## Documentation:
|
|
132
114
|
|
|
133
115
|
Full documentation for this library can be found here:
|
|
134
116
|
|
|
135
|
-
- [
|
|
117
|
+
- [API Reference](https://bchjs.fullstack.cash/)
|
|
136
118
|
|
|
137
119
|
bch-js uses [APIDOC](http://apidocjs.com/) so that documentation and working code
|
|
138
120
|
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
|
|
|
154
136
|
<p>bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr</p>
|
|
155
137
|
</div>
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
## IPFS & Radicle Releases
|
|
159
|
-
|
|
160
|
-
Copies of this repository are also published on [IPFS](https://ipfs.io).
|
|
161
|
-
|
|
162
|
-
- v6.2.10: `bafybeifsioj3ba77u2763nsyuzq53gtbdxsnqpoipvdl4immj6ytznjaoy`
|
|
163
|
-
- (with dependencies, node v14.18.2 and npm v8.8.0): `bafybeihfendd4oj6uxvvecm7sluobwwhpb5wdcxhvhmx56e667nxdncd4a`
|
|
164
|
-
|
|
165
|
-
They are also posted to the Radicle:
|
|
166
|
-
- v6.2.10: `rad:git:hnrkkroqnbfwj6uxpfjuhspoxnfm4i8e6oqwy`
|
|
167
|
-
|
|
168
139
|
## License
|
|
169
140
|
|
|
170
141
|
[MIT](LICENSE.md)
|
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
|
+
})
|