psf-bch-api 1.2.0 → 7.1.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 +28 -0
- package/bin/server.js +61 -9
- 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 +29 -25
- package/src/config/x402.js +7 -0
- 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 +4 -4
- 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 +23 -3
- package/src/controllers/rest-api/price/controller.js +96 -0
- package/src/controllers/rest-api/price/router.js +52 -0
- 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/middleware/basic-auth.js +61 -0
- package/src/use-cases/fulcrum-use-cases.js +155 -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 +10 -0
- package/src/use-cases/price-use-cases.js +83 -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/fulcrum-controller-unit.js +481 -0
- package/test/unit/controllers/mining-controller-unit.js +139 -0
- package/test/unit/controllers/price-controller-unit.js +116 -0
- package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
- package/test/unit/controllers/rest-api-index-unit.js +67 -3
- 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-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/price-use-cases-unit.js +103 -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/{index.js → psf-bch-api.js} +0 -0
- /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
package/.env-local
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
+
# START INFRASTRUCTURE SETUP
|
|
2
|
+
|
|
1
3
|
# Full Node Connection
|
|
2
4
|
RPC_BASEURL=http://172.17.0.1:8332
|
|
3
5
|
RPC_USERNAME=bitcoin
|
|
4
6
|
RPC_PASSWORD=password
|
|
7
|
+
|
|
8
|
+
# Fulcrum Indexer
|
|
9
|
+
FULCRUM_API=http://172.17.0.1:3001/v1
|
|
10
|
+
|
|
11
|
+
# SLP Indexer
|
|
12
|
+
SLP_INDEXER_API=http://localhost:5010
|
|
13
|
+
|
|
14
|
+
# REST API URL for wallet operations
|
|
15
|
+
LOCAL_RESTURL=http://localhost:5942/v6
|
|
16
|
+
|
|
17
|
+
# END INFRASTRUCTURE SETUP
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# START ACCESS CONTROL
|
|
21
|
+
|
|
22
|
+
# x402 payments required to access this API?
|
|
23
|
+
X402_ENABLED=true
|
|
24
|
+
SERVER_BCH_ADDRESS=bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d
|
|
25
|
+
FACILITATOR_URL=http://localhost:4345/facilitator
|
|
26
|
+
|
|
27
|
+
# Basic Authentication required to access this API?
|
|
28
|
+
USE_BASIC_AUTH=true
|
|
29
|
+
BASIC_AUTH_TOKEN=some-random-token
|
|
30
|
+
|
|
31
|
+
# END ACCESS CONTROL
|
|
32
|
+
|
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
|
|
|
@@ -15,7 +15,8 @@ import { dirname, join } from 'path'
|
|
|
15
15
|
import config from '../src/config/index.js'
|
|
16
16
|
import Controllers from '../src/controllers/index.js'
|
|
17
17
|
import wlogger from '../src/adapters/wlogger.js'
|
|
18
|
-
import { buildX402Routes, getX402Settings } from '../src/config/x402.js'
|
|
18
|
+
import { buildX402Routes, getX402Settings, getBasicAuthSettings } from '../src/config/x402.js'
|
|
19
|
+
import { basicAuthMiddleware } from '../src/middleware/basic-auth.js'
|
|
19
20
|
|
|
20
21
|
// Load environment variables
|
|
21
22
|
dotenv.config()
|
|
@@ -60,6 +61,7 @@ class Server {
|
|
|
60
61
|
const app = express()
|
|
61
62
|
|
|
62
63
|
const x402Settings = getX402Settings()
|
|
64
|
+
const basicAuthSettings = getBasicAuthSettings()
|
|
63
65
|
|
|
64
66
|
// MIDDLEWARE START
|
|
65
67
|
app.use(express.json())
|
|
@@ -72,22 +74,72 @@ class Server {
|
|
|
72
74
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
|
73
75
|
}))
|
|
74
76
|
|
|
75
|
-
if
|
|
77
|
+
// Apply basic auth middleware if enabled
|
|
78
|
+
// This must run before x402 middleware to set req.locals.basicAuthValid
|
|
79
|
+
if (basicAuthSettings.enabled) {
|
|
80
|
+
wlogger.info('Basic auth middleware enabled')
|
|
81
|
+
app.use(basicAuthMiddleware)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply x402 middleware based on configuration
|
|
85
|
+
// Logic:
|
|
86
|
+
// - If X402_ENABLED=false OR USE_BASIC_AUTH=false: Don't apply x402 (no rate limits)
|
|
87
|
+
// - If X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally (bypass if basic auth valid)
|
|
88
|
+
|
|
89
|
+
// Apply access control middleware based on configuration
|
|
90
|
+
if (x402Settings.enabled && basicAuthSettings.enabled) {
|
|
91
|
+
// X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally
|
|
76
92
|
const routes = buildX402Routes(this.config.apiPrefix)
|
|
77
93
|
const facilitatorOptions = x402Settings.facilitatorUrl
|
|
78
94
|
? { url: x402Settings.facilitatorUrl }
|
|
79
95
|
: undefined
|
|
80
96
|
|
|
81
|
-
wlogger.info(`x402 middleware enabled; enforcing ${x402Settings.priceSat} satoshis per request`)
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
wlogger.info(`x402 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceSat} satoshis per request (unless basic auth provided)`)
|
|
98
|
+
|
|
99
|
+
// Create conditional x402 middleware that bypasses if basic auth is valid
|
|
100
|
+
const conditionalX402Middleware = (req, res, next) => {
|
|
101
|
+
// If basic auth is valid, bypass x402
|
|
102
|
+
if (req.locals?.basicAuthValid === true) {
|
|
103
|
+
return next()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Otherwise, apply x402 middleware
|
|
107
|
+
return x402PaymentMiddleware(
|
|
84
108
|
x402Settings.serverAddress,
|
|
85
109
|
routes,
|
|
86
110
|
facilitatorOptions
|
|
87
|
-
)
|
|
88
|
-
|
|
111
|
+
)(req, res, next)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
app.use(conditionalX402Middleware)
|
|
115
|
+
} else if (basicAuthSettings.enabled && !x402Settings.enabled) {
|
|
116
|
+
// USE_BASIC_AUTH=true AND X402_ENABLED=false: Require basic auth, reject unauthenticated requests
|
|
117
|
+
wlogger.info('Basic auth enforcement enabled (x402 disabled)')
|
|
118
|
+
|
|
119
|
+
// Middleware that rejects requests without valid basic auth
|
|
120
|
+
const requireBasicAuthMiddleware = (req, res, next) => {
|
|
121
|
+
// Skip auth check for health endpoint and root
|
|
122
|
+
if (req.path === '/health' || req.path === '/') {
|
|
123
|
+
return next()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If basic auth is valid, allow the request
|
|
127
|
+
if (req.locals?.basicAuthValid === true) {
|
|
128
|
+
return next()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Reject unauthenticated requests
|
|
132
|
+
wlogger.warn(`Unauthenticated request rejected: ${req.method} ${req.path}`)
|
|
133
|
+
return res.status(401).json({
|
|
134
|
+
error: 'Unauthorized',
|
|
135
|
+
message: 'Valid Bearer token required in Authorization header'
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
app.use(requireBasicAuthMiddleware)
|
|
89
140
|
} else {
|
|
90
|
-
|
|
141
|
+
// X402_ENABLED=false AND USE_BASIC_AUTH=false: No access control middleware
|
|
142
|
+
wlogger.info('No access control middleware enabled')
|
|
91
143
|
}
|
|
92
144
|
|
|
93
145
|
// Endpoint logging middleware
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "psf-bch-api",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"main": "
|
|
3
|
+
"version": "7.1.0",
|
|
4
|
+
"main": "psf-bch-api.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node bin/server.js",
|
|
@@ -15,10 +15,14 @@
|
|
|
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
|
+
"psffpp": "1.2.0",
|
|
25
|
+
"slp-token-media": "1.2.10",
|
|
22
26
|
"winston": "3.11.0",
|
|
23
27
|
"winston-daily-rotate-file": "4.7.1",
|
|
24
28
|
"x402-bch-express": "1.1.1"
|
|
@@ -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,7 @@ 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.
|
|
19
20
|
const normalizeBoolean = (value, defaultValue) => {
|
|
20
21
|
if (value === undefined || value === null || value === '') return defaultValue
|
|
21
22
|
|
|
@@ -25,16 +26,23 @@ const normalizeBoolean = (value, defaultValue) => {
|
|
|
25
26
|
return defaultValue
|
|
26
27
|
}
|
|
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.
|
|
28
31
|
const parsedPriceSat = Number(process.env.X402_PRICE_SAT)
|
|
29
32
|
const priceSat = Number.isFinite(parsedPriceSat) && parsedPriceSat > 0 ? parsedPriceSat : 2000
|
|
30
33
|
|
|
31
34
|
const x402Defaults = {
|
|
32
35
|
enabled: normalizeBoolean(process.env.X402_ENABLED, true),
|
|
33
36
|
facilitatorUrl: process.env.FACILITATOR_URL || 'http://localhost:4345/facilitator',
|
|
34
|
-
serverAddress: process.env.SERVER_BCH_ADDRESS || 'bitcoincash:
|
|
37
|
+
serverAddress: process.env.SERVER_BCH_ADDRESS || 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
|
|
35
38
|
priceSat
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
const basicAuthDefaults = {
|
|
42
|
+
enabled: normalizeBoolean(process.env.USE_BASIC_AUTH, false),
|
|
43
|
+
token: process.env.BASIC_AUTH_TOKEN || ''
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
export default {
|
|
39
47
|
// Server port
|
|
40
48
|
port: process.env.PORT || 5942,
|
|
@@ -48,30 +56,6 @@ export default {
|
|
|
48
56
|
// Logging level
|
|
49
57
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
50
58
|
|
|
51
|
-
// Nostr relay configuration (array of relay URLs)
|
|
52
|
-
nostrRelayUrls: (() => {
|
|
53
|
-
// Support NOSTR_RELAY_URLS (plural) as comma-separated string or JSON array
|
|
54
|
-
if (process.env.NOSTR_RELAY_URLS) {
|
|
55
|
-
try {
|
|
56
|
-
// Try parsing as JSON array first
|
|
57
|
-
const parsed = JSON.parse(process.env.NOSTR_RELAY_URLS)
|
|
58
|
-
if (Array.isArray(parsed)) {
|
|
59
|
-
return parsed.filter(url => url && typeof url === 'string')
|
|
60
|
-
}
|
|
61
|
-
} catch (e) {
|
|
62
|
-
// Not JSON, treat as comma-separated string
|
|
63
|
-
return process.env.NOSTR_RELAY_URLS.split(',').map(url => url.trim()).filter(url => url.length > 0)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// Backward compatibility: support NOSTR_RELAY_URL (singular)
|
|
67
|
-
if (process.env.NOSTR_RELAY_URL) {
|
|
68
|
-
return [process.env.NOSTR_RELAY_URL]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Default
|
|
72
|
-
return ['wss://nostr-relay.psfoundation.info', 'wss://relay.damus.io']
|
|
73
|
-
})(),
|
|
74
|
-
|
|
75
59
|
// Full node RPC configuration
|
|
76
60
|
fullNode: {
|
|
77
61
|
rpcBaseUrl: process.env.RPC_BASEURL || 'http://127.0.0.1:8332',
|
|
@@ -81,8 +65,28 @@ export default {
|
|
|
81
65
|
rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
|
|
82
66
|
},
|
|
83
67
|
|
|
68
|
+
// Fulcrum API configuration
|
|
69
|
+
fulcrumApi: {
|
|
70
|
+
baseUrl: process.env.FULCRUM_API || '',
|
|
71
|
+
timeoutMs: Number(process.env.FULCRUM_TIMEOUT_MS || 15000)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// SLP Indexer API configuration
|
|
75
|
+
slpIndexerApi: {
|
|
76
|
+
baseUrl: process.env.SLP_INDEXER_API || '',
|
|
77
|
+
timeoutMs: Number(process.env.SLP_INDEXER_TIMEOUT_MS || 15000)
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// REST API URL for wallet operations
|
|
81
|
+
restURL: process.env.REST_URL || process.env.LOCAL_RESTURL || 'http://127.0.0.1:5942/v6/',
|
|
82
|
+
|
|
83
|
+
// IPFS Gateway URL
|
|
84
|
+
ipfsGateway: process.env.IPFS_GATEWAY || 'p2wdb-gateway-678.fullstack.cash',
|
|
85
|
+
|
|
84
86
|
x402: x402Defaults,
|
|
85
87
|
|
|
88
|
+
basicAuth: basicAuthDefaults,
|
|
89
|
+
|
|
86
90
|
// Version
|
|
87
91
|
version
|
|
88
92
|
}
|
package/src/config/x402.js
CHANGED