psf-bch-api 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.env-local +9 -0
  2. package/README.md +22 -0
  3. package/bin/server.js +24 -1
  4. package/package.json +6 -2
  5. package/src/adapters/fulcrum-api.js +124 -0
  6. package/src/adapters/full-node-rpc.js +2 -6
  7. package/src/adapters/index.js +4 -0
  8. package/src/adapters/slp-indexer-api.js +124 -0
  9. package/src/config/env/common.js +45 -24
  10. package/src/config/x402.js +43 -0
  11. package/src/controllers/index.js +3 -1
  12. package/src/controllers/rest-api/fulcrum/controller.js +563 -0
  13. package/src/controllers/rest-api/fulcrum/router.js +64 -0
  14. package/src/controllers/rest-api/full-node/blockchain/controller.js +26 -26
  15. package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +5 -1
  16. package/src/controllers/rest-api/full-node/control/controller.js +68 -0
  17. package/src/controllers/rest-api/full-node/control/router.js +51 -0
  18. package/src/controllers/rest-api/full-node/dsproof/controller.js +90 -0
  19. package/src/controllers/rest-api/full-node/dsproof/router.js +51 -0
  20. package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
  21. package/src/controllers/rest-api/full-node/mining/router.js +52 -0
  22. package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
  23. package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
  24. package/src/controllers/rest-api/index.js +33 -2
  25. package/src/controllers/rest-api/slp/controller.js +218 -0
  26. package/src/controllers/rest-api/slp/router.js +55 -0
  27. package/src/controllers/timer-controller.js +1 -1
  28. package/src/use-cases/fulcrum-use-cases.js +155 -0
  29. package/src/use-cases/full-node-control-use-cases.js +24 -0
  30. package/src/use-cases/full-node-dsproof-use-cases.js +24 -0
  31. package/src/use-cases/full-node-mining-use-cases.js +28 -0
  32. package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
  33. package/src/use-cases/index.js +12 -0
  34. package/src/use-cases/slp-use-cases.js +321 -0
  35. package/test/unit/controllers/blockchain-controller-unit.js +2 -3
  36. package/test/unit/controllers/control-controller-unit.js +88 -0
  37. package/test/unit/controllers/dsproof-controller-unit.js +117 -0
  38. package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
  39. package/test/unit/controllers/mining-controller-unit.js +139 -0
  40. package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
  41. package/test/unit/controllers/rest-api-index-unit.js +76 -6
  42. package/test/unit/controllers/slp-controller-unit.js +312 -0
  43. package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
  44. package/test/unit/use-cases/full-node-control-use-cases-unit.js +53 -0
  45. package/test/unit/use-cases/full-node-dsproof-use-cases-unit.js +54 -0
  46. package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
  47. package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
  48. package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
  49. package/src/entities/event.js +0 -71
  50. package/test/integration/api/event-integration.js +0 -250
  51. package/test/integration/api/req-integration.js +0 -173
  52. package/test/integration/api/subscription-integration.js +0 -198
  53. package/test/integration/use-cases/manage-subscription-integration.js +0 -163
  54. package/test/integration/use-cases/publish-event-integration.js +0 -104
  55. package/test/integration/use-cases/query-events-integration.js +0 -95
  56. package/test/unit/entities/event-unit.js +0 -139
package/.env-local CHANGED
@@ -2,3 +2,12 @@
2
2
  RPC_BASEURL=http://172.17.0.1:8332
3
3
  RPC_USERNAME=bitcoin
4
4
  RPC_PASSWORD=password
5
+
6
+ # x402 payments required to access this API?
7
+ X402_ENABLED=false
8
+
9
+ # Fulcrum Indexer
10
+ FULCRUM_API=http://192.168.2.127:3001
11
+
12
+ # SLP Indexer
13
+ SLP_INDEXER_API=http://192.168.2.127:5010
package/README.md CHANGED
@@ -6,3 +6,25 @@ This is a REST API for communicating with Bitcoin Cash infrastructure. It replac
6
6
 
7
7
  [MIT](./LICENSE.md)
8
8
 
9
+ ## x402-bch Payments
10
+
11
+ All REST endpoints exposed under the `/v6` prefix are protected by the [`x402-bch-express`](https://www.npmjs.com/package/x402-bch-express) middleware. Each API call requires a BCH payment authorization for **2000 satoshis**. The middleware advertises payment requirements via HTTP 402 responses and validates incoming `X-PAYMENT` headers with a configured Facilitator.
12
+
13
+ ### Configuration
14
+
15
+ Environment variables control the payment flow:
16
+
17
+ - `X402_ENABLED` — set to `false` (case-insensitive) to disable the middleware. Defaults to enabled.
18
+ - `SERVER_BCH_ADDRESS` — BCH cash address that receives funding transactions. Defaults to `bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d`.
19
+ - `FACILITATOR_URL` — Root URL of the facilitator service (e.g., `http://localhost:4345/facilitator`).
20
+ - `X402_PRICE_SAT` — Optional; override the satoshi price per call (defaults to `2000`).
21
+
22
+ When `X402_ENABLED=false`, the server continues to operate without payment headers for local development or trusted deployments.
23
+
24
+ ### Manual Verification
25
+
26
+ 1. Start or point to an `x402-bch` facilitator service (the example facilitator listens at `http://localhost:4345/facilitator`).
27
+ 2. Run the API server with the default configuration: `npm start`.
28
+ 3. Call a protected endpoint without an `X-PAYMENT` header, e.g. `curl -i http://localhost:5942/v6/full-node/control/getNetworkInfo`. The server will respond with HTTP `402` and include payment requirements.
29
+ 4. Restart the server with `X402_ENABLED=false npm start` to confirm that the same request now bypasses the middleware (useful for local development without payments).
30
+
package/bin/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- Express server for REST2NOSTR Proxy API.
2
+ Express server for psf-bch-api REST API.
3
3
  The architecture of the code follows the Clean Architecture pattern.
4
4
  */
5
5
 
@@ -7,6 +7,7 @@
7
7
  import express from 'express'
8
8
  import cors from 'cors'
9
9
  import dotenv from 'dotenv'
10
+ import { paymentMiddleware as x402PaymentMiddleware } from 'x402-bch-express'
10
11
  import { fileURLToPath } from 'url'
11
12
  import { dirname, join } from 'path'
12
13
 
@@ -14,6 +15,7 @@ import { dirname, join } from 'path'
14
15
  import config from '../src/config/index.js'
15
16
  import Controllers from '../src/controllers/index.js'
16
17
  import wlogger from '../src/adapters/wlogger.js'
18
+ import { buildX402Routes, getX402Settings } from '../src/config/x402.js'
17
19
 
18
20
  // Load environment variables
19
21
  dotenv.config()
@@ -57,6 +59,8 @@ class Server {
57
59
  // Create an Express instance.
58
60
  const app = express()
59
61
 
62
+ const x402Settings = getX402Settings()
63
+
60
64
  // MIDDLEWARE START
61
65
  app.use(express.json())
62
66
  app.use(express.urlencoded({ extended: true }))
@@ -68,6 +72,25 @@ class Server {
68
72
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
69
73
  }))
70
74
 
75
+ // Wrap all endpoints in x402 middleware. This handles payments for the API calls.
76
+ if (x402Settings.enabled) {
77
+ const routes = buildX402Routes(this.config.apiPrefix)
78
+ const facilitatorOptions = x402Settings.facilitatorUrl
79
+ ? { url: x402Settings.facilitatorUrl }
80
+ : undefined
81
+
82
+ wlogger.info(`x402 middleware enabled; enforcing ${x402Settings.priceSat} satoshis per request`)
83
+ app.use(
84
+ x402PaymentMiddleware(
85
+ x402Settings.serverAddress,
86
+ routes,
87
+ facilitatorOptions
88
+ )
89
+ )
90
+ } else {
91
+ wlogger.info('x402 middleware disabled via configuration')
92
+ }
93
+
71
94
  // Endpoint logging middleware
72
95
  app.use((req, res, next) => {
73
96
  console.log(`Endpoint called: ${req.method} ${req.path}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psf-bch-api",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -15,12 +15,16 @@
15
15
  "license": "MIT",
16
16
  "description": "REST API proxy to Bitcoin Cash infrastructure",
17
17
  "dependencies": {
18
+ "@psf/bch-js": "6.8.3",
18
19
  "axios": "1.7.7",
19
20
  "cors": "2.8.5",
20
21
  "dotenv": "16.3.1",
21
22
  "express": "5.1.0",
23
+ "minimal-slp-wallet": "5.13.3",
24
+ "slp-token-media": "1.2.10",
22
25
  "winston": "3.11.0",
23
- "winston-daily-rotate-file": "4.7.1"
26
+ "winston-daily-rotate-file": "4.7.1",
27
+ "x402-bch-express": "1.1.1"
24
28
  },
25
29
  "devDependencies": {
26
30
  "apidoc": "1.2.0",
@@ -0,0 +1,124 @@
1
+ /*
2
+ Adapter library for interacting with Fulcrum API service over HTTP.
3
+ */
4
+
5
+ import axios from 'axios'
6
+ import wlogger from './wlogger.js'
7
+ import config from '../config/index.js'
8
+
9
+ class FulcrumAPIAdapter {
10
+ constructor (localConfig = {}) {
11
+ this.config = localConfig.config || config
12
+
13
+ // Allow missing config for testing environments
14
+ if (!this.config.fulcrumApi || !this.config.fulcrumApi.baseUrl) {
15
+ if (process.env.NODE_ENV === 'test' || process.env.TEST) {
16
+ // In test environment, create a mock baseURL
17
+ this.config.fulcrumApi = {
18
+ baseUrl: 'http://localhost:50001',
19
+ timeoutMs: 15000
20
+ }
21
+ } else {
22
+ throw new Error('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
23
+ }
24
+ }
25
+
26
+ const {
27
+ baseUrl,
28
+ timeoutMs = 15000
29
+ } = this.config.fulcrumApi
30
+
31
+ this.http = axios.create({
32
+ baseURL: baseUrl,
33
+ timeout: timeoutMs
34
+ })
35
+ }
36
+
37
+ async get (path) {
38
+ try {
39
+ const response = await this.http.get(path)
40
+ return response.data
41
+ } catch (err) {
42
+ throw this._handleError(err)
43
+ }
44
+ }
45
+
46
+ async post (path, data) {
47
+ try {
48
+ const response = await this.http.post(path, data)
49
+ return response.data
50
+ } catch (err) {
51
+ throw this._handleError(err)
52
+ }
53
+ }
54
+
55
+ _handleError (err) {
56
+ const { status, message } = this.decodeError(err)
57
+ const error = new Error(message)
58
+ error.status = status
59
+ error.originalError = err
60
+ return error
61
+ }
62
+
63
+ decodeError (err) {
64
+ try {
65
+ // Attempt to extract error message from response data
66
+ if (err.response && err.response.data) {
67
+ const data = err.response.data
68
+ // Handle structured error responses
69
+ if (data.error) {
70
+ return this._formatError(data.error, err.response.status || 400)
71
+ }
72
+ // Handle string error messages
73
+ if (typeof data === 'string') {
74
+ return this._formatError(data, err.response.status || 400)
75
+ }
76
+ // Handle object responses that might contain error info
77
+ if (typeof data === 'object' && data.message) {
78
+ return this._formatError(data.message, err.response.status || 400)
79
+ }
80
+ // Fallback to returning the status
81
+ return this._formatError('Fulcrum API error', err.response.status || 500)
82
+ }
83
+
84
+ // Network errors
85
+ if (err.message) {
86
+ if (err.message.includes('ENOTFOUND') || err.message.includes('ENETUNREACH') || err.message.includes('EAI_AGAIN')) {
87
+ return this._formatError(
88
+ 'Network error: Could not communicate with Fulcrum API service.',
89
+ 503
90
+ )
91
+ }
92
+ }
93
+
94
+ if (err.code && (err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')) {
95
+ return this._formatError(
96
+ 'Network error: Could not communicate with Fulcrum API service.',
97
+ 503
98
+ )
99
+ }
100
+
101
+ if (err.error && typeof err.error === 'string' && err.error.includes('429')) {
102
+ return this._formatError('429 Too Many Requests', 429)
103
+ }
104
+
105
+ if (err.message) {
106
+ return this._formatError(err.message, err.status || 422)
107
+ }
108
+
109
+ return this._formatError('Unhandled Fulcrum API error', 500)
110
+ } catch (decodeError) {
111
+ wlogger.error('Unhandled error in FulcrumAPIAdapter.decodeError()', decodeError)
112
+ return this._formatError('Internal server error', 500)
113
+ }
114
+ }
115
+
116
+ _formatError (message, status = 500) {
117
+ return {
118
+ message: message || 'Internal server error',
119
+ status: status || 500
120
+ }
121
+ }
122
+ }
123
+
124
+ export default FulcrumAPIAdapter
@@ -113,12 +113,8 @@ class FullNodeRPCAdapter {
113
113
  }
114
114
  }
115
115
 
116
- validateArraySize (length, options = {}) {
117
- const { isProUser = false } = options
118
- const freemiumLimit = Number(this.config.fullNode?.freemiumArrayLimit || 20)
119
- const proLimit = Number(this.config.fullNode?.proArrayLimit || freemiumLimit)
120
-
121
- const limit = isProUser ? proLimit : freemiumLimit
116
+ validateArraySize (length) {
117
+ const limit = 20
122
118
  return length <= limit
123
119
  }
124
120
 
@@ -7,6 +7,8 @@
7
7
  // Load individual adapter libraries.
8
8
  // import NostrRelayAdapter from './nostr-relay.js'
9
9
  import FullNodeRPCAdapter from './full-node-rpc.js'
10
+ import FulcrumAPIAdapter from './fulcrum-api.js'
11
+ import SlpIndexerAPIAdapter from './slp-indexer-api.js'
10
12
  import config from '../config/index.js'
11
13
 
12
14
  class Adapters {
@@ -33,6 +35,8 @@ class Adapters {
33
35
  // this.nostrRelay = this.nostrRelays[0]
34
36
 
35
37
  this.fullNode = new FullNodeRPCAdapter({ config: this.config })
38
+ this.fulcrum = new FulcrumAPIAdapter({ config: this.config })
39
+ this.slpIndexer = new SlpIndexerAPIAdapter({ config: this.config })
36
40
  }
37
41
 
38
42
  async start () {
@@ -0,0 +1,124 @@
1
+ /*
2
+ Adapter library for interacting with SLP Indexer API service over HTTP.
3
+ */
4
+
5
+ import axios from 'axios'
6
+ import wlogger from './wlogger.js'
7
+ import config from '../config/index.js'
8
+
9
+ class SlpIndexerAPIAdapter {
10
+ constructor (localConfig = {}) {
11
+ this.config = localConfig.config || config
12
+
13
+ // Allow missing config for testing environments
14
+ if (!this.config.slpIndexerApi || !this.config.slpIndexerApi.baseUrl) {
15
+ if (process.env.NODE_ENV === 'test' || process.env.TEST) {
16
+ // In test environment, create a mock baseURL
17
+ this.config.slpIndexerApi = {
18
+ baseUrl: 'http://localhost:5021',
19
+ timeoutMs: 15000
20
+ }
21
+ } else {
22
+ throw new Error('SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.')
23
+ }
24
+ }
25
+
26
+ const {
27
+ baseUrl,
28
+ timeoutMs = 15000
29
+ } = this.config.slpIndexerApi
30
+
31
+ this.http = axios.create({
32
+ baseURL: baseUrl,
33
+ timeout: timeoutMs
34
+ })
35
+ }
36
+
37
+ async get (path) {
38
+ try {
39
+ const response = await this.http.get(path)
40
+ return response.data
41
+ } catch (err) {
42
+ throw this._handleError(err)
43
+ }
44
+ }
45
+
46
+ async post (path, data) {
47
+ try {
48
+ const response = await this.http.post(path, data)
49
+ return response.data
50
+ } catch (err) {
51
+ throw this._handleError(err)
52
+ }
53
+ }
54
+
55
+ _handleError (err) {
56
+ const { status, message } = this.decodeError(err)
57
+ const error = new Error(message)
58
+ error.status = status
59
+ error.originalError = err
60
+ return error
61
+ }
62
+
63
+ decodeError (err) {
64
+ try {
65
+ // Attempt to extract error message from response data
66
+ if (err.response && err.response.data) {
67
+ const data = err.response.data
68
+ // Handle structured error responses
69
+ if (data.error) {
70
+ return this._formatError(data.error, err.response.status || 400)
71
+ }
72
+ // Handle string error messages
73
+ if (typeof data === 'string') {
74
+ return this._formatError(data, err.response.status || 400)
75
+ }
76
+ // Handle object responses that might contain error info
77
+ if (typeof data === 'object' && data.message) {
78
+ return this._formatError(data.message, err.response.status || 400)
79
+ }
80
+ // Fallback to returning the status
81
+ return this._formatError('SLP Indexer API error', err.response.status || 500)
82
+ }
83
+
84
+ // Network errors
85
+ if (err.message) {
86
+ if (err.message.includes('ENOTFOUND') || err.message.includes('ENETUNREACH') || err.message.includes('EAI_AGAIN')) {
87
+ return this._formatError(
88
+ 'Network error: Could not communicate with SLP Indexer API service.',
89
+ 503
90
+ )
91
+ }
92
+ }
93
+
94
+ if (err.code && (err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')) {
95
+ return this._formatError(
96
+ 'Network error: Could not communicate with SLP Indexer API service.',
97
+ 503
98
+ )
99
+ }
100
+
101
+ if (err.error && typeof err.error === 'string' && err.error.includes('429')) {
102
+ return this._formatError('429 Too Many Requests', 429)
103
+ }
104
+
105
+ if (err.message) {
106
+ return this._formatError(err.message, err.status || 422)
107
+ }
108
+
109
+ return this._formatError('Unhandled SLP Indexer API error', 500)
110
+ } catch (decodeError) {
111
+ wlogger.error('Unhandled error in SlpIndexerAPIAdapter.decodeError()', decodeError)
112
+ return this._formatError('Internal server error', 500)
113
+ }
114
+ }
115
+
116
+ _formatError (message, status = 500) {
117
+ return {
118
+ message: message || 'Internal server error',
119
+ status: status || 500
120
+ }
121
+ }
122
+ }
123
+
124
+ export default SlpIndexerAPIAdapter
@@ -16,6 +16,28 @@ const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../../packag
16
16
 
17
17
  const version = pkgInfo.version
18
18
 
19
+ // This function is used to convert the string input of an environment variable to a boolean value.
20
+ const normalizeBoolean = (value, defaultValue) => {
21
+ if (value === undefined || value === null || value === '') return defaultValue
22
+
23
+ const normalized = String(value).trim().toLowerCase()
24
+ if (['false', '0', 'no', 'off'].includes(normalized)) return false
25
+ if (['true', '1', 'yes', 'on'].includes(normalized)) return true
26
+ return defaultValue
27
+ }
28
+
29
+ // By default, the price per API call is 2000 satoshis.
30
+ // But the user can override this value by setting the X402_PRICE_SAT environment variable.
31
+ const parsedPriceSat = Number(process.env.X402_PRICE_SAT)
32
+ const priceSat = Number.isFinite(parsedPriceSat) && parsedPriceSat > 0 ? parsedPriceSat : 2000
33
+
34
+ const x402Defaults = {
35
+ enabled: normalizeBoolean(process.env.X402_ENABLED, true),
36
+ facilitatorUrl: process.env.FACILITATOR_URL || 'http://localhost:4345/facilitator',
37
+ serverAddress: process.env.SERVER_BCH_ADDRESS || 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d',
38
+ priceSat
39
+ }
40
+
19
41
  export default {
20
42
  // Server port
21
43
  port: process.env.PORT || 5942,
@@ -23,33 +45,12 @@ export default {
23
45
  // Environment
24
46
  env: process.env.NODE_ENV || 'development',
25
47
 
48
+ // API prefix for REST controllers
49
+ apiPrefix: process.env.API_PREFIX || '/v6',
50
+
26
51
  // Logging level
27
52
  logLevel: process.env.LOG_LEVEL || 'info',
28
53
 
29
- // Nostr relay configuration (array of relay URLs)
30
- nostrRelayUrls: (() => {
31
- // Support NOSTR_RELAY_URLS (plural) as comma-separated string or JSON array
32
- if (process.env.NOSTR_RELAY_URLS) {
33
- try {
34
- // Try parsing as JSON array first
35
- const parsed = JSON.parse(process.env.NOSTR_RELAY_URLS)
36
- if (Array.isArray(parsed)) {
37
- return parsed.filter(url => url && typeof url === 'string')
38
- }
39
- } catch (e) {
40
- // Not JSON, treat as comma-separated string
41
- return process.env.NOSTR_RELAY_URLS.split(',').map(url => url.trim()).filter(url => url.length > 0)
42
- }
43
- }
44
- // Backward compatibility: support NOSTR_RELAY_URL (singular)
45
- if (process.env.NOSTR_RELAY_URL) {
46
- return [process.env.NOSTR_RELAY_URL]
47
- }
48
-
49
- // Default
50
- return ['wss://nostr-relay.psfoundation.info', 'wss://relay.damus.io']
51
- })(),
52
-
53
54
  // Full node RPC configuration
54
55
  fullNode: {
55
56
  rpcBaseUrl: process.env.RPC_BASEURL || 'http://127.0.0.1:8332',
@@ -59,6 +60,26 @@ export default {
59
60
  rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
60
61
  },
61
62
 
63
+ // Fulcrum API configuration
64
+ fulcrumApi: {
65
+ baseUrl: process.env.FULCRUM_API || '',
66
+ timeoutMs: Number(process.env.FULCRUM_TIMEOUT_MS || 15000)
67
+ },
68
+
69
+ // SLP Indexer API configuration
70
+ slpIndexerApi: {
71
+ baseUrl: process.env.SLP_INDEXER_API || '',
72
+ timeoutMs: Number(process.env.SLP_INDEXER_TIMEOUT_MS || 15000)
73
+ },
74
+
75
+ // REST API URL for wallet operations
76
+ restURL: process.env.REST_URL || process.env.LOCAL_RESTURL || 'http://127.0.0.1:3000/v5/',
77
+
78
+ // IPFS Gateway URL
79
+ ipfsGateway: process.env.IPFS_GATEWAY || 'p2wdb-gateway-678.fullstack.cash',
80
+
81
+ x402: x402Defaults,
82
+
62
83
  // Version
63
84
  version
64
85
  }
@@ -0,0 +1,43 @@
1
+ import config from './index.js'
2
+
3
+ const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
4
+ const DEFAULT_TIMEOUT_SECONDS = 60
5
+ const NETWORK = 'bch'
6
+
7
+ /**
8
+ * Builds a route configuration map for x402-bch middleware.
9
+ *
10
+ * @param {string} apiPrefix Express API prefix (e.g., "/v6")
11
+ * @returns {Object} Routes configuration compatible with x402-bch-express
12
+ */
13
+ export function buildX402Routes (apiPrefix = '/v6') {
14
+ const normalizedPrefix = apiPrefix.endsWith('/')
15
+ ? apiPrefix.slice(0, -1)
16
+ : apiPrefix
17
+ const prefixWithSlash = normalizedPrefix.startsWith('/')
18
+ ? normalizedPrefix
19
+ : `/${normalizedPrefix}`
20
+
21
+ const routeKey = `${prefixWithSlash}/*`
22
+
23
+ return {
24
+ network: NETWORK,
25
+ [routeKey]: {
26
+ price: config.x402.priceSat,
27
+ network: NETWORK,
28
+ config: {
29
+ description: `${DEFAULT_DESCRIPTION} (2000 satoshis)`,
30
+ maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ export function getX402Settings () {
37
+ return {
38
+ enabled: Boolean(config.x402?.enabled),
39
+ facilitatorUrl: config.x402?.facilitatorUrl,
40
+ serverAddress: config.x402?.serverAddress,
41
+ priceSat: config.x402?.priceSat
42
+ }
43
+ }
@@ -18,6 +18,7 @@ class Controllers {
18
18
  this.useCases = new UseCases({ adapters: this.adapters })
19
19
  this.config = config
20
20
  this.timerController = new TimerController({ adapters: this.adapters, useCases: this.useCases })
21
+ this.apiPrefix = this.config.apiPrefix || '/v6'
21
22
 
22
23
  // Bind 'this' object to all subfunctions
23
24
  this.initAdapters = this.initAdapters.bind(this)
@@ -45,7 +46,8 @@ class Controllers {
45
46
  attachRESTControllers (app) {
46
47
  const restControllers = new RESTControllers({
47
48
  adapters: this.adapters,
48
- useCases: this.useCases
49
+ useCases: this.useCases,
50
+ apiPrefix: this.apiPrefix
49
51
  })
50
52
 
51
53
  // Attach the REST API Controllers to the Express app.