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.
Files changed (53) hide show
  1. package/.env-local +4 -0
  2. package/LICENSE.md +8 -0
  3. package/README.md +8 -0
  4. package/apidoc.json +9 -0
  5. package/bin/server.js +183 -0
  6. package/dev-docs/README.md +4 -0
  7. package/dev-docs/creation-prompt.md +34 -0
  8. package/dev-docs/rest2nostr-poxy-api.plan.md +163 -0
  9. package/dev-docs/test-plan-for-rest2nostr.plan.md +161 -0
  10. package/dev-docs/unit-test-prompt.md +13 -0
  11. package/examples/01-create-account.js +67 -0
  12. package/examples/02-read-posts.js +44 -0
  13. package/examples/03-write-post.js +55 -0
  14. package/examples/04-read-alice-posts.js +49 -0
  15. package/examples/05-get-follow-list.js +53 -0
  16. package/examples/06-update-follow-list.js +63 -0
  17. package/examples/07-liking-event.js +59 -0
  18. package/examples/README.md +90 -0
  19. package/index.js +11 -0
  20. package/package.json +37 -0
  21. package/production/docker/Dockerfile +85 -0
  22. package/production/docker/cleanup-images.sh +5 -0
  23. package/production/docker/docker-compose.yml +19 -0
  24. package/production/docker/start-rest2nostr.sh +3 -0
  25. package/src/adapters/full-node-rpc.js +133 -0
  26. package/src/adapters/index.js +217 -0
  27. package/src/adapters/wlogger.js +79 -0
  28. package/src/config/env/common.js +64 -0
  29. package/src/config/env/development.js +7 -0
  30. package/src/config/env/production.js +7 -0
  31. package/src/config/index.js +14 -0
  32. package/src/controllers/index.js +56 -0
  33. package/src/controllers/rest-api/full-node/blockchain/controller.js +553 -0
  34. package/src/controllers/rest-api/full-node/blockchain/index.js +66 -0
  35. package/src/controllers/rest-api/index.js +55 -0
  36. package/src/controllers/timer-controller.js +72 -0
  37. package/src/entities/event.js +71 -0
  38. package/src/use-cases/full-node-blockchain-use-cases.js +134 -0
  39. package/src/use-cases/index.js +29 -0
  40. package/test/integration/api/event-integration.js +250 -0
  41. package/test/integration/api/req-integration.js +173 -0
  42. package/test/integration/api/subscription-integration.js +198 -0
  43. package/test/integration/use-cases/manage-subscription-integration.js +163 -0
  44. package/test/integration/use-cases/publish-event-integration.js +104 -0
  45. package/test/integration/use-cases/query-events-integration.js +95 -0
  46. package/test/unit/adapters/full-node-rpc-unit.js +122 -0
  47. package/test/unit/bin/server-unit.js +63 -0
  48. package/test/unit/controllers/blockchain-controller-unit.js +215 -0
  49. package/test/unit/controllers/rest-api-index-unit.js +85 -0
  50. package/test/unit/entities/event-unit.js +139 -0
  51. package/test/unit/mocks/controller-mocks.js +98 -0
  52. package/test/unit/mocks/event-mocks.js +194 -0
  53. package/test/unit/use-cases/full-node-blockchain-use-cases-unit.js +137 -0
package/.env-local ADDED
@@ -0,0 +1,4 @@
1
+ # Full Node Connection
2
+ RPC_BASEURL=http://172.17.0.1:8332
3
+ RPC_USERNAME=bitcoin
4
+ RPC_PASSWORD=password
package/LICENSE.md ADDED
@@ -0,0 +1,8 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2025 Christopher Troutner
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # psf-bch-api
2
+
3
+ This is a REST API for communicating with Bitcoin Cash infrastructure. It replaces [bch-api](https://github.com/Permissionless-Software-Foundation/bch-api), and it implements [x402-bch protocol](https://github.com/x402-bch/x402-bch) to handle payments to access the API.
4
+
5
+ ## License
6
+
7
+ [MIT](./LICENSE.md)
8
+
package/apidoc.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "psf-bch-api REST API",
3
+ "version": "1.0.0",
4
+ "description": "REST API proxy to Bitcoin Cash infrastructure",
5
+ "title": "psf-bch-api REST API",
6
+ "url": "http://localhost:5942",
7
+ "sampleUrl": "http://localhost:5942"
8
+ }
9
+
package/bin/server.js ADDED
@@ -0,0 +1,183 @@
1
+ /*
2
+ Express server for REST2NOSTR Proxy API.
3
+ The architecture of the code follows the Clean Architecture pattern.
4
+ */
5
+
6
+ // npm libraries
7
+ import express from 'express'
8
+ import cors from 'cors'
9
+ import dotenv from 'dotenv'
10
+ import { fileURLToPath } from 'url'
11
+ import { dirname, join } from 'path'
12
+
13
+ // Local libraries
14
+ import config from '../src/config/index.js'
15
+ import Controllers from '../src/controllers/index.js'
16
+ import wlogger from '../src/adapters/wlogger.js'
17
+
18
+ // Load environment variables
19
+ dotenv.config()
20
+
21
+ // Set up global error handlers to prevent server crashes
22
+ // These must be set up before the server starts to catch any unhandled errors
23
+ process.on('unhandledRejection', (reason, promise) => {
24
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason)
25
+ wlogger.error('Unhandled Rejection:', {
26
+ promise: promise.toString(),
27
+ reason: reason instanceof Error ? reason.stack : String(reason)
28
+ })
29
+ // Don't exit the process - log and continue
30
+ // The server should remain running to handle other requests
31
+ })
32
+
33
+ process.on('uncaughtException', (error) => {
34
+ console.error('Uncaught Exception:', error)
35
+ wlogger.error('Uncaught Exception:', {
36
+ message: error.message,
37
+ stack: error.stack
38
+ })
39
+ // For uncaught exceptions, we should exit gracefully
40
+ // but give time for the process manager to restart
41
+ console.log('Exiting after 5 seconds due to uncaught exception. Process manager should restart.')
42
+ setTimeout(() => {
43
+ process.exit(1)
44
+ }, 5000)
45
+ })
46
+
47
+ class Server {
48
+ constructor () {
49
+ // Encapsulate dependencies
50
+ this.controllers = new Controllers()
51
+ this.config = config
52
+ this.process = process
53
+ }
54
+
55
+ async startServer () {
56
+ try {
57
+ // Create an Express instance.
58
+ const app = express()
59
+
60
+ // MIDDLEWARE START
61
+ app.use(express.json())
62
+ app.use(express.urlencoded({ extended: true }))
63
+
64
+ app.use(cors({
65
+ origin: true, // Allow all origins (more reliable than '*')
66
+ credentials: false, // Set to true if you need to support credentials
67
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
68
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
69
+ }))
70
+
71
+ // Endpoint logging middleware
72
+ app.use((req, res, next) => {
73
+ console.log(`Endpoint called: ${req.method} ${req.path}`)
74
+ res.on('finish', () => {
75
+ console.log(`Endpoint responded: ${req.method} ${req.path} - ${res.statusCode}`)
76
+ })
77
+ next()
78
+ })
79
+
80
+ // Request logging middleware
81
+ app.use((req, res, next) => {
82
+ wlogger.info(`${req.method} ${req.path}`)
83
+ next()
84
+ })
85
+
86
+ // Error handling middleware
87
+ app.use((err, req, res, next) => {
88
+ wlogger.error('Express error:', err)
89
+
90
+ // Handle JSON parsing errors
91
+ if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
92
+ return res.status(400).json({
93
+ error: 'Invalid JSON in request body'
94
+ })
95
+ }
96
+
97
+ // Default to 500 for other errors
98
+ res.status(500).json({
99
+ error: err.message || 'Internal server error'
100
+ })
101
+ })
102
+
103
+ // Wait for any adapters to initialize.
104
+ await this.controllers.initAdapters()
105
+
106
+ // Wait for any use-libraries to initialize.
107
+ await this.controllers.initUseCases()
108
+
109
+ // Attach REST API controllers to the app.
110
+ this.controllers.attachRESTControllers(app)
111
+
112
+ // Initialize any other controller libraries.
113
+ this.controllers.initControllers()
114
+
115
+ // Serve static assets from docs directory
116
+ const __filename = fileURLToPath(import.meta.url)
117
+ const __dirname = dirname(__filename)
118
+ app.use('/assets', express.static(join(__dirname, '..', 'docs', 'assets')))
119
+
120
+ // Health check endpoint
121
+ app.get('/health', (req, res) => {
122
+ res.json({
123
+ status: 'ok',
124
+ service: 'rest2nostr',
125
+ version: config.version
126
+ })
127
+ })
128
+
129
+ // Root endpoint
130
+ app.get('/', (req, res) => {
131
+ const docsPath = join(__dirname, '..', 'docs', 'index.html')
132
+ res.sendFile(docsPath)
133
+ })
134
+
135
+ // MIDDLEWARE END
136
+
137
+ console.log(`Running server in environment: ${this.config.env}`)
138
+ wlogger.info(`Running server in environment: ${this.config.env}`)
139
+
140
+ this.server = app.listen(this.config.port, () => {
141
+ console.log(`Server started on port ${this.config.port}`)
142
+ wlogger.info(`Server started on port ${this.config.port}`)
143
+ })
144
+
145
+ this.server.on('error', (err) => {
146
+ console.error('Server error:', err)
147
+ wlogger.error('Server error:', err)
148
+ })
149
+
150
+ this.server.on('close', () => {
151
+ console.log('Server closed')
152
+ wlogger.info('Server closed')
153
+ })
154
+
155
+ return app
156
+ } catch (err) {
157
+ console.error('Could not start server. Error: ', err)
158
+ wlogger.error('Could not start server. Error: ', err)
159
+
160
+ console.log(
161
+ 'Exiting after 5 seconds. Depending on process manager to restart.'
162
+ )
163
+ await this.sleep(5000)
164
+ this.process.exit(1)
165
+ }
166
+ }
167
+
168
+ sleep (ms) {
169
+ return new Promise((resolve) => setTimeout(resolve, ms))
170
+ }
171
+ }
172
+
173
+ // Start the server if this file is run directly
174
+ const __filename = fileURLToPath(import.meta.url)
175
+ if (process.argv[1] === __filename) {
176
+ const server = new Server()
177
+ server.startServer().catch(err => {
178
+ console.error('Failed to start server:', err)
179
+ process.exit(1)
180
+ })
181
+ }
182
+
183
+ export default Server
@@ -0,0 +1,4 @@
1
+ # Developer Documentation
2
+
3
+ - [prompt.md](./prompt.md) - This is the original prompt [trout](https://github.com/christroutner) gave the AI to build the app. He uses Cursor 2.0 in Planning mode and the Composer 1 LLM.
4
+ - [plan](./rest2nostr-poxy-api.plan.md) - This is the plan created by the Cursor AI from the prompt and support files it was given. This is the plan it used to build the app.
@@ -0,0 +1,34 @@
1
+ ## Build a REST API implementing REST2NOSTR Proxy
2
+
3
+ Your task is to plan out the building of a REST API server that implements the idea for a REST2NOSTR proxy. The code for this REST API server app should be placed in the `/app` directory. You can edit any files in the `/app` directory. The REST API server should be built using node.js JavaScript and the express.js library. It should also use dotenv to manage the use of environment variables. It should use [api-doc](https://www.npmjs.com/package/api-doc) to generate API documentation for the REST API.
4
+
5
+ ### Nostr
6
+
7
+ Nostr is a social media protocol. It is defined by nip-01.md in the `nostr/` directory. This is the primary specification that defines Nostr, but there are many other NIPS for other features. If you need to dig deeper into the Nostr standards you can explore the [NIPs on Github](https://github.com/nostr-protocol/nips/tree/master).
8
+
9
+ The `nostr-sandbox/` directory contains a series of small example code snippets for common social media use-cases using Nostr. You can study this code understand how a Nostr Client would interact with a Nostr Relay.
10
+
11
+ [This discussion thread on GitHub](https://github.com/nostr-protocol/nips/issues/1549) introduces a concept called REST2NOSTR. It solves an common issue experienced by JavaScript web developers: a high preference for REST APIs over Websockets. The proposal is to create a REST API server that operates as a proxy for the Websockets protocol used by Nostr Relays. This would allow Client developers to interact with a Nostr Relay over a familiar REST API, instead of using Websockets. Here is a summary of the API endpoints the discussion proposes:
12
+
13
+ | REST Method | Endpoint | Nostr WebSocket Equivalent | Purpose |
14
+ | --- | --- | --- | --- |
15
+ | `POST` | `/event` | `["EVENT", <event>]` | Publish a signed Nostr event to the relay. |
16
+ | `GET` | `/req/` | `["REQ", <sub_id>, <filters>]` | Retrieve a list of events based on filters (stateless query). |
17
+ | `POST`/`PUT` | `/req/` | `["REQ", <sub_id>, <filters>]` | Establish a subscription (for long-polling or SSE). |
18
+ | `DELETE` | `/req/` | `["CLOSE", <sub_id>]` | Close an existing subscription. |
19
+
20
+ Your task is to plan out the building of a REST API server that implements the idea for a REST2NOSTR proxy. All code examples in the `nostr-sandbox/` directory that interact with a Relay over Websockets should be able to be implemented using the new REST API. The code examples are a good benchmark to use as a frame of reference as to weather the REST API has been implemented correctly. It would be a good idea to create an `examples/` directory that contain many of these code example, refactored for use with the new REST API.
21
+
22
+ There is a similar implementation of REST2NOSTR available at [https://nostr-api.com/](https://nostr-api.com/). While the API and documentation are available, the source code for that implementation is not available. However, it is good to study as an example of the kind of output we are looking for.
23
+
24
+ ### Follow the Clean Architecture code pattern
25
+
26
+ As you plan out the code layout, you should follow the Clean Architecture patterns. Here is background information you can follow to ensure you follow the Clean Architecture pattern:
27
+
28
+ * [Clean Architecture Summary](https://raw.githubusercontent.com/christroutner/trouts-blog/refs/heads/master/blog/2021-07-06-clean-architecture/index.md) - This is a markdown document that summarizes the Clean Architecture pattern, and links to additional support information.
29
+ * [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider) - This is a node.js JavaScript code base that follows the Clean Architecture patterns. Notice that within the `src` directory, the sub-directories are split up according to the guidance in the Clean Architecture Summary article. This is the primary pattern you should follow.
30
+ * A copy of the ipfs-service-provider repository has been copied to the `clean-architecture/` directory, so that you can study the code locally.
31
+
32
+ ### Summary
33
+
34
+ Your task is to plan out the building of a REST API server that implements the idea for a REST2NOSTR proxy. The code for this REST API server app should be placed in the `/app` directory. You can edit any files in the `/app` directory. The REST API server should be built using node.js JavaScript and the express.js library. It should also use dotenv to manage the use of environment variables. It should use [api-doc](https://www.npmjs.com/package/api-doc) to generate API documentation for the REST API.
@@ -0,0 +1,163 @@
1
+ # REST2NOSTR Proxy API Implementation Plan
2
+
3
+ ## Overview
4
+
5
+ Build a REST API server in `/app` that proxies Nostr WebSocket protocol to REST endpoints, enabling JavaScript developers to interact with Nostr relays via familiar REST APIs instead of WebSockets.
6
+
7
+ ## Architecture
8
+
9
+ Follow Clean Architecture pattern with these layers:
10
+
11
+ - **Entities** (`src/entities/`): Domain models (Event, Subscription, etc.)
12
+ - **Use Cases** (`src/use-cases/`): Business logic (PublishEvent, QueryEvents, ManageSubscription)
13
+ - **Adapters** (`src/adapters/`): External interfaces (NostrRelay WebSocket client, logger)
14
+ - **Controllers** (`src/controllers/rest-api/`): Express.js route handlers
15
+
16
+ ## Directory Structure
17
+
18
+ ```
19
+ /app
20
+ ├── src/
21
+ │ ├── entities/
22
+ │ │ └── event.js
23
+ │ ├── use-cases/
24
+ │ │ ├── index.js
25
+ │ │ ├── publish-event.js
26
+ │ │ ├── query-events.js
27
+ │ │ └── manage-subscription.js
28
+ │ ├── adapters/
29
+ │ │ ├── index.js
30
+ │ │ ├── nostr-relay.js
31
+ │ │ └── wlogger.js
32
+ │ ├── controllers/
33
+ │ │ ├── index.js
34
+ │ │ └── rest-api/
35
+ │ │ ├── index.js
36
+ │ │ ├── event/
37
+ │ │ │ ├── controller.js
38
+ │ │ │ └── index.js
39
+ │ │ └── req/
40
+ │ │ ├── controller.js
41
+ │ │ └── index.js
42
+ │ └── config/
43
+ │ ├── index.js
44
+ │ └── env/
45
+ │ ├── common.js
46
+ │ ├── development.js
47
+ │ └── production.js
48
+ ├── examples/
49
+ │ └── [refactored sandbox examples]
50
+ ├── bin/
51
+ │ └── server.js
52
+ ├── .env.example
53
+ ├── .env
54
+ ├── index.js
55
+ ├── package.json
56
+ └── apidoc.json
57
+ ```
58
+
59
+ ## Core Endpoints
60
+
61
+ ### POST /event
62
+
63
+ - Maps to: `["EVENT", <event>]`
64
+ - Publish a signed Nostr event to relay
65
+ - Body: JSON event object
66
+ - Response: `{"accepted": true/false, "message": ""}` (maps to `["OK", ...]`)
67
+
68
+ ### GET /req/:subId
69
+
70
+ - Maps to: `["REQ", <sub_id>, <filters>]`
71
+ - Stateless query - returns events immediately
72
+ - Query params: filters (JSON encoded or separate params)
73
+ - Response: Array of events
74
+
75
+ ### POST /req/:subId
76
+
77
+ - Maps to: `["REQ", <sub_id>, <filters>]`
78
+ - Establish subscription for Server-Sent Events (SSE)
79
+ - Body: filters object
80
+ - Response: SSE stream of events
81
+
82
+ ### DELETE /req/:subId
83
+
84
+ - Maps to: `["CLOSE", <sub_id>]`
85
+ - Close an existing subscription
86
+ - Response: Confirmation
87
+
88
+ ## Implementation Details
89
+
90
+ ### 1. Package Dependencies
91
+
92
+ - `express`: REST API framework
93
+ - `dotenv`: Environment variable management
94
+ - `apidoc`: API documentation generation
95
+ - `ws` or `nostr-tools`: WebSocket client for Nostr relays
96
+ - `winston`: Logging (following example pattern)
97
+
98
+ ### 2. Adapter Layer (`src/adapters/`)
99
+
100
+ - **nostr-relay.js**: WebSocket client wrapper
101
+ - Connect to configured relay(s)
102
+ - Send `EVENT`, `REQ`, `CLOSE` messages
103
+ - Handle relay responses (`EVENT`, `OK`, `EOSE`, `CLOSED`, `NOTICE`)
104
+ - Manage connection pooling for multiple relays
105
+ - **wlogger.js**: Winston-based logger
106
+
107
+ ### 3. Use Cases (`src/use-cases/`)
108
+
109
+ - **publish-event.js**: Validate event, send to relay, return OK response
110
+ - **query-events.js**: Stateless query - send REQ, collect events until EOSE, return results
111
+ - **manage-subscription.js**: Create/close subscriptions, handle SSE streaming
112
+
113
+ ### 4. Controllers (`src/controllers/rest-api/`)
114
+
115
+ - **event/controller.js**: Handle POST /event
116
+ - **req/controller.js**: Handle GET/POST/DELETE /req/:subId
117
+ - Express middleware for validation, error handling
118
+ - SSE support for subscription endpoint
119
+
120
+ ### 5. Configuration (`src/config/`)
121
+
122
+ - Environment-based config (development, production, test)
123
+ - Default relay URL(s) from environment variables
124
+ - Port, logging level, etc.
125
+
126
+ ### 6. Subscription Management
127
+
128
+ - Store active subscriptions in memory (Map with subId as key)
129
+ - Map subscription IDs to WebSocket connections
130
+ - Handle cleanup on DELETE /req/:subId
131
+ - Support SSE streaming for POST /req/:subId
132
+
133
+ ### 7. Examples (`examples/`)
134
+
135
+ Refactor sandbox examples to use REST API:
136
+
137
+ - `01-create-account/` - Use REST API to publish kind 0 event
138
+ - `02-read-posts/` - Use GET /req/:subId for stateless query
139
+ - `03-write-post/` - Use POST /event
140
+ - `04-read-alice-posts/` - Use GET /req/:subId with author filter
141
+ - `14-get-follow-list/` - Use GET /req/:subId with kind 3 filter
142
+ - Additional examples as needed
143
+
144
+ ### 8. API Documentation
145
+
146
+ - Use api-doc annotations in controller files
147
+ - Generate docs with `npm run docs`
148
+ - Follow pattern from clean-architecture example
149
+
150
+ ## Environment Variables
151
+
152
+ - `NOSTR_RELAY_URL`: Default relay WebSocket URL (e.g., `wss://nostr-relay.psfoundation.info`)
153
+ - `PORT`: Server port (default: 3000)
154
+ - `NODE_ENV`: Environment (development, production, test)
155
+ - `LOG_LEVEL`: Logging level (info, debug, error)
156
+
157
+ ## Key Implementation Notes
158
+
159
+ - WebSocket connections: Maintain persistent connections to relay(s) in adapter
160
+ - Error handling: Map Nostr relay errors to appropriate HTTP status codes
161
+ - Validation: Validate Nostr events before forwarding (event structure, signature)
162
+ - SSE: Use Express response.write() for Server-Sent Events in subscription endpoint
163
+ - Stateless queries: For GET /req/:subId, collect events until EOSE, then close subscription automatically
@@ -0,0 +1,161 @@
1
+ # Test Plan for REST2NOSTR Application
2
+
3
+ ## Overview
4
+
5
+ Create unit and integration tests for the REST2NOSTR Express.js application following patterns from `tests/testing-example-code/`. Tests will use mocha, chai, and sinon, and cover all code paths exercised by the examples in `/app/examples/`.
6
+
7
+ ## Test Structure
8
+
9
+ Create test directory structure:
10
+
11
+ - `app/test/unit/` - Unit tests with mocked dependencies
12
+ - `app/test/integration/` - Integration tests with real dependencies
13
+ - `app/test/unit/mocks/` - Mock data for unit tests
14
+
15
+ ## Unit Tests
16
+
17
+ ### 1. Entity Tests (`test/unit/entities/`)
18
+
19
+ - **event-unit.js**: Test Event entity validation and serialization
20
+ - Test `isValid()` with valid events
21
+ - Test `isValid()` with invalid events (missing fields, wrong types, wrong lengths)
22
+ - Test `toJSON()` serialization
23
+ - Use mock event data from `mocks/event-mocks.js`
24
+
25
+ ### 2. Use Case Tests (`test/unit/use-cases/`)
26
+
27
+ - **publish-event-unit.js**: Test PublishEventUseCase
28
+ - Mock NostrRelayAdapter.sendEvent()
29
+ - Test successful event publishing
30
+ - Test invalid event rejection
31
+ - Test adapter error handling
32
+ - Use mocks from `mocks/nostr-relay-mocks.js`
33
+
34
+ - **query-events-unit.js**: Test QueryEventsUseCase
35
+ - Mock NostrRelayAdapter.sendReq() and sendClose()
36
+ - Test successful query with events returned
37
+ - Test query timeout handling
38
+ - Test CLOSED message handling
39
+ - Test EOSE handling
40
+
41
+ - **manage-subscription-unit.js**: Test ManageSubscriptionUseCase
42
+ - Mock NostrRelayAdapter.sendReq() and sendClose()
43
+ - Test subscription creation
44
+ - Test subscription closure
45
+ - Test duplicate subscription prevention
46
+ - Test handler callbacks (onEvent, onEose, onClosed)
47
+
48
+ ### 3. Controller Tests (`test/unit/controllers/`)
49
+
50
+ - **event-controller-unit.js**: Test EventRESTControllerLib
51
+ - Mock UseCases.publishEvent.execute()
52
+ - Test POST /event with valid event data
53
+ - Test POST /event with missing event data
54
+ - Test error handling
55
+ - Use Express request/response mocks
56
+
57
+ - **req-controller-unit.js**: Test ReqRESTControllerLib
58
+ - Mock UseCases.queryEvents.execute() and manageSubscription methods
59
+ - Test GET /req/:subId with filters (JSON string and parsed)
60
+ - Test GET /req/:subId with individual query params
61
+ - Test POST /req/:subId for SSE subscription
62
+ - Test DELETE /req/:subId for closing subscription
63
+ - Test error handling for invalid filters
64
+ - Test missing subscription ID validation
65
+
66
+ ### 4. Adapter Tests (`test/unit/adapters/`)
67
+
68
+ - **nostr-relay-unit.js**: Test NostrRelayAdapter
69
+ - Mock WebSocket connections
70
+ - Test connection establishment
71
+ - Test sendEvent() and OK response handling
72
+ - Test sendReq() and EVENT/EOSE/CLOSED message handling
73
+ - Test sendClose()
74
+ - Test message queuing when disconnected
75
+ - Test reconnection logic
76
+ - Test error handling
77
+
78
+ ### 5. Server Tests (`test/unit/bin/`)
79
+
80
+ - **server-unit.js**: Test Server class
81
+ - Mock Express app, Controllers, and adapters
82
+ - Test server initialization
83
+ - Test middleware attachment
84
+ - Test route attachment
85
+ - Test error handling
86
+ - Test health check endpoint
87
+
88
+ ## Integration Tests
89
+
90
+ ### 1. API Endpoint Tests (`test/integration/api/`)
91
+
92
+ - **event-integration.js**: Test POST /event endpoint
93
+ - Create a test server instance
94
+ - Test publishing kind 0 event (profile metadata) - covers example 01
95
+ - Test publishing kind 1 event (text post) - covers example 03
96
+ - Test publishing kind 3 event (follow list) - covers example 06
97
+ - Test publishing kind 7 event (reaction/like) - covers example 07
98
+ - Test invalid event rejection
99
+ - Use real Nostr relay connection (may need test relay or mock relay)
100
+
101
+ - **req-integration.js**: Test GET /req/:subId endpoint
102
+ - Create a test server instance
103
+ - Test querying kind 1 events (posts) - covers examples 02, 04
104
+ - Test querying kind 3 events (follow list) - covers example 05
105
+ - Test with various filter combinations
106
+ - Test with filters as JSON string query param
107
+ - Test with individual query params
108
+ - Test error handling
109
+
110
+ - **subscription-integration.js**: Test POST /req/:subId SSE subscription
111
+ - Create a test server instance
112
+ - Test SSE subscription creation
113
+ - Test event streaming
114
+ - Test EOSE handling
115
+ - Test subscription closure
116
+ - Test DELETE /req/:subId endpoint
117
+
118
+ ### 2. Use Case Integration Tests (`test/integration/use-cases/`)
119
+
120
+ - **publish-event-integration.js**: Test PublishEventUseCase with real adapter
121
+ - **query-events-integration.js**: Test QueryEventsUseCase with real adapter
122
+ - **manage-subscription-integration.js**: Test ManageSubscriptionUseCase with real adapter
123
+
124
+ ## Mock Data Files
125
+
126
+ Create mock data files in `test/unit/mocks/`:
127
+
128
+ - **event-mocks.js**: Mock event data for various event kinds (0, 1, 3, 7)
129
+ - **nostr-relay-mocks.js**: Mock responses from Nostr relay (OK, EVENT, EOSE, CLOSED)
130
+ - **controller-mocks.js**: Mock Express request/response objects
131
+
132
+ ## Key Considerations
133
+
134
+ 1. **ES Modules**: The app uses ES modules (import/export) while the example uses CommonJS. Tests should use ES modules with `.js` extension and proper import syntax.
135
+
136
+ 2. **Test Environment**: Integration tests will need a running Nostr relay. Consider:
137
+
138
+ - Using a test relay URL from config
139
+ - Or creating a mock WebSocket server for integration tests
140
+ - Or documenting that tests require a relay URL in environment
141
+
142
+ 3. **Test Coverage**: Ensure tests cover:
143
+
144
+ - All endpoints used by examples (POST /event, GET /req/:subId)
145
+ - All event kinds (0, 1, 3, 7)
146
+ - Error paths and edge cases
147
+ - Validation logic
148
+
149
+ 4. **Test Data**: Use the same test keys and data patterns from examples where applicable (e.g., Alice's private key, Bob's public key)
150
+
151
+ 5. **Async Handling**: Properly handle async/await in tests, especially for WebSocket operations and SSE streams
152
+
153
+ 6. **Cleanup**: Ensure proper cleanup of WebSocket connections and subscriptions in tests
154
+
155
+ ## Test Execution
156
+
157
+ Tests will run using existing npm scripts:
158
+
159
+ - `npm test` - Runs unit tests with linting and coverage
160
+ - `npm run test:integration` - Runs integration tests with extended timeout
161
+ - `npm run coverage` - Generates coverage report
@@ -0,0 +1,13 @@
1
+ The express.js app in the `app/` directory needs unit and integration tests. The scope of this task is to create a plan for adding these
2
+
3
+ When considering the creation of tests, follow the patters provided in the example available in the `testing/testing-example-code/` directory. This is simple node.js application which uses the following testing libraries:
4
+
5
+ - mocha as a test runner
6
+ - chai as an assertion library
7
+ - sinon as a stubing library
8
+
9
+ The code example show common patterns for writing both unit and integration tests.
10
+
11
+ The `/app/examples/` directory contains working code that I've tested and verified works correctly. The unit and integration tests you create should cover the same code paths exercised by these examples.
12
+
13
+ The testing dependencies are already installed and listed in the package.json file under the `app/` directory. The package.json file also contains the `test`, `test:integration`, and `coverage` scripts that we'll use to run tests and measure test coverage.
@@ -0,0 +1,67 @@
1
+ /*
2
+ Example script creating a key-pair for a Nostr account and publishing profile metadata.
3
+ Refactored to use REST API instead of WebSocket.
4
+
5
+ Run the server with `npm start` in the main directory, before running this example.
6
+ */
7
+
8
+ import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'
9
+ import * as nip19 from 'nostr-tools/nip19'
10
+ import { bytesToHex } from '@noble/hashes/utils.js'
11
+
12
+ const API_URL = process.env.API_URL || 'http://localhost:5942'
13
+
14
+ // Generate keys
15
+ const sk = generateSecretKey() // `sk` is a Uint8Array
16
+ const nsec = nip19.nsecEncode(sk)
17
+ const skHex = bytesToHex(sk)
18
+
19
+ const pk = getPublicKey(sk) // `pk` is a hex string
20
+ const npub = nip19.npubEncode(pk)
21
+
22
+ console.log('private key:', skHex)
23
+ console.log('encoded private key:', nsec)
24
+ console.log()
25
+ console.log('public key:', pk)
26
+ console.log('encoded public key:', npub)
27
+ console.log()
28
+
29
+ // Create profile metadata event (kind 0)
30
+ const profileMetadata = {
31
+ name: 'Alice',
32
+ about: 'Hello, I am Alice!',
33
+ picture: 'https://example.com/alice.jpg'
34
+ }
35
+
36
+ const eventTemplate = {
37
+ kind: 0,
38
+ created_at: Math.floor(Date.now() / 1000),
39
+ tags: [],
40
+ content: JSON.stringify(profileMetadata)
41
+ }
42
+
43
+ // Sign the event
44
+ const signedEvent = finalizeEvent(eventTemplate, sk)
45
+ console.log('Signed event:', JSON.stringify(signedEvent, null, 2))
46
+
47
+ // Publish to REST API
48
+ try {
49
+ const response = await fetch(`${API_URL}/event`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json'
53
+ },
54
+ body: JSON.stringify(signedEvent)
55
+ })
56
+
57
+ const result = await response.json()
58
+ console.log('Publish result:', result)
59
+
60
+ if (result.accepted) {
61
+ console.log('Profile metadata published successfully!')
62
+ } else {
63
+ console.error('Failed to publish:', result.message)
64
+ }
65
+ } catch (err) {
66
+ console.error('Error publishing event:', err)
67
+ }