psf-bch-api 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.env-local +9 -0
  2. package/bin/server.js +2 -1
  3. package/package.json +4 -1
  4. package/src/adapters/fulcrum-api.js +124 -0
  5. package/src/adapters/full-node-rpc.js +2 -6
  6. package/src/adapters/index.js +4 -0
  7. package/src/adapters/slp-indexer-api.js +124 -0
  8. package/src/config/env/common.js +21 -24
  9. package/src/controllers/rest-api/fulcrum/controller.js +563 -0
  10. package/src/controllers/rest-api/fulcrum/router.js +64 -0
  11. package/src/controllers/rest-api/full-node/blockchain/controller.js +4 -4
  12. package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
  13. package/src/controllers/rest-api/full-node/mining/router.js +52 -0
  14. package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
  15. package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
  16. package/src/controllers/rest-api/index.js +19 -3
  17. package/src/controllers/rest-api/slp/controller.js +218 -0
  18. package/src/controllers/rest-api/slp/router.js +55 -0
  19. package/src/controllers/timer-controller.js +1 -1
  20. package/src/use-cases/fulcrum-use-cases.js +155 -0
  21. package/src/use-cases/full-node-mining-use-cases.js +28 -0
  22. package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
  23. package/src/use-cases/index.js +8 -0
  24. package/src/use-cases/slp-use-cases.js +321 -0
  25. package/test/unit/controllers/blockchain-controller-unit.js +2 -3
  26. package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
  27. package/test/unit/controllers/mining-controller-unit.js +139 -0
  28. package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
  29. package/test/unit/controllers/rest-api-index-unit.js +59 -3
  30. package/test/unit/controllers/slp-controller-unit.js +312 -0
  31. package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
  32. package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
  33. package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
  34. package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
  35. package/src/entities/event.js +0 -71
  36. package/test/integration/api/event-integration.js +0 -250
  37. package/test/integration/api/req-integration.js +0 -173
  38. package/test/integration/api/subscription-integration.js +0 -198
  39. package/test/integration/use-cases/manage-subscription-integration.js +0 -163
  40. package/test/integration/use-cases/publish-event-integration.js +0 -104
  41. package/test/integration/use-cases/query-events-integration.js +0 -95
  42. package/test/unit/entities/event-unit.js +0 -139
  43. /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
  44. /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
  45. /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
@@ -1,250 +0,0 @@
1
- /*
2
- Integration tests for POST /event endpoint.
3
- These tests require a running Nostr relay.
4
- */
5
-
6
- // npm libraries
7
- import { assert } from 'chai'
8
-
9
- // Unit under test
10
- import Server from '../../../bin/server.js'
11
- import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure'
12
- import { hexToBytes } from '@noble/hashes/utils.js'
13
-
14
- describe('#event-integration.js', () => {
15
- let server
16
- const baseUrl = 'http://localhost:3001' // Use different port for tests
17
-
18
- before(async () => {
19
- // Start test server
20
- server = new Server()
21
- server.config.port = 3001
22
- await server.startServer()
23
-
24
- // Wait for server to be ready
25
- await new Promise(resolve => setTimeout(resolve, 1000))
26
- })
27
-
28
- after(async () => {
29
- // Stop server
30
- if (server && server.server) {
31
- await new Promise((resolve) => {
32
- server.server.close(() => {
33
- resolve()
34
- })
35
- })
36
- }
37
- })
38
-
39
- describe('POST /event', () => {
40
- it('should publish kind 0 event (profile metadata) - covers example 01', async () => {
41
- // Generate keys
42
- const sk = generateSecretKey()
43
-
44
- // Create profile metadata event (kind 0)
45
- const profileMetadata = {
46
- name: 'Test User',
47
- about: 'Integration test user',
48
- picture: 'https://example.com/test.jpg'
49
- }
50
-
51
- const eventTemplate = {
52
- kind: 0,
53
- created_at: Math.floor(Date.now() / 1000),
54
- tags: [],
55
- content: JSON.stringify(profileMetadata)
56
- }
57
-
58
- // Sign the event
59
- const signedEvent = finalizeEvent(eventTemplate, sk)
60
-
61
- // Publish to REST API
62
- const response = await fetch(`${baseUrl}/event`, {
63
- method: 'POST',
64
- headers: {
65
- 'Content-Type': 'application/json'
66
- },
67
- body: JSON.stringify(signedEvent)
68
- })
69
-
70
- const result = await response.json()
71
-
72
- // Assert response
73
- assert.equal(response.status, 200)
74
- assert.property(result, 'accepted')
75
- assert.property(result, 'eventId')
76
- assert.equal(result.eventId, signedEvent.id)
77
- })
78
-
79
- it('should publish kind 1 event (text post) - covers example 03', async () => {
80
- // Alice's private key from examples
81
- const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
82
- const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
83
- const alicePubKey = getPublicKey(alicePrivKeyBin)
84
-
85
- // Generate a post
86
- const eventTemplate = {
87
- kind: 1,
88
- created_at: Math.floor(Date.now() / 1000),
89
- tags: [],
90
- content: 'Integration test post'
91
- }
92
-
93
- // Sign the post
94
- const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
95
-
96
- // Publish to REST API
97
- const response = await fetch(`${baseUrl}/event`, {
98
- method: 'POST',
99
- headers: {
100
- 'Content-Type': 'application/json'
101
- },
102
- body: JSON.stringify(signedEvent)
103
- })
104
-
105
- const result = await response.json()
106
-
107
- // Assert response
108
- assert.equal(response.status, 200)
109
- assert.property(result, 'accepted')
110
- assert.property(result, 'eventId')
111
- assert.equal(result.eventId, signedEvent.id)
112
- assert.equal(signedEvent.pubkey, alicePubKey)
113
- })
114
-
115
- it('should publish kind 3 event (follow list) - covers example 06', async () => {
116
- // Alice's private key
117
- const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
118
- const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
119
-
120
- // Bob's public key
121
- const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
122
- const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
123
- const bobPubKey = getPublicKey(bobPrivKeyBin)
124
-
125
- const psf = 'wss://nostr-relay.psfoundation.info'
126
-
127
- const followList = [
128
- ['p', bobPubKey, psf, 'bob']
129
- ]
130
-
131
- // Generate a follow list event (kind 3)
132
- const eventTemplate = {
133
- kind: 3,
134
- created_at: Math.floor(Date.now() / 1000),
135
- tags: followList,
136
- content: ''
137
- }
138
-
139
- // Sign the event
140
- const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
141
-
142
- // Publish to REST API
143
- const response = await fetch(`${baseUrl}/event`, {
144
- method: 'POST',
145
- headers: {
146
- 'Content-Type': 'application/json'
147
- },
148
- body: JSON.stringify(signedEvent)
149
- })
150
-
151
- const result = await response.json()
152
-
153
- // Assert response
154
- assert.equal(response.status, 200)
155
- assert.property(result, 'accepted')
156
- assert.property(result, 'eventId')
157
- assert.equal(result.eventId, signedEvent.id)
158
- })
159
-
160
- it('should publish kind 7 event (reaction/like) - covers example 07', async () => {
161
- // Bob's private key
162
- const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
163
- const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
164
- const bobPubKey = getPublicKey(bobPrivKeyBin)
165
-
166
- const psf = 'wss://nostr-relay.psfoundation.info'
167
-
168
- // Use a test event ID
169
- const evIdToLike = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
170
- const evIdAuthorPubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
171
-
172
- // Generate like event (kind 7)
173
- const likeEventTemplate = {
174
- kind: 7,
175
- created_at: Math.floor(Date.now() / 1000),
176
- pubkey: bobPubKey,
177
- tags: [
178
- ['e', evIdToLike, psf],
179
- ['p', evIdAuthorPubKey, psf]
180
- ],
181
- content: '+'
182
- }
183
-
184
- // Sign the event
185
- const signedEvent = finalizeEvent(likeEventTemplate, bobPrivKeyBin)
186
-
187
- // Publish to REST API
188
- const response = await fetch(`${baseUrl}/event`, {
189
- method: 'POST',
190
- headers: {
191
- 'Content-Type': 'application/json'
192
- },
193
- body: JSON.stringify(signedEvent)
194
- })
195
-
196
- const result = await response.json()
197
-
198
- // Assert response
199
- assert.equal(response.status, 200)
200
- assert.property(result, 'accepted')
201
- assert.property(result, 'eventId')
202
- assert.equal(result.eventId, signedEvent.id)
203
- })
204
-
205
- it('should reject invalid event', async () => {
206
- const invalidEvent = {
207
- id: 'invalid',
208
- pubkey: 'invalid',
209
- created_at: Math.floor(Date.now() / 1000),
210
- kind: 1,
211
- tags: [],
212
- content: 'Test',
213
- sig: 'invalid'
214
- }
215
-
216
- const response = await fetch(`${baseUrl}/event`, {
217
- method: 'POST',
218
- headers: {
219
- 'Content-Type': 'application/json'
220
- },
221
- body: JSON.stringify(invalidEvent)
222
- })
223
-
224
- const result = await response.json()
225
-
226
- // Should reject invalid event - error response format
227
- assert.equal(response.status, 400)
228
- assert.property(result, 'error')
229
- assert.include(result.error, 'Invalid event structure')
230
- })
231
-
232
- it('should return 400 when event data is missing', async () => {
233
- // Send empty body - Express will parse as undefined, controller should handle it
234
- const response = await fetch(`${baseUrl}/event`, {
235
- method: 'POST',
236
- headers: {
237
- 'Content-Type': 'application/json'
238
- },
239
- body: ''
240
- })
241
-
242
- // Empty body should be parsed as undefined by Express
243
- const result = await response.json()
244
-
245
- assert.equal(response.status, 400)
246
- assert.property(result, 'error')
247
- assert.include(result.error, 'Event data is required')
248
- })
249
- })
250
- })
@@ -1,173 +0,0 @@
1
- /*
2
- Integration tests for GET /req/:subId endpoint.
3
- These tests require a running Nostr relay.
4
- */
5
-
6
- // npm libraries
7
- import { assert } from 'chai'
8
-
9
- // Unit under test
10
- import Server from '../../../bin/server.js'
11
- import { getPublicKey } from 'nostr-tools/pure'
12
- import { hexToBytes } from '@noble/hashes/utils.js'
13
-
14
- describe('#req-integration.js', () => {
15
- let server
16
- const baseUrl = 'http://localhost:3002' // Use different port for tests
17
-
18
- before(async () => {
19
- // Start test server
20
- server = new Server()
21
- server.config.port = 3002
22
- await server.startServer()
23
-
24
- // Wait for server to be ready
25
- await new Promise(resolve => setTimeout(resolve, 1000))
26
- })
27
-
28
- after(async () => {
29
- // Stop server
30
- if (server && server.server) {
31
- await new Promise((resolve) => {
32
- server.server.close(() => {
33
- resolve()
34
- })
35
- })
36
- }
37
- })
38
-
39
- describe('GET /req/:subId', () => {
40
- it('should query kind 1 events (posts) - covers examples 02, 04', async () => {
41
- // JB55's public key from example 02
42
- const jb55 = '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
43
-
44
- // Create subscription ID
45
- const subId = 'read-posts-' + Date.now()
46
-
47
- // Create filters - read posts from JB55
48
- const filters = {
49
- limit: 2,
50
- kinds: [1],
51
- authors: [jb55]
52
- }
53
-
54
- // Query events using GET /req/:subId
55
- const filtersJson = encodeURIComponent(JSON.stringify([filters]))
56
- const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
57
-
58
- const response = await fetch(url)
59
- const events = await response.json()
60
-
61
- // Assert response
62
- assert.equal(response.status, 200)
63
- assert.isArray(events)
64
- // May be empty if no events exist, but structure should be correct
65
- if (events.length > 0) {
66
- assert.property(events[0], 'id')
67
- assert.property(events[0], 'pubkey')
68
- assert.property(events[0], 'created_at')
69
- assert.property(events[0], 'kind')
70
- assert.property(events[0], 'content')
71
- assert.equal(events[0].kind, 1)
72
- }
73
- })
74
-
75
- it('should query Alice posts - covers example 04', async () => {
76
- // Alice's public key
77
- const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
78
- const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
79
- const alicePubKey = getPublicKey(alicePrivKeyBin)
80
-
81
- // Create subscription ID
82
- const subId = 'read-alice-posts-' + Date.now()
83
-
84
- // Create filters - read posts from Alice
85
- const filters = {
86
- limit: 2,
87
- kinds: [1],
88
- authors: [alicePubKey]
89
- }
90
-
91
- // Query events using GET /req/:subId
92
- const filtersJson = encodeURIComponent(JSON.stringify([filters]))
93
- const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
94
-
95
- const response = await fetch(url)
96
- const events = await response.json()
97
-
98
- // Assert response
99
- assert.equal(response.status, 200)
100
- assert.isArray(events)
101
- if (events.length > 0) {
102
- assert.equal(events[0].pubkey, alicePubKey)
103
- assert.equal(events[0].kind, 1)
104
- }
105
- })
106
-
107
- it('should query kind 3 events (follow list) - covers example 05', async () => {
108
- // Alice's public key
109
- const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
110
- const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
111
- const alicePubKey = getPublicKey(alicePrivKeyBin)
112
-
113
- // Create subscription ID
114
- const subId = 'get-follow-list-' + Date.now()
115
-
116
- // Create filters - get follow list (kind 3) from Alice
117
- const filters = {
118
- limit: 5,
119
- kinds: [3],
120
- authors: [alicePubKey]
121
- }
122
-
123
- // Query events using GET /req/:subId
124
- const filtersJson = encodeURIComponent(JSON.stringify([filters]))
125
- const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
126
-
127
- const response = await fetch(url)
128
- const events = await response.json()
129
-
130
- // Assert response
131
- assert.equal(response.status, 200)
132
- assert.isArray(events)
133
- if (events.length > 0) {
134
- assert.equal(events[0].kind, 3)
135
- assert.equal(events[0].pubkey, alicePubKey)
136
- assert.isArray(events[0].tags)
137
- }
138
- })
139
-
140
- it('should handle filters as individual query params', async () => {
141
- const subId = 'test-sub-' + Date.now()
142
- const url = `${baseUrl}/req/${subId}?kinds=[1]&limit=10`
143
-
144
- const response = await fetch(url)
145
- const events = await response.json()
146
-
147
- assert.equal(response.status, 200)
148
- assert.isArray(events)
149
- })
150
-
151
- it('should return 400 when subscription ID is missing', async () => {
152
- const url = `${baseUrl}/req/?filters=${encodeURIComponent(JSON.stringify([{ kinds: [1] }]))}`
153
-
154
- const response = await fetch(url)
155
- await response.json()
156
-
157
- // Should return 404 or 400
158
- assert.isAtLeast(response.status, 400)
159
- })
160
-
161
- it('should return 400 when filters JSON is invalid', async () => {
162
- const subId = 'test-sub-' + Date.now()
163
- const url = `${baseUrl}/req/${subId}?filters=invalid-json{`
164
-
165
- const response = await fetch(url)
166
- const result = await response.json()
167
-
168
- assert.equal(response.status, 400)
169
- assert.property(result, 'error')
170
- assert.include(result.error, 'Invalid filters JSON')
171
- })
172
- })
173
- })
@@ -1,198 +0,0 @@
1
- /*
2
- Integration tests for POST /req/:subId SSE subscription and DELETE /req/:subId.
3
- These tests require a running Nostr relay.
4
- */
5
-
6
- // npm libraries
7
- import { assert } from 'chai'
8
-
9
- // Unit under test
10
- import Server from '../../../bin/server.js'
11
-
12
- describe('#subscription-integration.js', () => {
13
- let server
14
- const baseUrl = 'http://localhost:3003' // Use different port for tests
15
-
16
- before(async () => {
17
- // Start test server
18
- server = new Server()
19
- server.config.port = 3003
20
- await server.startServer()
21
-
22
- // Wait for server to be ready
23
- await new Promise(resolve => setTimeout(resolve, 1000))
24
- })
25
-
26
- after(async function () {
27
- this.timeout(10000) // Increase timeout for cleanup
28
- // Stop server
29
- if (server && server.server) {
30
- // Close all connections forcefully if available
31
- if (server.server.closeAllConnections) {
32
- server.server.closeAllConnections()
33
- }
34
-
35
- await new Promise((resolve) => {
36
- const timeout = setTimeout(() => {
37
- resolve() // Force resolve after 2 seconds
38
- }, 2000)
39
-
40
- server.server.close(() => {
41
- clearTimeout(timeout)
42
- resolve()
43
- })
44
- })
45
- }
46
- })
47
-
48
- describe('POST /req/:subId', () => {
49
- it('should create SSE subscription', async () => {
50
- const subId = 'test-sub-' + Date.now()
51
- const filters = { kinds: [1], limit: 10 }
52
-
53
- const response = await fetch(`${baseUrl}/req/${subId}`, {
54
- method: 'POST',
55
- headers: {
56
- 'Content-Type': 'application/json'
57
- },
58
- body: JSON.stringify(filters)
59
- })
60
-
61
- // Assert SSE headers
62
- assert.equal(response.headers.get('content-type'), 'text/event-stream')
63
- assert.equal(response.headers.get('cache-control'), 'no-cache')
64
- assert.equal(response.headers.get('connection'), 'keep-alive')
65
-
66
- // Read initial connection message
67
- const reader = response.body.getReader()
68
- const decoder = new TextDecoder()
69
-
70
- try {
71
- const { value } = await reader.read()
72
- const text = decoder.decode(value)
73
- assert.include(text, 'connected')
74
- assert.include(text, subId)
75
- } finally {
76
- reader.releaseLock()
77
- }
78
- })
79
-
80
- it('should return 400 when subscription ID is missing', async () => {
81
- const filters = { kinds: [1] }
82
-
83
- const response = await fetch(`${baseUrl}/req/`, {
84
- method: 'POST',
85
- headers: {
86
- 'Content-Type': 'application/json'
87
- },
88
- body: JSON.stringify(filters)
89
- })
90
-
91
- // Should return 404 or 400
92
- assert.isAtLeast(response.status, 400)
93
- })
94
-
95
- it('should return 400 when filters are missing', async () => {
96
- const subId = 'test-sub-' + Date.now()
97
-
98
- const response = await fetch(`${baseUrl}/req/${subId}`, {
99
- method: 'POST',
100
- headers: {
101
- 'Content-Type': 'application/json'
102
- },
103
- body: JSON.stringify({})
104
- })
105
-
106
- const result = await response.json()
107
-
108
- assert.equal(response.status, 400)
109
- assert.property(result, 'error')
110
- assert.include(result.error, 'Filters are required')
111
- })
112
- })
113
-
114
- describe('DELETE /req/:subId', () => {
115
- it('should close a subscription', async () => {
116
- const subId = 'test-sub-' + Date.now()
117
-
118
- const response = await fetch(`${baseUrl}/req/${subId}`, {
119
- method: 'DELETE'
120
- })
121
-
122
- // May return 200 if subscription exists, or 500 if it doesn't
123
- // The important thing is it doesn't crash
124
- assert.isAtMost(response.status, 500)
125
- })
126
-
127
- it('should return 400 when subscription ID is missing', async () => {
128
- const response = await fetch(`${baseUrl}/req/`, {
129
- method: 'DELETE'
130
- })
131
-
132
- // Should return 404 or 400
133
- assert.isAtLeast(response.status, 400)
134
- })
135
- })
136
-
137
- describe('PUT /req/:subId', () => {
138
- it('should create SSE subscription (alternative method)', async function () {
139
- this.timeout(10000) // Increase timeout for this test
140
-
141
- const subId = 'test-sub-' + Date.now()
142
- const filters = { kinds: [1], limit: 10 }
143
-
144
- const controller = new AbortController()
145
- const timeoutId = setTimeout(() => controller.abort(), 8000)
146
-
147
- let response
148
- try {
149
- response = await fetch(`${baseUrl}/req/${subId}`, {
150
- method: 'PUT',
151
- headers: {
152
- 'Content-Type': 'application/json'
153
- },
154
- body: JSON.stringify(filters),
155
- signal: controller.signal
156
- })
157
-
158
- // Assert SSE headers
159
- assert.equal(response.headers.get('content-type'), 'text/event-stream')
160
-
161
- clearTimeout(timeoutId)
162
-
163
- // Consume the stream to prevent hanging
164
- const reader = response.body.getReader()
165
- const decoder = new TextDecoder()
166
-
167
- try {
168
- // Read initial connection message with timeout
169
- const readPromise = reader.read()
170
- const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ value: null, done: true }), 2000))
171
- const { value, done } = await Promise.race([readPromise, timeoutPromise])
172
-
173
- if (value && !done) {
174
- const text = decoder.decode(value)
175
- assert.include(text, 'connected')
176
- }
177
- } finally {
178
- reader.releaseLock()
179
- }
180
- } catch (err) {
181
- // AbortController aborted - this is expected
182
- if (err.name !== 'AbortError') {
183
- throw err
184
- }
185
- } finally {
186
- clearTimeout(timeoutId)
187
- // Always close the subscription
188
- try {
189
- await fetch(`${baseUrl}/req/${subId}`, {
190
- method: 'DELETE'
191
- })
192
- } catch (err) {
193
- // Ignore errors when closing
194
- }
195
- }
196
- })
197
- })
198
- })