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.
- package/.env-local +9 -0
- package/README.md +22 -0
- package/bin/server.js +24 -1
- package/package.json +6 -2
- package/src/adapters/fulcrum-api.js +124 -0
- package/src/adapters/full-node-rpc.js +2 -6
- package/src/adapters/index.js +4 -0
- package/src/adapters/slp-indexer-api.js +124 -0
- package/src/config/env/common.js +45 -24
- package/src/config/x402.js +43 -0
- package/src/controllers/index.js +3 -1
- package/src/controllers/rest-api/fulcrum/controller.js +563 -0
- package/src/controllers/rest-api/fulcrum/router.js +64 -0
- package/src/controllers/rest-api/full-node/blockchain/controller.js +26 -26
- package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +5 -1
- package/src/controllers/rest-api/full-node/control/controller.js +68 -0
- package/src/controllers/rest-api/full-node/control/router.js +51 -0
- package/src/controllers/rest-api/full-node/dsproof/controller.js +90 -0
- package/src/controllers/rest-api/full-node/dsproof/router.js +51 -0
- package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
- package/src/controllers/rest-api/full-node/mining/router.js +52 -0
- package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
- package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
- package/src/controllers/rest-api/index.js +33 -2
- package/src/controllers/rest-api/slp/controller.js +218 -0
- package/src/controllers/rest-api/slp/router.js +55 -0
- package/src/controllers/timer-controller.js +1 -1
- package/src/use-cases/fulcrum-use-cases.js +155 -0
- package/src/use-cases/full-node-control-use-cases.js +24 -0
- package/src/use-cases/full-node-dsproof-use-cases.js +24 -0
- package/src/use-cases/full-node-mining-use-cases.js +28 -0
- package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
- package/src/use-cases/index.js +12 -0
- package/src/use-cases/slp-use-cases.js +321 -0
- package/test/unit/controllers/blockchain-controller-unit.js +2 -3
- package/test/unit/controllers/control-controller-unit.js +88 -0
- package/test/unit/controllers/dsproof-controller-unit.js +117 -0
- package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
- package/test/unit/controllers/mining-controller-unit.js +139 -0
- package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
- package/test/unit/controllers/rest-api-index-unit.js +76 -6
- package/test/unit/controllers/slp-controller-unit.js +312 -0
- package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
- package/test/unit/use-cases/full-node-control-use-cases-unit.js +53 -0
- package/test/unit/use-cases/full-node-dsproof-use-cases-unit.js +54 -0
- package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
- package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
- package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
- package/src/entities/event.js +0 -71
- package/test/integration/api/event-integration.js +0 -250
- package/test/integration/api/req-integration.js +0 -173
- package/test/integration/api/subscription-integration.js +0 -198
- package/test/integration/use-cases/manage-subscription-integration.js +0 -163
- package/test/integration/use-cases/publish-event-integration.js +0 -104
- package/test/integration/use-cases/query-events-integration.js +0 -95
- 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
|
|
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.
|
|
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
|
|
117
|
-
const
|
|
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
|
|
package/src/adapters/index.js
CHANGED
|
@@ -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
|
package/src/config/env/common.js
CHANGED
|
@@ -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
|
+
}
|
package/src/controllers/index.js
CHANGED
|
@@ -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.
|