Compare commits

...
51 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
Chris Troutner fe33ba3381 Merge pull request #275 from Permissionless-Software-Foundation/ct-unstable
Compatibility upgrades for minimal-slp-wallet
2025-11-26 14:46:59 -08:00
Chris Troutner 6f913cf3ad Merge branch 'master' into ct-unstable 2025-11-26 14:43:18 -08:00
Chris Troutner 4d94ad13ff fix(getUsd): Updating endpoint 2025-11-26 13:59:38 -08:00
Chris Troutner c02080d12a fix(scripts.js): Removing @psf/bitcoincash-ops as a dependency 2025-11-26 13:27:45 -08:00
Chris Troutner cc1809b242 fix(script.js): Trying to satisfy ESM, linter, and minify packager at the same time 2025-11-26 13:15:07 -08:00
Chris Troutner 13a7917d9b fix(rest URL): Normalizing URL so user can submit with or without trailing forward slash 2025-11-26 12:28:12 -08:00
Chris Troutner 1573e0bc4c feat(REST URL): removing default REST URL anti-pattern. Error thrown if URL is not specified at startup 2025-11-26 10:55:34 -08:00
Chris Troutner 3fb5377acd Merge pull request #274 from Permissionless-Software-Foundation/ct-unstable
fix(package.json): Fixing minor issue with package.json that was caus…
2025-11-26 08:37:15 -08:00
Chris Troutner a1ba2ac2f7 Merge branch 'master' into ct-unstable 2025-11-26 08:33:46 -08:00
Chris Troutner 7dc29230da fix(package.json): Fixing minor issue with package.json that was causing warning 2025-11-26 08:30:50 -08:00
22 changed files with 1451 additions and 757 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2022 Permissionless Software Foundation contributors
Copyright 2022-2026 Permissionless Software Foundation contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+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)
+739 -557
View File
File diff suppressed because it is too large Load Diff
+12 -11
View File
@@ -2,22 +2,24 @@
"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>",
"Stoyan Zhekov <zh@zhware.net>"
],
"main": "src/bch-js",
"main": "src/bch-js.js",
"scripts": {
"test": "c8 mocha --trace-warnings --unhandled-rejections=strict --timeout 30000 test/unit/",
"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:nft": "export RESTURL=https://bchn.fullstack.cash/v6/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 -g '#nft1' test/integration/slp.js",
"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"
@@ -31,7 +33,6 @@
"dependencies": {
"@chris.troutner/bip32-utils": "1.0.5",
"@psf/bip21": "2.0.1",
"@psf/bitcoincash-ops": "2.0.0",
"@psf/bitcoincashjs-lib": "4.0.3",
"@psf/coininfo": "4.0.0",
"axios": "1.13.2",
@@ -53,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",
+28 -5
View File
@@ -45,7 +45,6 @@ import Ecash from './ecash.js'
// Indexers
import Electrumx from './electrumx.js'
import PsfSlpIndexer from './psf-slp-indexer.js'
const DEFAULT_REST_API = 'https://api.fullstack.cash/v6/'
class BCHJS {
constructor (config) {
@@ -54,7 +53,16 @@ class BCHJS {
this.restURL = config.restURL
} else if (process.env.RESTURL && process.env.RESTURL !== '') {
this.restURL = process.env.RESTURL
} else this.restURL = DEFAULT_REST_API
} else {
throw new Error(
'REST API URL is required. Provide config.restURL or set the RESTURL environment variable.'
)
}
// Normalize restURL to always have a trailing slash
if (!this.restURL.endsWith('/')) {
this.restURL = this.restURL + '/'
}
// Retrieve the Bearer token for simple token authentication.
this.bearerToken = '' // default value.
@@ -78,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,
@@ -100,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
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ class Price {
async getUsd () {
try {
const response = await this.axios.get(
`${this.restURL}price/usd`,
`${this.restURL}price/bchusd`,
this.axiosOptions
)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
+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.')
+126 -3
View File
@@ -1,7 +1,130 @@
import Bitcoin from '@psf/bitcoincashjs-lib'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const opcodes = require('@psf/bitcoincash-ops')
// Inline opcodes data to avoid fs/import.meta issues in browser builds.
// This data is from @psf/bitcoincash-ops/index.json
const opcodes = {
OP_FALSE: 0,
OP_0: 0,
OP_PUSHDATA1: 76,
OP_PUSHDATA2: 77,
OP_PUSHDATA4: 78,
OP_1NEGATE: 79,
OP_RESERVED: 80,
OP_TRUE: 81,
OP_1: 81,
OP_2: 82,
OP_3: 83,
OP_4: 84,
OP_5: 85,
OP_6: 86,
OP_7: 87,
OP_8: 88,
OP_9: 89,
OP_10: 90,
OP_11: 91,
OP_12: 92,
OP_13: 93,
OP_14: 94,
OP_15: 95,
OP_16: 96,
OP_NOP: 97,
OP_VER: 98,
OP_IF: 99,
OP_NOTIF: 100,
OP_VERIF: 101,
OP_VERNOTIF: 102,
OP_ELSE: 103,
OP_ENDIF: 104,
OP_VERIFY: 105,
OP_RETURN: 106,
OP_TOALTSTACK: 107,
OP_FROMALTSTACK: 108,
OP_2DROP: 109,
OP_2DUP: 110,
OP_3DUP: 111,
OP_2OVER: 112,
OP_2ROT: 113,
OP_2SWAP: 114,
OP_IFDUP: 115,
OP_DEPTH: 116,
OP_DROP: 117,
OP_DUP: 118,
OP_NIP: 119,
OP_OVER: 120,
OP_PICK: 121,
OP_ROLL: 122,
OP_ROT: 123,
OP_SWAP: 124,
OP_TUCK: 125,
OP_CAT: 126,
OP_SPLIT: 127,
OP_NUM2BIN: 128,
OP_BIN2NUM: 129,
OP_SIZE: 130,
OP_INVERT: 131,
OP_AND: 132,
OP_OR: 133,
OP_XOR: 134,
OP_EQUAL: 135,
OP_EQUALVERIFY: 136,
OP_RESERVED1: 137,
OP_RESERVED2: 138,
OP_1ADD: 139,
OP_1SUB: 140,
OP_2MUL: 141,
OP_2DIV: 142,
OP_NEGATE: 143,
OP_ABS: 144,
OP_NOT: 145,
OP_0NOTEQUAL: 146,
OP_ADD: 147,
OP_SUB: 148,
OP_MUL: 149,
OP_DIV: 150,
OP_MOD: 151,
OP_LSHIFT: 152,
OP_RSHIFT: 153,
OP_BOOLAND: 154,
OP_BOOLOR: 155,
OP_NUMEQUAL: 156,
OP_NUMEQUALVERIFY: 157,
OP_NUMNOTEQUAL: 158,
OP_LESSTHAN: 159,
OP_GREATERTHAN: 160,
OP_LESSTHANOREQUAL: 161,
OP_GREATERTHANOREQUAL: 162,
OP_MIN: 163,
OP_MAX: 164,
OP_WITHIN: 165,
OP_RIPEMD160: 166,
OP_SHA1: 167,
OP_SHA256: 168,
OP_HASH160: 169,
OP_HASH256: 170,
OP_CODESEPARATOR: 171,
OP_CHECKSIG: 172,
OP_CHECKSIGVERIFY: 173,
OP_CHECKMULTISIG: 174,
OP_CHECKMULTISIGVERIFY: 175,
OP_NOP1: 176,
OP_NOP2: 177,
OP_CHECKLOCKTIMEVERIFY: 177,
OP_NOP3: 178,
OP_CHECKSEQUENCEVERIFY: 178,
OP_NOP4: 179,
OP_NOP5: 180,
OP_NOP6: 181,
OP_NOP7: 182,
OP_NOP8: 183,
OP_NOP9: 184,
OP_NOP10: 185,
OP_CHECKDATASIG: 186,
OP_CHECKDATASIGVERIFY: 187,
OP_REVERSEBYTES: 188,
OP_PUBKEYHASH: 253,
OP_PUBKEY: 254,
OP_INVALIDOPCODE: 255
}
class Script {
constructor () {
+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 = [
{