diff --git a/README.md b/README.md index c6922d8..f7e9170 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,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 diff --git a/package.json b/package.json index 7af6ea9..85cf58b 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,12 @@ "scripts": { "test": "export RESTURL=http://localhost:5942/v6 && c8 mocha --trace-warnings --unhandled-rejections=strict --timeout 30000 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: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 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:local:auth": "export RESTURL=http://192.168.1.115: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:x402": "export RESTURL=http://localhost:5942/v6 && export BCHJSWIF=L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4 && 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" diff --git a/src/bch-js.js b/src/bch-js.js index 5b11c22..3de7f02 100644 --- a/src/bch-js.js +++ b/src/bch-js.js @@ -87,7 +87,16 @@ class BCHJS { this.wif = process.env.BCHJSWIF } this.paymentAmountSats = (config && config.paymentAmountSats) || 2000 * 10 - this.bchServerURL = (config && config.bchServerURL) || 'https://free-bch.fullstack.cash' + + // 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' + } const libConfig = { restURL: this.restURL, diff --git a/src/utxo.js b/src/utxo.js index 68d55f4..c54d9d7 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -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) } /** diff --git a/test/unit/x402.js b/test/unit/x402.js index 7160b00..b7e9257 100644 --- a/test/unit/x402.js +++ b/test/unit/x402.js @@ -15,16 +15,19 @@ 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.bchServerURL, 'https://bch.fullstack.cash') }) it('should accept wif in config', () => { const testWif = 'L1eYaneXDDXy8VDig4Arwe8wYHbhtsA5wuQvwsKwhaYeneoZuKG4' const bchjs = new BCHJS({ + restURL: 'http://localhost:3000/v5/', wif: testWif }) @@ -33,6 +36,7 @@ describe('#X402 Integration', () => { it('should accept paymentAmountSats in config', () => { const bchjs = new BCHJS({ + restURL: 'http://localhost:3000/v5/', paymentAmountSats: 5000 }) @@ -42,6 +46,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 }) @@ -55,7 +60,7 @@ describe('#X402 Integration', () => { assert.strictEqual(bchjs.wif, '') assert.strictEqual(bchjs.paymentAmountSats, 20000) - assert.strictEqual(bchjs.bchServerURL, 'https://free-bch.fullstack.cash') + assert.strictEqual(bchjs.bchServerURL, 'https://bch.fullstack.cash') }) it('should read WIF from BCHJSWIF environment variable', () => { @@ -64,7 +69,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 +90,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 +104,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') + }) }) 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 +208,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 +224,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 +238,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 +264,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 = [ {