psf-bch-api 1.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 +4 -0
- package/LICENSE.md +8 -0
- package/README.md +8 -0
- package/apidoc.json +9 -0
- package/bin/server.js +183 -0
- package/dev-docs/README.md +4 -0
- package/dev-docs/creation-prompt.md +34 -0
- package/dev-docs/rest2nostr-poxy-api.plan.md +163 -0
- package/dev-docs/test-plan-for-rest2nostr.plan.md +161 -0
- package/dev-docs/unit-test-prompt.md +13 -0
- package/examples/01-create-account.js +67 -0
- package/examples/02-read-posts.js +44 -0
- package/examples/03-write-post.js +55 -0
- package/examples/04-read-alice-posts.js +49 -0
- package/examples/05-get-follow-list.js +53 -0
- package/examples/06-update-follow-list.js +63 -0
- package/examples/07-liking-event.js +59 -0
- package/examples/README.md +90 -0
- package/index.js +11 -0
- package/package.json +37 -0
- package/production/docker/Dockerfile +85 -0
- package/production/docker/cleanup-images.sh +5 -0
- package/production/docker/docker-compose.yml +19 -0
- package/production/docker/start-rest2nostr.sh +3 -0
- package/src/adapters/full-node-rpc.js +133 -0
- package/src/adapters/index.js +217 -0
- package/src/adapters/wlogger.js +79 -0
- package/src/config/env/common.js +64 -0
- package/src/config/env/development.js +7 -0
- package/src/config/env/production.js +7 -0
- package/src/config/index.js +14 -0
- package/src/controllers/index.js +56 -0
- package/src/controllers/rest-api/full-node/blockchain/controller.js +553 -0
- package/src/controllers/rest-api/full-node/blockchain/index.js +66 -0
- package/src/controllers/rest-api/index.js +55 -0
- package/src/controllers/timer-controller.js +72 -0
- package/src/entities/event.js +71 -0
- package/src/use-cases/full-node-blockchain-use-cases.js +134 -0
- package/src/use-cases/index.js +29 -0
- package/test/integration/api/event-integration.js +250 -0
- package/test/integration/api/req-integration.js +173 -0
- package/test/integration/api/subscription-integration.js +198 -0
- package/test/integration/use-cases/manage-subscription-integration.js +163 -0
- package/test/integration/use-cases/publish-event-integration.js +104 -0
- package/test/integration/use-cases/query-events-integration.js +95 -0
- package/test/unit/adapters/full-node-rpc-unit.js +122 -0
- package/test/unit/bin/server-unit.js +63 -0
- package/test/unit/controllers/blockchain-controller-unit.js +215 -0
- package/test/unit/controllers/rest-api-index-unit.js +85 -0
- package/test/unit/entities/event-unit.js +139 -0
- package/test/unit/mocks/controller-mocks.js +98 -0
- package/test/unit/mocks/event-mocks.js +194 -0
- package/test/unit/use-cases/full-node-blockchain-use-cases-unit.js +137 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Adapter library for interacting with a BCH full node over JSON-RPC.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
import wlogger from './wlogger.js'
|
|
7
|
+
import config from '../config/index.js'
|
|
8
|
+
|
|
9
|
+
class FullNodeRPCAdapter {
|
|
10
|
+
constructor (localConfig = {}) {
|
|
11
|
+
this.config = localConfig.config || config
|
|
12
|
+
|
|
13
|
+
if (!this.config.fullNode || !this.config.fullNode.rpcBaseUrl) {
|
|
14
|
+
throw new Error('Full node RPC configuration is required')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
rpcBaseUrl,
|
|
19
|
+
rpcUsername,
|
|
20
|
+
rpcPassword,
|
|
21
|
+
rpcTimeoutMs = 15000
|
|
22
|
+
} = this.config.fullNode
|
|
23
|
+
|
|
24
|
+
this.requestIdPrefix = this.config.fullNode.rpcRequestIdPrefix || 'psf-bch-api'
|
|
25
|
+
|
|
26
|
+
this.http = axios.create({
|
|
27
|
+
baseURL: rpcBaseUrl,
|
|
28
|
+
timeout: rpcTimeoutMs,
|
|
29
|
+
auth: {
|
|
30
|
+
username: rpcUsername,
|
|
31
|
+
password: rpcPassword
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
this.defaultRequestPayload = {
|
|
36
|
+
jsonrpc: '1.0'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async call (method, params = [], requestId) {
|
|
41
|
+
const id = requestId || `${this.requestIdPrefix}-${method}`
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.http.post('', {
|
|
45
|
+
...this.defaultRequestPayload,
|
|
46
|
+
id,
|
|
47
|
+
method,
|
|
48
|
+
params
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (response.data && response.data.error) {
|
|
52
|
+
const rpcError = this._formatError(response.data.error.message, 400)
|
|
53
|
+
throw rpcError
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return response.data.result
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw this._handleError(err)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_handleError (err) {
|
|
63
|
+
const { status, message } = this.decodeError(err)
|
|
64
|
+
const error = new Error(message)
|
|
65
|
+
error.status = status
|
|
66
|
+
error.originalError = err
|
|
67
|
+
return error
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
decodeError (err) {
|
|
71
|
+
try {
|
|
72
|
+
if (
|
|
73
|
+
err.response &&
|
|
74
|
+
err.response.data &&
|
|
75
|
+
err.response.data.error &&
|
|
76
|
+
err.response.data.error.message
|
|
77
|
+
) {
|
|
78
|
+
return this._formatError(err.response.data.error.message, 400)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (err.response && err.response.data) {
|
|
82
|
+
return this._formatError(err.response.data, err.response.status || 500)
|
|
83
|
+
}
|
|
84
|
+
|
|
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 full node or other external 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 full node or other external 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 full node error', 500)
|
|
110
|
+
} catch (decodeError) {
|
|
111
|
+
wlogger.error('Unhandled error in FullNodeRPCAdapter.decodeError()', decodeError)
|
|
112
|
+
return this._formatError('Internal server error', 500)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
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
|
|
122
|
+
return length <= limit
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_formatError (message, status = 500) {
|
|
126
|
+
return {
|
|
127
|
+
message: message || 'Internal server error',
|
|
128
|
+
status: status || 500
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default FullNodeRPCAdapter
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a top-level library that encapsulates all the additional Adapters.
|
|
3
|
+
The concept of Adapters comes from Clean Architecture:
|
|
4
|
+
https://troutsblog.com/blog/clean-architecture
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Load individual adapter libraries.
|
|
8
|
+
// import NostrRelayAdapter from './nostr-relay.js'
|
|
9
|
+
import FullNodeRPCAdapter from './full-node-rpc.js'
|
|
10
|
+
import config from '../config/index.js'
|
|
11
|
+
|
|
12
|
+
class Adapters {
|
|
13
|
+
constructor (localConfig = {}) {
|
|
14
|
+
// Encapsulate dependencies
|
|
15
|
+
this.config = config
|
|
16
|
+
|
|
17
|
+
// Determine relay URLs: prefer localConfig, fall back to config
|
|
18
|
+
// let relayUrls = []
|
|
19
|
+
// if (localConfig.relayUrls && Array.isArray(localConfig.relayUrls)) {
|
|
20
|
+
// relayUrls = localConfig.relayUrls
|
|
21
|
+
// } else if (localConfig.relayUrl) {
|
|
22
|
+
// // Backward compatibility: single relay URL
|
|
23
|
+
// relayUrls = [localConfig.relayUrl]
|
|
24
|
+
// } else {
|
|
25
|
+
// relayUrls = config.nostrRelayUrls
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
// Create one adapter per relay URL
|
|
29
|
+
// this.nostrRelays = relayUrls.map(relayUrl => new NostrRelayAdapter({ relayUrl }))
|
|
30
|
+
|
|
31
|
+
// Maintain backward compatibility: expose first relay as nostrRelay
|
|
32
|
+
// This allows existing code to work during transition
|
|
33
|
+
// this.nostrRelay = this.nostrRelays[0]
|
|
34
|
+
|
|
35
|
+
this.fullNode = new FullNodeRPCAdapter({ config: this.config })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async start () {
|
|
39
|
+
// try {
|
|
40
|
+
// Connect to all Nostr relays concurrently
|
|
41
|
+
// const connectPromises = this.nostrRelays.map(async (relay, index) => {
|
|
42
|
+
// try {
|
|
43
|
+
// await relay.connect()
|
|
44
|
+
// console.log(`Nostr relay adapter ${index + 1}/${this.nostrRelays.length} started: ${relay.relayUrl}`)
|
|
45
|
+
// return { success: true, relay }
|
|
46
|
+
// } catch (err) {
|
|
47
|
+
// console.error(`Failed to connect to relay ${relay.relayUrl}:`, err.message)
|
|
48
|
+
// return { success: false, relay, error: err }
|
|
49
|
+
// }
|
|
50
|
+
// })
|
|
51
|
+
|
|
52
|
+
// const results = await Promise.allSettled(connectPromises)
|
|
53
|
+
|
|
54
|
+
// const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length
|
|
55
|
+
// const failed = results.length - successful
|
|
56
|
+
|
|
57
|
+
// if (successful === 0) {
|
|
58
|
+
// throw new Error('Failed to connect to any Nostr relay')
|
|
59
|
+
// }
|
|
60
|
+
|
|
61
|
+
// if (failed > 0) {
|
|
62
|
+
// console.warn(`Connected to ${successful}/${this.nostrRelays.length} relays. Some relays failed to connect.`)
|
|
63
|
+
// } else {
|
|
64
|
+
// console.log(`All ${successful} Nostr relay adapters started successfully.`)
|
|
65
|
+
// }
|
|
66
|
+
|
|
67
|
+
return true
|
|
68
|
+
// } catch (err) {
|
|
69
|
+
// console.error('Error in adapters/index.js/start()')
|
|
70
|
+
// throw err
|
|
71
|
+
// }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get all relay adapters
|
|
76
|
+
* @returns {Array<NostrRelayAdapter>}
|
|
77
|
+
*/
|
|
78
|
+
getRelays () {
|
|
79
|
+
return this.nostrRelays
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Broadcast an event to all relays
|
|
84
|
+
* @param {Object} event - Event object to broadcast
|
|
85
|
+
* @returns {Promise<Array>} Array of results from each relay: { accepted, message, relayUrl }
|
|
86
|
+
*/
|
|
87
|
+
async broadcastEvent (event) {
|
|
88
|
+
const broadcastPromises = this.nostrRelays.map(async (relay) => {
|
|
89
|
+
try {
|
|
90
|
+
const result = await relay.sendEvent(event)
|
|
91
|
+
return {
|
|
92
|
+
accepted: result.accepted,
|
|
93
|
+
message: result.message || '',
|
|
94
|
+
relayUrl: relay.relayUrl,
|
|
95
|
+
success: true
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
accepted: false,
|
|
100
|
+
message: err.message || 'Error broadcasting to relay',
|
|
101
|
+
relayUrl: relay.relayUrl,
|
|
102
|
+
success: false,
|
|
103
|
+
error: err
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return Promise.allSettled(broadcastPromises).then(results => {
|
|
109
|
+
return results.map((result, index) => {
|
|
110
|
+
if (result.status === 'fulfilled') {
|
|
111
|
+
return result.value
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
accepted: false,
|
|
115
|
+
message: result.reason?.message || 'Unknown error',
|
|
116
|
+
relayUrl: this.nostrRelays[index].relayUrl,
|
|
117
|
+
success: false,
|
|
118
|
+
error: result.reason
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Query all relays concurrently and merge results
|
|
127
|
+
* @param {Array} filters - Array of filter objects
|
|
128
|
+
* @param {string} subscriptionId - Unique subscription ID (will be modified per relay)
|
|
129
|
+
* @returns {Promise<Array>} Merged and de-duplicated array of events
|
|
130
|
+
*/
|
|
131
|
+
async queryAllRelays (filters, subscriptionId) {
|
|
132
|
+
// Create unique subscription IDs for each relay
|
|
133
|
+
const subscriptionIds = this.nostrRelays.map((relay, index) =>
|
|
134
|
+
`${subscriptionId}-relay-${index}`
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
// Collect events from all relays
|
|
138
|
+
const allEvents = []
|
|
139
|
+
const relayStatuses = this.nostrRelays.map(() => ({
|
|
140
|
+
eoseReceived: false,
|
|
141
|
+
closedReceived: false,
|
|
142
|
+
closedMessage: ''
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
// Query all relays concurrently
|
|
146
|
+
const queryPromises = this.nostrRelays.map(async (relay, index) => {
|
|
147
|
+
const subscriptionIdForRelay = subscriptionIds[index]
|
|
148
|
+
const status = relayStatuses[index]
|
|
149
|
+
|
|
150
|
+
// Create handlers for this relay
|
|
151
|
+
const handlers = {
|
|
152
|
+
onEvent: (event) => {
|
|
153
|
+
allEvents.push(event)
|
|
154
|
+
},
|
|
155
|
+
onEose: () => {
|
|
156
|
+
status.eoseReceived = true
|
|
157
|
+
},
|
|
158
|
+
onClosed: (message) => {
|
|
159
|
+
status.closedReceived = true
|
|
160
|
+
status.closedMessage = message
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await relay.sendReq(subscriptionIdForRelay, filters, handlers)
|
|
166
|
+
|
|
167
|
+
// Wait for EOSE or CLOSED with timeout
|
|
168
|
+
const timeout = 30000 // 30 seconds
|
|
169
|
+
const startTime = Date.now()
|
|
170
|
+
|
|
171
|
+
while (!status.eoseReceived && !status.closedReceived && (Date.now() - startTime) < timeout) {
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Clean up subscription - always try to close, even if EOSE didn't come
|
|
176
|
+
try {
|
|
177
|
+
await relay.sendClose(subscriptionIdForRelay)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
// Ignore close errors
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (status.closedReceived) {
|
|
183
|
+
throw new Error(`Subscription closed on ${relay.relayUrl}: ${status.closedMessage}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// If EOSE never came but timeout expired, that's OK - we proceed with what we got
|
|
187
|
+
if (!status.eoseReceived && (Date.now() - startTime) >= timeout) {
|
|
188
|
+
// Timeout reached without EOSE - proceed anyway with events collected so far
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Ensure cleanup even on error
|
|
192
|
+
try {
|
|
193
|
+
await relay.sendClose(subscriptionIdForRelay)
|
|
194
|
+
} catch (closeErr) {
|
|
195
|
+
// Ignore close errors
|
|
196
|
+
}
|
|
197
|
+
// Log error but don't fail the entire query
|
|
198
|
+
console.warn(`Query failed for relay ${relay.relayUrl}:`, err.message)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Wait for all queries to complete
|
|
203
|
+
await Promise.allSettled(queryPromises)
|
|
204
|
+
|
|
205
|
+
// Merge and de-duplicate events by event ID
|
|
206
|
+
const eventMap = new Map()
|
|
207
|
+
allEvents.forEach(event => {
|
|
208
|
+
if (event && event.id && !eventMap.has(event.id)) {
|
|
209
|
+
eventMap.set(event.id, event)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return Array.from(eventMap.values())
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default Adapters
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Instantiates and configures the Winston logging library. This utility library
|
|
3
|
+
can be called by other parts of the application to conveniently tap into the
|
|
4
|
+
logging library.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Global npm libraries
|
|
8
|
+
import winston from 'winston'
|
|
9
|
+
import 'winston-daily-rotate-file'
|
|
10
|
+
|
|
11
|
+
// Local libraries
|
|
12
|
+
import config from '../config/index.js'
|
|
13
|
+
|
|
14
|
+
// Hack to get __dirname back.
|
|
15
|
+
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
|
16
|
+
import * as url from 'url'
|
|
17
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
|
18
|
+
|
|
19
|
+
let _this = null
|
|
20
|
+
|
|
21
|
+
class Wlogger {
|
|
22
|
+
constructor (localConfig = {}) {
|
|
23
|
+
this.config = config
|
|
24
|
+
|
|
25
|
+
// Configure daily-rotation transport.
|
|
26
|
+
this.transport = new winston.transports.DailyRotateFile({
|
|
27
|
+
filename: `${__dirname.toString()}/../../logs/rest2nostr-${
|
|
28
|
+
this.config.env
|
|
29
|
+
}-%DATE%.log`,
|
|
30
|
+
datePattern: 'YYYY-MM-DD',
|
|
31
|
+
zippedArchive: false,
|
|
32
|
+
maxSize: '1m', // 1 megabyte
|
|
33
|
+
maxFiles: '5d', // 5 days
|
|
34
|
+
format: winston.format.combine(
|
|
35
|
+
winston.format.timestamp(),
|
|
36
|
+
winston.format.json()
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
this.transport.on('rotate', this.notifyRotation)
|
|
41
|
+
|
|
42
|
+
// This controls what goes into the log FILES
|
|
43
|
+
this.wlogger = winston.createLogger({
|
|
44
|
+
level: this.config.logLevel || 'info',
|
|
45
|
+
format: winston.format.json(),
|
|
46
|
+
transports: [
|
|
47
|
+
this.transport
|
|
48
|
+
]
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Bind 'this' object to all methods
|
|
52
|
+
this.notifyRotation = this.notifyRotation.bind(this)
|
|
53
|
+
this.outputToConsole = this.outputToConsole.bind(this)
|
|
54
|
+
|
|
55
|
+
_this = this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
notifyRotation (oldFilename, newFilename) {
|
|
59
|
+
_this.wlogger.info('Rotating log files')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
outputToConsole () {
|
|
63
|
+
this.wlogger.add(
|
|
64
|
+
new winston.transports.Console({
|
|
65
|
+
format: winston.format.simple(),
|
|
66
|
+
level: this.config.logLevel || 'info'
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const logger = new Wlogger()
|
|
73
|
+
|
|
74
|
+
// Allow the logger to write to the console.
|
|
75
|
+
logger.outputToConsole()
|
|
76
|
+
|
|
77
|
+
const wlogger = logger.wlogger
|
|
78
|
+
|
|
79
|
+
export { wlogger as default, Wlogger }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is used to store unsecure, application-specific data common to all
|
|
3
|
+
environments.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import dotenv from 'dotenv'
|
|
7
|
+
|
|
8
|
+
// Hack to get __dirname back.
|
|
9
|
+
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
|
10
|
+
import * as url from 'url'
|
|
11
|
+
import { readFileSync } from 'fs'
|
|
12
|
+
dotenv.config()
|
|
13
|
+
|
|
14
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
|
15
|
+
const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../../package.json`))
|
|
16
|
+
|
|
17
|
+
const version = pkgInfo.version
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
// Server port
|
|
21
|
+
port: process.env.PORT || 5942,
|
|
22
|
+
|
|
23
|
+
// Environment
|
|
24
|
+
env: process.env.NODE_ENV || 'development',
|
|
25
|
+
|
|
26
|
+
// Logging level
|
|
27
|
+
logLevel: process.env.LOG_LEVEL || 'info',
|
|
28
|
+
|
|
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
|
+
// Full node RPC configuration
|
|
54
|
+
fullNode: {
|
|
55
|
+
rpcBaseUrl: process.env.RPC_BASEURL || 'http://127.0.0.1:8332',
|
|
56
|
+
rpcUsername: process.env.RPC_USERNAME || '',
|
|
57
|
+
rpcPassword: process.env.RPC_PASSWORD || '',
|
|
58
|
+
rpcTimeoutMs: Number(process.env.RPC_TIMEOUT_MS || 15000),
|
|
59
|
+
rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Version
|
|
63
|
+
version
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import common from './env/common.js'
|
|
2
|
+
|
|
3
|
+
import development from './env/development.js'
|
|
4
|
+
import production from './env/production.js'
|
|
5
|
+
|
|
6
|
+
const env = process.env.NODE_ENV || 'development'
|
|
7
|
+
console.log(`Loading config for this environment: ${env}`)
|
|
8
|
+
|
|
9
|
+
let config = development
|
|
10
|
+
if (env === 'production') {
|
|
11
|
+
config = production
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default Object.assign({}, common, config)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a top-level library that encapsulates all the additional Controllers.
|
|
3
|
+
The concept of Controllers comes from Clean Architecture:
|
|
4
|
+
https://troutsblog.com/blog/clean-architecture
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Local libraries
|
|
8
|
+
import Adapters from '../adapters/index.js'
|
|
9
|
+
import UseCases from '../use-cases/index.js'
|
|
10
|
+
import RESTControllers from './rest-api/index.js'
|
|
11
|
+
import TimerController from './timer-controller.js'
|
|
12
|
+
import config from '../config/index.js'
|
|
13
|
+
|
|
14
|
+
class Controllers {
|
|
15
|
+
constructor (localConfig = {}) {
|
|
16
|
+
// Encapsulate dependencies
|
|
17
|
+
this.adapters = new Adapters(localConfig)
|
|
18
|
+
this.useCases = new UseCases({ adapters: this.adapters })
|
|
19
|
+
this.config = config
|
|
20
|
+
this.timerController = new TimerController({ adapters: this.adapters, useCases: this.useCases })
|
|
21
|
+
|
|
22
|
+
// Bind 'this' object to all subfunctions
|
|
23
|
+
this.initAdapters = this.initAdapters.bind(this)
|
|
24
|
+
this.initUseCases = this.initUseCases.bind(this)
|
|
25
|
+
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Spin up any adapter libraries that have async startup needs.
|
|
29
|
+
async initAdapters () {
|
|
30
|
+
await this.adapters.start()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Run any Use Cases to startup the app.
|
|
34
|
+
async initUseCases () {
|
|
35
|
+
await this.useCases.start()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Initialize all the controllers.
|
|
39
|
+
async initControllers () {
|
|
40
|
+
this.timerController.startTimerControllers()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Top-level function for this library.
|
|
44
|
+
// Start the various Controllers and attach them to the app.
|
|
45
|
+
attachRESTControllers (app) {
|
|
46
|
+
const restControllers = new RESTControllers({
|
|
47
|
+
adapters: this.adapters,
|
|
48
|
+
useCases: this.useCases
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Attach the REST API Controllers to the Express app.
|
|
52
|
+
restControllers.attachRESTControllers(app)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default Controllers
|