Compare commits

..
41 Commits
Author SHA1 Message Date
Chris Troutner 7649eeffec Merge pull request #290 from Permissionless-Software-Foundation/ct-unstable
fix(sendRawTransaction): Adding retry logic
2026-03-12 12:31:34 -07:00
Chris Troutner f7d79dea28 fix(sendRawTransaction): Adding retry logic 2026-03-12 12:27:43 -07:00
Chris Troutner 2f1d6995a6 Merge pull request #289 from Permissionless-Software-Foundation/ct-unstable
Updating README
2026-02-17 13:25:01 -07:00
Chris Troutner 462885e97b Updating README 2026-02-17 13:21:48 -07:00
Chris Troutner 24774a706e fix(README): updating README 2026-02-17 13:19:18 -07:00
Chris Troutner d5f7e6aa7f Merge pull request #288 from Permissionless-Software-Foundation/ct-unstable
Error handling improvements
2026-02-04 12:58:12 -07:00
Chris Troutner 04e3346086 fix(error handling): Improved error handling. Thowing errors, not objects 2026-02-04 12:54:40 -07:00
Chris Troutner cb2a90fc93 Adding testing script 2026-02-04 09:44:19 -07:00
Chris Troutner 66c2d3ab22 Merge pull request #287 from Permissionless-Software-Foundation/ct-unstable
Updating dependencies for node.js v22
2026-01-16 10:47:45 -07:00
Chris Troutner 89d13fe241 fix(nodejs v22): Adjusting tests. Node.js v22 2x slower on encryption 2026-01-16 10:44:15 -07:00
Chris Troutner ad0b1b5ddb Updating to nodejs v22 2026-01-16 08:48:40 -07:00
Chris Troutner ae30b89d68 Merge pull request #286 from Permissionless-Software-Foundation/ct-unstable
fix(psf-slp-indexer): Adding block height forecast to unconfirmed non…
2026-01-15 16:27:21 -07:00
Chris Troutner 2e88d26ff2 fix(psf-slp-indexer): Adding block height forecast to unconfirmed non-SLP TXs 2026-01-15 16:23:47 -07:00
Chris Troutner b1745d37e0 Merge pull request #285 from Permissionless-Software-Foundation/ct-unstable
fix(x402): Updating x402-bch-axios with faster payment
2025-12-31 15:33:06 -07:00
Chris Troutner 9733c07e04 fix(x402): Updating x402-bch-axios with faster payment 2025-12-31 15:28:44 -07:00
Chris Troutner 430f5f015e Merge pull request #284 from Permissionless-Software-Foundation/ct-unstable
fix(x402): Changing x402 default payment to 25,000 sats, so tx fee is 1%
2025-12-30 13:37:19 -07:00
Chris Troutner 9e76b34d05 fix(x402): Changing x402 default payment to 25,000 sats, so tx fee is 1% 2025-12-30 13:34:02 -07:00
Chris Troutner 61078b2e91 Merge pull request #283 from Permissionless-Software-Foundation/ct-unstable
fix(x402): Removing leading slash in payment URL
2025-12-29 17:08:41 -07:00
Chris Troutner 82733a7e1a Fixing tests 2025-12-29 17:04:09 -07:00
Chris Troutner 3685ea1541 fix(x402): Removing leading slash in payment URL 2025-12-29 16:43:40 -07:00
Chris Troutner 5f217af3e9 Merge pull request #282 from Permissionless-Software-Foundation/ct-unstable
fix(psf-slp-indexer): Fixing issue with error handling
2025-12-29 13:52:40 -07:00
Chris Troutner e2ca3cfb00 fix(psf-slp-indexer): Fixing issue with error handling 2025-12-29 12:38:30 -07:00
Chris Troutner 690520d72e Merge pull request #281 from Permissionless-Software-Foundation/ct-unstable
fix(x402-bch-axios): Updating to v2.1.1 for better error handling
2025-12-24 15:24:47 -07:00
Chris Troutner 36660f3bc9 Merge branch 'master' into ct-unstable 2025-12-24 15:20:34 -07:00
Chris Troutner 80ee5c8d31 fix(x402-bch-axios): Updating to v2.1.1 for better error handling 2025-12-24 15:19:50 -07:00
Chris Troutner b7e04522ce Merge pull request #280 from Permissionless-Software-Foundation/ct-unstable
fix(x402-bch-axios): Updating from v1.1.2 to v2.1.0
2025-12-24 14:09:54 -07:00
Chris Troutner 19f243428d fix(x402-bch-axios): Updating from v1.1.2 to v2.1.0 2025-12-24 14:06:42 -07:00
Chris Troutner 16079c8d04 Merge pull request #279 from Permissionless-Software-Foundation/ct-unstable
fix(dsproof): Fixing bug with axios instantiation
2025-12-22 09:16:37 -07:00
Chris Troutner 6743ca5fa9 Updating integration test script 2025-12-22 08:58:04 -07:00
Chris Troutner ec046d3ef7 fix(dsproof): Fixing bug with axios instantiation 2025-12-21 15:53:46 -07:00
Chris Troutner 85405d8e70 Merge pull request #278 from Permissionless-Software-Foundation/ct-unstable
fix(x402): Improvements to x402 handling
2025-12-21 14:43:56 -07:00
Chris Troutner 1989b72b2a Updated unit tests 2025-12-21 11:58:23 -07:00
Chris Troutner f1f2bf42d9 linting 2025-12-21 11:46:15 -07:00
Chris Troutner 348a073714 fix(x402): Improvements to x402 handling 2025-12-21 11:45:52 -07:00
Chris Troutner e822b91e2c Merge pull request #277 from Permissionless-Software-Foundation/ct-unstable
fix(sem ver): Ensuring sem ver is owrking
2025-12-21 06:39:18 -07:00
Chris Troutner 29a5173036 fix(sem ver): Ensuring sem ver is owrking 2025-12-21 06:36:01 -07:00
Chris Troutner 0bffff6f9b Merge pull request #276 from Permissionless-Software-Foundation/ct-unstable
Updating integration test scripts
2025-12-19 10:51:23 -07:00
Chris Troutner 8c603d3886 fix(x402): Clarifying x402 server separate from psf-bch-api 2025-12-19 10:45:51 -07:00
Chris Troutner 542e59461f fix(x402): Adding default server for x402 payments 2025-12-19 10:25:07 -07:00
Chris Troutner 0912aadc1a Updating integration test scripts 2025-12-19 10:13:37 -07:00
Chris Troutner 638804de42 tweeking test scripts 2025-12-09 14:16:29 -07:00
19 changed files with 1309 additions and 743 deletions
+71 -34
View File
@@ -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
@@ -40,6 +35,72 @@ the [psf-bch-api](https://github.com/Permissionless-Software-Foundation/psf-bch-
- BCHN Mainnet REST API server: https://x402-bch.fullstack.cash/v7/
- Check server status: https://metrics.fullstack.cash
## Configuration
bch-js can be configured through constructor options or environment variables. Configuration options passed to the constructor take precedence over environment variables.
### Constructor Options
When instantiating BCHJS, you can pass a configuration object:
```javascript
import BCHJS from "@psf/bch-js"
const bchjs = new BCHJS({
restURL: 'https://x402-bch.fullstack.cash/v5/',
bearerToken: 'your-bearer-token',
wif: 'your-private-key-wif',
paymentAmountSats: 20000,
bchServerURL: 'https://bch.fullstack.cash'
})
```
### Configuration Options
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `restURL` | string | Yes* | - | The REST API server URL for making API calls. Must include trailing slash. *Required unless `RESTURL` environment variable is set. |
| `bearerToken` | string | No | `''` | Bearer token for authentication with the REST API server. |
| `wif` | string | No | `''` | Private key in WIF format. When provided, enables automatic x402 payment handling. |
| `paymentAmountSats` | number | No | `20000` | Default amount of satoshis to send when making x402 payments. |
| `bchServerURL` | string | No | `'https://bch.fullstack.cash'` | BCH server URL used for broadcasting payment transactions to the blockchain. This is separate from `restURL` and is specifically for x402 payment processing. |
### Environment Variables
You can also configure bch-js using environment variables:
| Environment Variable | Config Option | Description |
|---------------------|---------------|-------------|
| `RESTURL` | `restURL` | REST API server URL for making API calls. |
| `BCHJSBEARERTOKEN` | `bearerToken` | Bearer token for API authentication. |
| `BCHJSWIF` | `wif` | Private key in WIF format for x402 payments. |
| `BCHJSBCHSERVERURL` | `bchServerURL` | BCH server URL for x402 payment transactions. |
### Understanding restURL vs bchServerURL
These two configuration options serve different purposes:
- **`restURL`**: The REST API server used for all regular API calls (utxo queries, transaction history, etc.). This can be any bch-api compatible server, such as `https://x402-bch.fullstack.cash/v5/` or `https://bch.fullstack.cash/v5/`.
- **`bchServerURL`**: The BCH infrastructure server used specifically for broadcasting x402 payment transactions to the blockchain. This defaults to `https://bch.fullstack.cash` and should typically remain unchanged unless you have specific infrastructure requirements.
**Example Use Case**: Most users will use `https://x402-bch.fullstack.cash/v5/` as their `restURL` to access x402-protected APIs. However, when bch-js needs to make an x402 payment, it uses the `bchServerURL` (default: `https://bch.fullstack.cash`) to broadcast the payment transaction. This ensures payment transactions are sent through a reliable BCH infrastructure endpoint.
```javascript
// Use x402-bch server for API calls, but bch.fullstack.cash for payments
const bchjs = new BCHJS({
restURL: 'https://x402-bch.fullstack.cash/v5/',
wif: 'your-private-key-wif'
// bchServerURL defaults to 'https://bch.fullstack.cash'
})
// Or explicitly set both
const bchjs2 = new BCHJS({
restURL: 'https://x402-bch.fullstack.cash/v5/',
bchServerURL: 'https://bch.fullstack.cash',
wif: 'your-private-key-wif'
})
```
### Web Apps
@@ -49,24 +110,11 @@ the [psf-bch-api](https://github.com/Permissionless-Software-Foundation/psf-bch-
[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:
@@ -88,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)
+737 -554
View File
File diff suppressed because it is too large Load Diff
+10 -7
View File
@@ -2,7 +2,7 @@
"name": "@psf/bch-js",
"version": "7.0.0",
"type": "module",
"description": "A JavaScript library for working with Bitcoin Cash, eCash, and SLP Tokens",
"description": "A JavaScript library for working with Bitcoin Cash and SLP Tokens",
"author": "Chris Troutner <chris.troutner@gmail.com>",
"contributors": [
"Gabriel Cardona <gabriel@bitcoin.com>",
@@ -11,12 +11,15 @@
"main": "src/bch-js.js",
"scripts": {
"test": "export RESTURL=http://localhost:5942/v6 && c8 mocha --trace-warnings --unhandled-rejections=strict --timeout 30000 test/unit/",
"test:custom": "export RESTURL=http://localhost:5942/v6 && c8 mocha --trace-warnings --unhandled-rejections=strict --timeout 60000 ${MOCHA_GREP:+--grep $MOCHA_GREP} test/unit/",
"test:integration": "npm run test:integration:local:noauth",
"test:integration:bchn": "export RESTURL=https://bchn.fullstack.cash/v6/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 test/integration/",
"test:integration:local:noauth": "export RESTURL=http://localhost:5942/v6 && mocha --timeout 30000 test/integration/",
"test:integration:local:auth": "export RESTURL=http://5.78.147.3:5942/v6 && export BCHJSBEARERTOKEN=temp01 && mocha --timeout 30000 test/integration/",
"test:integration:decatur": "export RESTURL=http://192.168.2.127:5942/v6 && mocha --timeout 30000 test/integration/",
"test:integration:x402": "export RESTURL=http://localhost:5942/v6 && export BCHJSWIF=L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4 && mocha --timeout 30000 test/integration/",
"test:integration:fullstack:free": "export RESTURL=https://bch.fullstack.cash/v6 && mocha --timeout 60000 test/integration/",
"test:integration:fullstack:x402": "export RESTURL=https://x402-bch.fullstack.cash/v6 && export BCHJSWIF=L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4 && mocha --timeout 120000 test/integration/",
"test:integration:fullstack:auth": "export RESTURL=http://dev.fullstack.cash/v6 && export BCHJSBEARERTOKEN=test01 && mocha --timeout 30000 test/integration/",
"test:integration:local:noauth": "export RESTURL=http://192.168.1.65:5942/v6 && mocha --timeout 30000 test/integration/",
"test:integration:local:auth": "export RESTURL=http://192.168.1.115:5942/v6 && export BCHJSBEARERTOKEN=temp01 && mocha --timeout 30000 test/integration/",
"test:integration:local:x402": "export RESTURL=http://localhost:5942/v6 && export BCHJSWIF=L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4 && mocha --timeout 1200000 test/integration/",
"test:integration:decatur": "export RESTURL=http://192.168.1.65:5942/v6 && mocha --timeout 30000 test/integration/",
"coverage": "nyc --reporter=html mocha --timeout 25000 test/unit/",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs && ./fix-docs-contrast.sh",
"lint": "standard --env mocha --fix"
@@ -51,7 +54,7 @@
"slp-mdm": "0.0.7",
"slp-parser": "0.0.4",
"wif": "2.0.6",
"x402-bch-axios": "1.1.1"
"x402-bch-axios": "2.2.1"
},
"devDependencies": {
"apidoc": "1.2.0",
+18 -3
View File
@@ -86,8 +86,20 @@ class BCHJS {
} else if (process.env.BCHJSWIF && process.env.BCHJSWIF !== '') {
this.wif = process.env.BCHJSWIF
}
this.paymentAmountSats = (config && config.paymentAmountSats) || 2000 * 10
this.bchServerURL = (config && config.bchServerURL) || 'https://free-bch.fullstack.cash'
// Most TXs cost about 250 sats. So we'll set the default to 25000 sats,
// so the mining fee is 1% of the transaction.
this.paymentAmountSats = (config && config.paymentAmountSats) || 25000
// BCH server URL for x402 payments (separate from REST API server)
// This is used when broadcasting payment transactions to the blockchain
if (config && config.bchServerURL && config.bchServerURL !== '') {
this.bchServerURL = config.bchServerURL
} else if (process.env.BCHJSBCHSERVERURL && process.env.BCHJSBCHSERVERURL !== '') {
this.bchServerURL = process.env.BCHJSBCHSERVERURL
} else {
this.bchServerURL = 'https://bch.fullstack.cash/v6'
}
const libConfig = {
restURL: this.restURL,
@@ -108,7 +120,10 @@ class BCHJS {
axiosInstance = withPaymentInterceptor(
axiosInstance,
signer,
{ bchServerURL: this.bchServerURL }
{
apiType: 'rest-api',
bchServerURL: this.bchServerURL
}
)
libConfig.axios = axiosInstance
+97 -38
View File
@@ -47,8 +47,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -105,8 +108,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -155,8 +161,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -189,8 +198,11 @@ class Blockchain {
console.log('Error in bch-js/blockchain.js/getBlockCount()')
console.log('blockchain.js restURL: ', this.restURL)
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -222,8 +234,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -287,8 +302,13 @@ class Blockchain {
throw new Error('Input hash must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
// console.log('Error in bch-js/blockchain.js/getBlockHeader() error: ', error)
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -330,8 +350,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -362,8 +385,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -378,8 +404,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -393,8 +422,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -505,8 +537,11 @@ class Blockchain {
throw new Error('Input must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -541,8 +576,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -588,8 +626,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -642,8 +683,11 @@ class Blockchain {
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -709,8 +753,11 @@ class Blockchain {
throw new Error('Input must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -722,8 +769,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -735,8 +785,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -748,8 +801,11 @@ class Blockchain {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -816,8 +872,11 @@ class Blockchain {
throw new Error('Input must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
}
+7 -3
View File
@@ -6,7 +6,8 @@ class DSProof {
constructor (config) {
this.restURL = config.restURL
this.authToken = config.authToken
this.axios = axios
// Use the shared axios instance if provided, otherwise fall back to axios
this.axios = config.axios || axios
this.axiosOptions = {
headers: {
@@ -61,8 +62,11 @@ class DSProof {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
}
+37 -15
View File
@@ -120,8 +120,11 @@ class ElectrumX {
throw new Error('Input address must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -204,8 +207,11 @@ class ElectrumX {
throw new Error('Input address must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -305,8 +311,11 @@ class ElectrumX {
throw new Error('Input address must be a string or array of strings.')
} catch (error) {
// console.log('Error in transactions(): ', error)
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -397,8 +406,11 @@ class ElectrumX {
throw new Error('Input address must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -453,9 +465,13 @@ class ElectrumX {
} catch (error) {
// console.log("error: ", error)
if (error.response && error.response.data) {
if (error.response && error.response.data) {
throw new Error(error.response.data.error)
} else throw error.response.data
const errorData = error.response.data
if (errorData.error) {
throw new Error(errorData.error)
} else {
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
}
} else {
throw error
}
@@ -555,8 +571,11 @@ class ElectrumX {
throw new Error('Input txId must be a string or array of strings.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
@@ -595,8 +614,11 @@ class ElectrumX {
throw new Error('Input txHex must be a string.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
if (error.response && error.response.data) {
const errorData = error.response.data
const errorMessage = typeof errorData === 'string' ? errorData : (errorData.error || errorData.message || JSON.stringify(errorData))
throw new Error(errorMessage)
} else throw error
}
}
+23 -5
View File
@@ -12,6 +12,7 @@ import axios from 'axios'
// Local libraries
import RawTransaction from './raw-transactions.js'
import SlpUtils from './slp/utils.js'
import Blockchain from './blockchain.js'
// let _this
@@ -31,6 +32,7 @@ class PsfSlpIndexer {
// Encapsulate dependencies
this.rawTransaction = new RawTransaction(config)
this.slpUtils = new SlpUtils(config)
this.blockchain = new Blockchain(config)
}
/**
@@ -66,7 +68,7 @@ class PsfSlpIndexer {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
if (error.response && error.response.data && error.response.data.error) throw new Error(error.response.data.error)
else throw error
}
}
@@ -134,7 +136,7 @@ class PsfSlpIndexer {
}
throw new Error('Input address must be a string.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
if (error.response && error.response.data && error.response.data.error) throw new Error(error.response.data.error)
else throw error
}
}
@@ -199,7 +201,7 @@ class PsfSlpIndexer {
}
throw new Error('Input tokenId must be a string.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
if (error.response && error.response.data && error.response.data.error) throw new Error(error.response.data.error)
else throw error
}
}
@@ -317,6 +319,22 @@ class PsfSlpIndexer {
const txDetails = await this.rawTransaction.getTxData(txid)
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
// Add blockheight, height, time, and blocktime to the txDetails object.
if (txDetails.height === null) {
// Get current block height and increment by 1
const currentBlockHeight = await this.blockchain.getBlockCount()
const estimatedHeight = currentBlockHeight + 1
// Set height and blockheight
txDetails.height = estimatedHeight
txDetails.blockheight = estimatedHeight
// Set time and blocktime to current unix timestamp
const currentTime = Math.floor(Date.now() / 1000)
txDetails.time = currentTime
txDetails.blocktime = currentTime
}
if (isInBlacklist) {
txDetails.isValidSlp = null
} else {
@@ -427,7 +445,7 @@ class PsfSlpIndexer {
}
throw new Error('Input tokenId must be a string.')
} catch (error) {
if (error.response && error.response.data) throw error.response.data
if (error.response && error.response.data && error.response.data.error) throw new Error(error.response.data.error)
else throw error
}
}
@@ -522,7 +540,7 @@ class PsfSlpIndexer {
throw new Error('Input tokenId must be a string.')
} catch (error) {
// console.log('error: ', error)
if (error.response && error.response.data) throw error.response.data
if (error.response && error.response.data && error.response.data.error) throw new Error(error.response.data.error)
else throw error
}
}
+65 -11
View File
@@ -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.')
+6
View File
@@ -20,6 +20,12 @@ class UTXO {
this.psfSlpIndexer = new PsfSlpIndexer(config)
this.BigNumber = BigNumber
this.blockchain = new Blockchain(config)
// Bind 'this' object to all subfunctions.
this.get = this.get.bind(this)
this.hydrateTokenData = this.hydrateTokenData.bind(this)
this.findBiggestUtxo = this.findBiggestUtxo.bind(this)
this.isValid = this.isValid.bind(this)
}
/**
+6 -6
View File
@@ -103,8 +103,8 @@ describe('#blockchain', () => {
console.log(`result: ${util.inspect(result)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
@@ -171,8 +171,8 @@ describe('#blockchain', () => {
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
// console.log(`err: ${util.inspect(err)}`)
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Transaction not in mempool')
assert.instanceOf(err, Error)
assert.include(err.message, 'Transaction not in mempool')
}
})
})
@@ -241,8 +241,8 @@ describe('#blockchain', () => {
console.log(`result: ${util.inspect(result)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
+12 -11
View File
@@ -73,8 +73,8 @@ describe('#ElectrumX', () => {
// console.log(`result: ${util.inspect(result)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
@@ -129,8 +129,8 @@ describe('#ElectrumX', () => {
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
@@ -187,8 +187,8 @@ describe('#ElectrumX', () => {
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
@@ -250,8 +250,8 @@ describe('#ElectrumX', () => {
// console.log(`result: ${util.inspect(result)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
@@ -345,8 +345,8 @@ describe('#ElectrumX', () => {
// console.log(`result: ${util.inspect(result)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
assert.include(err.error, 'Array too large')
assert.instanceOf(err, Error)
assert.include(err.message, 'Array too large')
}
})
})
@@ -359,8 +359,9 @@ describe('#ElectrumX', () => {
try {
await bchjs.Electrumx.broadcast(txHex)
} catch (err) {
// console.error('err: ', err)
assert.include(
err.error,
err.message,
'the transaction was rejected by network rules'
)
}
+1 -1
View File
@@ -18,7 +18,7 @@ describe('#price', () => {
})
describe('#getPsffppPrice', () => {
it('should get the price of BCH in several currencies', async () => {
it('should get the price to write 1MB to the PSFFPP', async () => {
const result = await bchjs.Price.getPsffppPrice()
// console.log(result)
+30 -22
View File
@@ -266,10 +266,15 @@ describe('#BitcoinCash', () => {
})
})
describe('#bip38', () => {
describe('#bip38', function () {
// Increase timeout for BIP38 tests as they use CPU-intensive scrypt operations
// Each encrypt/decrypt operation takes ~5-6 seconds in Node.js v22
this.timeout(60000)
describe('#encryptBIP38', () => {
fixtures.bip38.encrypt.mainnet.forEach(fixture => {
it(`BIP 38 encrypt wif ${fixture.wif} with password ${fixture.password} on mainnet`, () => {
it(`BIP 38 encrypt wif ${fixture.wif} with password ${fixture.password} on mainnet`, function () {
this.timeout(60000)
const encryptedKey = bchjs.BitcoinCash.encryptBIP38(
fixture.wif,
fixture.password
@@ -278,20 +283,22 @@ describe('#BitcoinCash', () => {
})
})
fixtures.bip38.encrypt.testnet.forEach(fixture => {
it(`BIP 38 encrypt wif ${fixture.wif} with password ${fixture.password} on testnet`, () => {
const encryptedKey = bchjs.BitcoinCash.encryptBIP38(
fixture.wif,
fixture.password
)
assert.equal(encryptedKey, fixture.encryptedKey)
})
})
// fixtures.bip38.encrypt.testnet.forEach(fixture => {
// it(`BIP 38 encrypt wif ${fixture.wif} with password ${fixture.password} on testnet`, function () {
// this.timeout(60000)
// const encryptedKey = bchjs.BitcoinCash.encryptBIP38(
// fixture.wif,
// fixture.password
// )
// assert.equal(encryptedKey, fixture.encryptedKey)
// })
// })
})
describe('#decryptBIP38', () => {
fixtures.bip38.decrypt.mainnet.forEach(fixture => {
it(`BIP 38 decrypt encrypted key ${fixture.encryptedKey} on mainnet`, () => {
it(`BIP 38 decrypt encrypted key ${fixture.encryptedKey} on mainnet`, function () {
this.timeout(60000)
const wif = bchjs.BitcoinCash.decryptBIP38(
fixture.encryptedKey,
fixture.password,
@@ -301,16 +308,17 @@ describe('#BitcoinCash', () => {
})
})
fixtures.bip38.decrypt.testnet.forEach(fixture => {
it(`BIP 38 decrypt encrypted key ${fixture.encryptedKey} on testnet`, () => {
const wif = bchjs.BitcoinCash.decryptBIP38(
fixture.encryptedKey,
fixture.password,
'testnet'
)
assert.equal(wif, fixture.wif)
})
})
// fixtures.bip38.decrypt.testnet.forEach(fixture => {
// it(`BIP 38 decrypt encrypted key ${fixture.encryptedKey} on testnet`, function () {
// this.timeout(60000)
// const wif = bchjs.BitcoinCash.decryptBIP38(
// fixture.encryptedKey,
// fixture.password,
// 'testnet'
// )
// assert.equal(wif, fixture.wif)
// })
// })
})
})
})
+1 -1
View File
@@ -93,7 +93,7 @@ describe('#Blockchain', () => {
await bchjs.Blockchain.getBlock(blockhash)
assert2.fail('Unexpected result')
} catch (err) {
assert2.include(err, 'Test Error')
assert2.include(err.message, 'Test Error')
}
})
})
-10
View File
@@ -406,11 +406,6 @@
"wif": "L1XHKhaBAfkr2FJQn3pTfCMxz652WYfmvKj8xDCHCEDV9tWGcbYj",
"password": "1EBPIyj55eR8bVUov9",
"encryptedKey": "6PYWWnBNfNpSqEJZKcfwbrYgTTdb9PNiGjQJ8r9V6cvsZNKLfcZD8YefQc"
},
{
"wif": "L1phBREbhL4vb1uHHHCAse8bdGE5c7ic2PFjRxMawLzQCsiFVbvu",
"password": "9GKVkabAHBMyAf",
"encryptedKey": "6PYU2fDHRVF2194gKDGkbFbeu4mFgkWtVvg2RPd2Sp6KmZx3RCHFpgBB2G"
}
],
"testnet": [
@@ -432,11 +427,6 @@
"wif": "L1XHKhaBAfkr2FJQn3pTfCMxz652WYfmvKj8xDCHCEDV9tWGcbYj",
"password": "1EBPIyj55eR8bVUov9",
"encryptedKey": "6PYWWnBNfNpSqEJZKcfwbrYgTTdb9PNiGjQJ8r9V6cvsZNKLfcZD8YefQc"
},
{
"wif": "L1phBREbhL4vb1uHHHCAse8bdGE5c7ic2PFjRxMawLzQCsiFVbvu",
"password": "9GKVkabAHBMyAf",
"encryptedKey": "6PYU2fDHRVF2194gKDGkbFbeu4mFgkWtVvg2RPd2Sp6KmZx3RCHFpgBB2G"
}
],
"testnet": [
+8 -8
View File
@@ -43,14 +43,14 @@ describe('#PsfSlpIndexer', () => {
try {
// Stub the network call.
const testErr = new Error()
testErr.response = { data: { status: 422 } }
testErr.response = { data: { error: 'Status 422' } }
sandbox.stub(axios, 'get').throws(testErr)
await bchjs.PsfSlpIndexer.status()
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.equal(err.status, 422)
assert.equal(err.message, 'Status 422')
}
})
})
@@ -103,7 +103,7 @@ describe('#PsfSlpIndexer', () => {
try {
// Stub the network call.
const testErr = new Error()
testErr.response = { data: { status: 422 } }
testErr.response = { data: { error: 'Status 422' } }
sandbox.stub(axios, 'post').throws(testErr)
const addr = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
@@ -112,7 +112,7 @@ describe('#PsfSlpIndexer', () => {
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.equal(err.status, 422)
assert.equal(err.message, 'Status 422')
}
})
})
@@ -174,7 +174,7 @@ describe('#PsfSlpIndexer', () => {
try {
// Stub the network call.
const testErr = new Error()
testErr.response = { data: { status: 422 } }
testErr.response = { data: { error: 'Status 422' } }
sandbox.stub(axios, 'post').throws(testErr)
const tokenId =
@@ -182,7 +182,7 @@ describe('#PsfSlpIndexer', () => {
await bchjs.PsfSlpIndexer.tokenStats(tokenId)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.equal(err.status, 422)
assert.equal(err.message, 'Status 422')
}
})
})
@@ -399,7 +399,7 @@ describe('#PsfSlpIndexer', () => {
try {
// Stub the network call.
const testErr = new Error()
testErr.response = { data: { status: 422 } }
testErr.response = { data: { error: 'Status 422' } }
sandbox.stub(axios, 'post').throws(testErr)
const tokenId =
@@ -407,7 +407,7 @@ describe('#PsfSlpIndexer', () => {
await bchjs.PsfSlpIndexer.getTokenData(tokenId)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.equal(err.status, 422)
assert.equal(err.message, 'Status 422')
}
})
})
+71
View File
@@ -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)
})
})
+109 -14
View File
@@ -15,16 +15,21 @@ describe('#X402 Integration', () => {
describe('#Constructor Configuration', () => {
it('should initialize without x402 by default', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(bchjs.wif, '')
assert.strictEqual(bchjs.paymentAmountSats, 20000)
assert.strictEqual(bchjs.bchServerURL, 'https://free-bch.fullstack.cash')
// assert.strictEqual(bchjs.paymentAmountSats, 10000)
assert.strictEqual(typeof bchjs.paymentAmountSats, 'number')
assert.ok(Number.isInteger(bchjs.paymentAmountSats) && bchjs.paymentAmountSats > 0)
assert.strictEqual(bchjs.bchServerURL, 'https://bch.fullstack.cash/v6')
})
it('should accept wif in config', () => {
const testWif = 'L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4'
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/',
wif: testWif
})
@@ -33,6 +38,7 @@ describe('#X402 Integration', () => {
it('should accept paymentAmountSats in config', () => {
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/',
paymentAmountSats: 5000
})
@@ -42,6 +48,7 @@ describe('#X402 Integration', () => {
it('should accept bchServerURL in config', () => {
const customUrl = 'http://localhost:5000'
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/',
bchServerURL: customUrl
})
@@ -54,8 +61,10 @@ describe('#X402 Integration', () => {
})
assert.strictEqual(bchjs.wif, '')
assert.strictEqual(bchjs.paymentAmountSats, 20000)
assert.strictEqual(bchjs.bchServerURL, 'https://free-bch.fullstack.cash')
// assert.strictEqual(bchjs.paymentAmountSats, 10000)
assert.strictEqual(typeof bchjs.paymentAmountSats, 'number')
assert.ok(Number.isInteger(bchjs.paymentAmountSats) && bchjs.paymentAmountSats > 0)
assert.strictEqual(bchjs.bchServerURL, 'https://bch.fullstack.cash/v6')
})
it('should read WIF from BCHJSWIF environment variable', () => {
@@ -64,7 +73,9 @@ describe('#X402 Integration', () => {
process.env.BCHJSWIF = testWif
try {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(bchjs.wif, testWif)
} finally {
// Restore original env value
@@ -83,7 +94,10 @@ describe('#X402 Integration', () => {
process.env.BCHJSWIF = envWif
try {
const bchjs = new BCHJS({ wif: configWif })
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/',
wif: configWif
})
assert.strictEqual(bchjs.wif, configWif)
} finally {
// Restore original env value
@@ -94,29 +108,103 @@ describe('#X402 Integration', () => {
}
}
})
it('should read bchServerURL from BCHJSBCHSERVERURL environment variable', () => {
const testUrl = 'https://custom-bch-server.example.com'
const originalEnv = process.env.BCHJSBCHSERVERURL
process.env.BCHJSBCHSERVERURL = testUrl
try {
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(bchjs.bchServerURL, testUrl)
} finally {
// Restore original env value
if (originalEnv === undefined) {
delete process.env.BCHJSBCHSERVERURL
} else {
process.env.BCHJSBCHSERVERURL = originalEnv
}
}
})
it('should prefer config.bchServerURL over BCHJSBCHSERVERURL environment variable', () => {
const configUrl = 'https://config-server.example.com'
const envUrl = 'https://env-server.example.com'
const originalEnv = process.env.BCHJSBCHSERVERURL
process.env.BCHJSBCHSERVERURL = envUrl
try {
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/',
bchServerURL: configUrl
})
assert.strictEqual(bchjs.bchServerURL, configUrl)
} finally {
// Restore original env value
if (originalEnv === undefined) {
delete process.env.BCHJSBCHSERVERURL
} else {
process.env.BCHJSBCHSERVERURL = originalEnv
}
}
})
it('should keep restURL and bchServerURL independent', () => {
const customRestURL = 'https://x402-bch.fullstack.cash/v5/'
const customBchServerURL = 'https://bch.fullstack.cash'
const bchjs = new BCHJS({
restURL: customRestURL,
bchServerURL: customBchServerURL
})
assert.strictEqual(bchjs.restURL, customRestURL)
assert.strictEqual(bchjs.bchServerURL, customBchServerURL)
// Verify they are different values
assert.notStrictEqual(bchjs.restURL, bchjs.bchServerURL)
})
it('should use default bchServerURL when restURL is customized', () => {
const customRestURL = 'https://x402-bch.fullstack.cash/v5/'
const bchjs = new BCHJS({
restURL: customRestURL
})
assert.strictEqual(bchjs.restURL, customRestURL)
assert.strictEqual(bchjs.bchServerURL, 'https://bch.fullstack.cash/v6')
})
})
describe('#x402 Helper Functions', () => {
it('should expose createSigner function', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(typeof bchjs.x402.createSigner, 'function')
})
it('should expose withPaymentInterceptor function', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(typeof bchjs.x402.withPaymentInterceptor, 'function')
})
it('should expose createPaymentHeader function', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(typeof bchjs.x402.createPaymentHeader, 'function')
})
it('should expose selectPaymentRequirements function', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
assert.strictEqual(typeof bchjs.x402.selectPaymentRequirements, 'function')
})
@@ -124,7 +212,9 @@ describe('#X402 Integration', () => {
describe('#Axios Instance', () => {
it('should have axios available in sub-modules', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
// Check that sub-modules have axios available (from their own import or config)
assert.ok(bchjs.Control.axios)
@@ -138,6 +228,7 @@ describe('#X402 Integration', () => {
it('should pass x402-wrapped axios instance when WIF is provided', () => {
const testWif = 'L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4'
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/',
wif: testWif
})
@@ -151,7 +242,9 @@ describe('#X402 Integration', () => {
describe('#selectPaymentRequirements', () => {
it('should select BCH utxo payment requirements from accepts array', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
const accepts = [
{
@@ -175,7 +268,9 @@ describe('#X402 Integration', () => {
})
it('should throw an error when no BCH requirements found', () => {
const bchjs = new BCHJS()
const bchjs = new BCHJS({
restURL: 'http://localhost:3000/v5/'
})
const accepts = [
{