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
package/.env-local
ADDED
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
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
|
+
}
|