@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 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
- - [Documentation](https://bchjs.fullstack.cash/)
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
- - [Documentation](https://bchjs.fullstack.cash/)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psf/bch-js",
3
- "version": "7.1.12",
3
+ "version": "7.1.14",
4
4
  "type": "module",
5
5
  "description": "A JavaScript library for working with Bitcoin Cash and SLP Tokens",
6
6
  "author": "Chris Troutner <chris.troutner@gmail.com>",
@@ -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
- this.axiosOptions
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
- const options = {
467
- method: 'POST',
468
- url: `${this.restURL}full-node/rawtransactions/sendRawTransaction`,
469
- data: {
470
- hexes: hex
471
- },
472
- headers: this.axiosOptions.headers
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
- return response.data
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
+ })