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
@@ -0,0 +1,139 @@
1
+ /*
2
+ Unit tests for the Event entity.
3
+ */
4
+
5
+ // npm libraries
6
+ import { assert } from 'chai'
7
+
8
+ // Mocking data libraries
9
+ import {
10
+ mockKind0Event,
11
+ mockKind1Event,
12
+ mockKind3Event,
13
+ mockKind7Event,
14
+ mockInvalidEventMissingId,
15
+ mockInvalidEventWrongIdLength,
16
+ mockInvalidEventMissingPubkey,
17
+ mockInvalidEventWrongPubkeyLength,
18
+ mockInvalidEventMissingCreatedAt,
19
+ mockInvalidEventWrongCreatedAtType,
20
+ mockInvalidEventMissingKind,
21
+ mockInvalidEventKindOutOfRange,
22
+ mockInvalidEventMissingSig,
23
+ mockInvalidEventWrongSigLength,
24
+ mockInvalidEventTagsNotArray
25
+ } from '../mocks/event-mocks.js'
26
+
27
+ // Unit under test
28
+ import Event from '../../../src/entities/event.js'
29
+
30
+ describe('#event.js', () => {
31
+ describe('#isValid()', () => {
32
+ it('should return true for valid kind 0 event', () => {
33
+ const event = new Event(mockKind0Event)
34
+ assert.isTrue(event.isValid())
35
+ })
36
+
37
+ it('should return true for valid kind 1 event', () => {
38
+ const event = new Event(mockKind1Event)
39
+ assert.isTrue(event.isValid())
40
+ })
41
+
42
+ it('should return true for valid kind 3 event', () => {
43
+ const event = new Event(mockKind3Event)
44
+ assert.isTrue(event.isValid())
45
+ })
46
+
47
+ it('should return true for valid kind 7 event', () => {
48
+ const event = new Event(mockKind7Event)
49
+ assert.isTrue(event.isValid())
50
+ })
51
+
52
+ it('should return false for event missing id', () => {
53
+ const event = new Event(mockInvalidEventMissingId)
54
+ assert.isFalse(event.isValid())
55
+ })
56
+
57
+ it('should return false for event with wrong id length', () => {
58
+ const event = new Event(mockInvalidEventWrongIdLength)
59
+ assert.isFalse(event.isValid())
60
+ })
61
+
62
+ it('should return false for event missing pubkey', () => {
63
+ const event = new Event(mockInvalidEventMissingPubkey)
64
+ assert.isFalse(event.isValid())
65
+ })
66
+
67
+ it('should return false for event with wrong pubkey length', () => {
68
+ const event = new Event(mockInvalidEventWrongPubkeyLength)
69
+ assert.isFalse(event.isValid())
70
+ })
71
+
72
+ it('should return false for event missing created_at', () => {
73
+ const event = new Event(mockInvalidEventMissingCreatedAt)
74
+ assert.isFalse(event.isValid())
75
+ })
76
+
77
+ it('should return false for event with wrong created_at type', () => {
78
+ const event = new Event(mockInvalidEventWrongCreatedAtType)
79
+ assert.isFalse(event.isValid())
80
+ })
81
+
82
+ it('should return false for event missing kind', () => {
83
+ const event = new Event(mockInvalidEventMissingKind)
84
+ assert.isFalse(event.isValid())
85
+ })
86
+
87
+ it('should return false for event with kind out of range', () => {
88
+ const event = new Event(mockInvalidEventKindOutOfRange)
89
+ assert.isFalse(event.isValid())
90
+ })
91
+
92
+ it('should return false for event missing sig', () => {
93
+ const event = new Event(mockInvalidEventMissingSig)
94
+ assert.isFalse(event.isValid())
95
+ })
96
+
97
+ it('should return false for event with wrong sig length', () => {
98
+ const event = new Event(mockInvalidEventWrongSigLength)
99
+ assert.isFalse(event.isValid())
100
+ })
101
+
102
+ it('should return false for event with tags not an array', () => {
103
+ const event = new Event(mockInvalidEventTagsNotArray)
104
+ assert.isFalse(event.isValid())
105
+ })
106
+ })
107
+
108
+ describe('#toJSON()', () => {
109
+ it('should serialize event to JSON correctly', () => {
110
+ const event = new Event(mockKind1Event)
111
+ const json = event.toJSON()
112
+
113
+ assert.property(json, 'id')
114
+ assert.property(json, 'pubkey')
115
+ assert.property(json, 'created_at')
116
+ assert.property(json, 'kind')
117
+ assert.property(json, 'tags')
118
+ assert.property(json, 'content')
119
+ assert.property(json, 'sig')
120
+
121
+ assert.equal(json.id, mockKind1Event.id)
122
+ assert.equal(json.pubkey, mockKind1Event.pubkey)
123
+ assert.equal(json.created_at, mockKind1Event.created_at)
124
+ assert.equal(json.kind, mockKind1Event.kind)
125
+ assert.deepEqual(json.tags, mockKind1Event.tags)
126
+ assert.equal(json.content, mockKind1Event.content)
127
+ assert.equal(json.sig, mockKind1Event.sig)
128
+ })
129
+
130
+ it('should serialize event with tags correctly', () => {
131
+ const event = new Event(mockKind3Event)
132
+ const json = event.toJSON()
133
+
134
+ assert.isArray(json.tags)
135
+ assert.equal(json.tags.length, 1)
136
+ assert.deepEqual(json.tags, mockKind3Event.tags)
137
+ })
138
+ })
139
+ })
@@ -0,0 +1,98 @@
1
+ /*
2
+ Mock Express request/response objects for controller unit tests.
3
+ */
4
+
5
+ // Mock Express request object
6
+ export function createMockRequest (overrides = {}) {
7
+ return {
8
+ body: {},
9
+ params: {},
10
+ query: {},
11
+ method: 'GET',
12
+ path: '/',
13
+ ...overrides
14
+ }
15
+ }
16
+
17
+ // Mock Express response object
18
+ export function createMockResponse () {
19
+ const res = {
20
+ statusCode: 200,
21
+ jsonData: null,
22
+ statusValue: null,
23
+ headers: {},
24
+ writeData: [],
25
+ endCalled: false,
26
+ writable: true, // Stream is writable by default
27
+ destroyed: false, // Stream is not destroyed by default
28
+ closed: false, // Stream is not closed by default
29
+ eventHandlers: {} // Store event handlers
30
+ }
31
+
32
+ res.status = function (code) {
33
+ res.statusCode = code
34
+ res.statusValue = code
35
+ return res
36
+ }
37
+
38
+ res.json = function (data) {
39
+ res.jsonData = data
40
+ return res
41
+ }
42
+
43
+ res.setHeader = function (name, value) {
44
+ res.headers[name] = value
45
+ return res
46
+ }
47
+
48
+ res.write = function (data) {
49
+ res.writeData.push(data)
50
+ return true
51
+ }
52
+
53
+ res.end = function () {
54
+ res.endCalled = true
55
+ return res
56
+ }
57
+
58
+ res.on = function (event, callback) {
59
+ // Store event handlers for different event types
60
+ if (!res.eventHandlers[event]) {
61
+ res.eventHandlers[event] = []
62
+ }
63
+ res.eventHandlers[event].push(callback)
64
+
65
+ // For backward compatibility with existing tests
66
+ if (event === 'close') {
67
+ res.closeCallback = callback
68
+ }
69
+
70
+ return res
71
+ }
72
+
73
+ // Helper to trigger an event (useful for testing)
74
+ res.trigger = function (event, ...args) {
75
+ if (res.eventHandlers[event]) {
76
+ for (const handler of res.eventHandlers[event]) {
77
+ handler(...args)
78
+ }
79
+ }
80
+ }
81
+
82
+ return res
83
+ }
84
+
85
+ // Helper to create a mock request with body
86
+ export function createMockRequestWithBody (body) {
87
+ return createMockRequest({ body })
88
+ }
89
+
90
+ // Helper to create a mock request with params
91
+ export function createMockRequestWithParams (params) {
92
+ return createMockRequest({ params })
93
+ }
94
+
95
+ // Helper to create a mock request with query
96
+ export function createMockRequestWithQuery (query) {
97
+ return createMockRequest({ query })
98
+ }
@@ -0,0 +1,194 @@
1
+ /*
2
+ Mock event data for unit tests.
3
+ Contains mock Nostr events for various event kinds.
4
+ */
5
+
6
+ // Alice's public key from examples
7
+ const alicePubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
8
+ const bobPubKey = 'b'.repeat(64)
9
+
10
+ // Valid event ID (64 hex chars)
11
+ const validEventId = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
12
+ // Valid signature (128 hex chars)
13
+ const validSig = 'a'.repeat(128)
14
+
15
+ // Kind 0: Profile metadata event
16
+ const mockKind0Event = {
17
+ id: validEventId,
18
+ pubkey: alicePubKey,
19
+ created_at: Math.floor(Date.now() / 1000),
20
+ kind: 0,
21
+ tags: [],
22
+ content: JSON.stringify({
23
+ name: 'Alice',
24
+ about: 'Hello, I am Alice!',
25
+ picture: 'https://example.com/alice.jpg'
26
+ }),
27
+ sig: validSig
28
+ }
29
+
30
+ // Kind 1: Text post event
31
+ const mockKind1Event = {
32
+ id: validEventId,
33
+ pubkey: alicePubKey,
34
+ created_at: Math.floor(Date.now() / 1000),
35
+ kind: 1,
36
+ tags: [],
37
+ content: 'This is a test message',
38
+ sig: validSig
39
+ }
40
+
41
+ // Kind 3: Follow list event
42
+ const mockKind3Event = {
43
+ id: validEventId,
44
+ pubkey: alicePubKey,
45
+ created_at: Math.floor(Date.now() / 1000),
46
+ kind: 3,
47
+ tags: [
48
+ ['p', bobPubKey, 'wss://nostr-relay.psfoundation.info', 'bob']
49
+ ],
50
+ content: '',
51
+ sig: validSig
52
+ }
53
+
54
+ // Kind 7: Reaction/like event
55
+ const mockKind7Event = {
56
+ id: validEventId,
57
+ pubkey: bobPubKey,
58
+ created_at: Math.floor(Date.now() / 1000),
59
+ kind: 7,
60
+ tags: [
61
+ ['e', validEventId, 'wss://nostr-relay.psfoundation.info'],
62
+ ['p', alicePubKey, 'wss://nostr-relay.psfoundation.info']
63
+ ],
64
+ content: '+',
65
+ sig: validSig
66
+ }
67
+
68
+ // Invalid events for testing validation
69
+ const mockInvalidEventMissingId = {
70
+ pubkey: alicePubKey,
71
+ created_at: Math.floor(Date.now() / 1000),
72
+ kind: 1,
73
+ tags: [],
74
+ content: 'Test',
75
+ sig: validSig
76
+ }
77
+
78
+ const mockInvalidEventWrongIdLength = {
79
+ id: 'short',
80
+ pubkey: alicePubKey,
81
+ created_at: Math.floor(Date.now() / 1000),
82
+ kind: 1,
83
+ tags: [],
84
+ content: 'Test',
85
+ sig: validSig
86
+ }
87
+
88
+ const mockInvalidEventMissingPubkey = {
89
+ id: validEventId,
90
+ created_at: Math.floor(Date.now() / 1000),
91
+ kind: 1,
92
+ tags: [],
93
+ content: 'Test',
94
+ sig: validSig
95
+ }
96
+
97
+ const mockInvalidEventWrongPubkeyLength = {
98
+ id: validEventId,
99
+ pubkey: 'short',
100
+ created_at: Math.floor(Date.now() / 1000),
101
+ kind: 1,
102
+ tags: [],
103
+ content: 'Test',
104
+ sig: validSig
105
+ }
106
+
107
+ const mockInvalidEventMissingCreatedAt = {
108
+ id: validEventId,
109
+ pubkey: alicePubKey,
110
+ kind: 1,
111
+ tags: [],
112
+ content: 'Test',
113
+ sig: validSig
114
+ }
115
+
116
+ const mockInvalidEventWrongCreatedAtType = {
117
+ id: validEventId,
118
+ pubkey: alicePubKey,
119
+ created_at: 'not-a-number',
120
+ kind: 1,
121
+ tags: [],
122
+ content: 'Test',
123
+ sig: validSig
124
+ }
125
+
126
+ const mockInvalidEventMissingKind = {
127
+ id: validEventId,
128
+ pubkey: alicePubKey,
129
+ created_at: Math.floor(Date.now() / 1000),
130
+ tags: [],
131
+ content: 'Test',
132
+ sig: validSig
133
+ }
134
+
135
+ const mockInvalidEventKindOutOfRange = {
136
+ id: validEventId,
137
+ pubkey: alicePubKey,
138
+ created_at: Math.floor(Date.now() / 1000),
139
+ kind: 70000, // Out of range (0-65535)
140
+ tags: [],
141
+ content: 'Test',
142
+ sig: validSig
143
+ }
144
+
145
+ const mockInvalidEventMissingSig = {
146
+ id: validEventId,
147
+ pubkey: alicePubKey,
148
+ created_at: Math.floor(Date.now() / 1000),
149
+ kind: 1,
150
+ tags: [],
151
+ content: 'Test'
152
+ }
153
+
154
+ const mockInvalidEventWrongSigLength = {
155
+ id: validEventId,
156
+ pubkey: alicePubKey,
157
+ created_at: Math.floor(Date.now() / 1000),
158
+ kind: 1,
159
+ tags: [],
160
+ content: 'Test',
161
+ sig: 'short'
162
+ }
163
+
164
+ const mockInvalidEventTagsNotArray = {
165
+ id: validEventId,
166
+ pubkey: alicePubKey,
167
+ created_at: Math.floor(Date.now() / 1000),
168
+ kind: 1,
169
+ tags: 'not-an-array',
170
+ content: 'Test',
171
+ sig: validSig
172
+ }
173
+
174
+ export {
175
+ mockKind0Event,
176
+ mockKind1Event,
177
+ mockKind3Event,
178
+ mockKind7Event,
179
+ mockInvalidEventMissingId,
180
+ mockInvalidEventWrongIdLength,
181
+ mockInvalidEventMissingPubkey,
182
+ mockInvalidEventWrongPubkeyLength,
183
+ mockInvalidEventMissingCreatedAt,
184
+ mockInvalidEventWrongCreatedAtType,
185
+ mockInvalidEventMissingKind,
186
+ mockInvalidEventKindOutOfRange,
187
+ mockInvalidEventMissingSig,
188
+ mockInvalidEventWrongSigLength,
189
+ mockInvalidEventTagsNotArray,
190
+ alicePubKey,
191
+ bobPubKey,
192
+ validEventId,
193
+ validSig
194
+ }
@@ -0,0 +1,137 @@
1
+ /*
2
+ Unit tests for BlockchainUseCases.
3
+ */
4
+
5
+ import { assert } from 'chai'
6
+ import sinon from 'sinon'
7
+
8
+ import BlockchainUseCases from '../../../src/use-cases/full-node-blockchain-use-cases.js'
9
+
10
+ describe('#full-node-blockchain-use-cases.js', () => {
11
+ let sandbox
12
+ let mockAdapters
13
+ let uut
14
+
15
+ const createAdapters = () => {
16
+ return {
17
+ fullNode: {
18
+ call: sandbox.stub()
19
+ }
20
+ }
21
+ }
22
+
23
+ beforeEach(() => {
24
+ sandbox = sinon.createSandbox()
25
+ mockAdapters = createAdapters()
26
+ uut = new BlockchainUseCases({ adapters: mockAdapters })
27
+ })
28
+
29
+ afterEach(() => {
30
+ sandbox.restore()
31
+ })
32
+
33
+ describe('#constructor()', () => {
34
+ it('should require adapters', () => {
35
+ assert.throws(() => {
36
+ // eslint-disable-next-line no-new
37
+ new BlockchainUseCases()
38
+ }, /Adapters instance required/)
39
+ })
40
+
41
+ it('should require full node adapter', () => {
42
+ assert.throws(() => {
43
+ // eslint-disable-next-line no-new
44
+ new BlockchainUseCases({ adapters: {} })
45
+ }, /Full node adapter required/)
46
+ })
47
+ })
48
+
49
+ describe('#getBestBlockHash()', () => {
50
+ it('should call full node adapter without parameters', async () => {
51
+ mockAdapters.fullNode.call.resolves('hash')
52
+
53
+ const result = await uut.getBestBlockHash()
54
+
55
+ assert.equal(result, 'hash')
56
+ assert.isTrue(mockAdapters.fullNode.call.calledOnceWithExactly('getbestblockhash'))
57
+ })
58
+ })
59
+
60
+ describe('#getBlockHeaders()', () => {
61
+ it('should call adapter for each hash and return aggregated result', async () => {
62
+ const hashes = ['a'.repeat(64), 'b'.repeat(64)]
63
+ mockAdapters.fullNode.call
64
+ .onFirstCall().resolves('header-1')
65
+ .onSecondCall().resolves('header-2')
66
+
67
+ const result = await uut.getBlockHeaders({ hashes, verbose: true })
68
+
69
+ assert.deepEqual(result, ['header-1', 'header-2'])
70
+ assert.isTrue(
71
+ mockAdapters.fullNode.call.calledWithExactly(
72
+ 'getblockheader',
73
+ [hashes[0], true],
74
+ `getblockheader-${hashes[0]}`
75
+ )
76
+ )
77
+ assert.isTrue(
78
+ mockAdapters.fullNode.call.calledWithExactly(
79
+ 'getblockheader',
80
+ [hashes[1], true],
81
+ `getblockheader-${hashes[1]}`
82
+ )
83
+ )
84
+ })
85
+
86
+ it('should rethrow errors from adapter', async () => {
87
+ const hashes = ['a'.repeat(64)]
88
+ mockAdapters.fullNode.call.rejects(new Error('failure'))
89
+
90
+ try {
91
+ await uut.getBlockHeaders({ hashes })
92
+ assert.fail('Unexpected success')
93
+ } catch (err) {
94
+ assert.equal(err.message, 'failure')
95
+ }
96
+ })
97
+ })
98
+
99
+ describe('#getTxOut()', () => {
100
+ it('should pass parameters to full node call', async () => {
101
+ mockAdapters.fullNode.call.resolves({ value: 1 })
102
+
103
+ const result = await uut.getTxOut({
104
+ txid: 'txid',
105
+ n: 0,
106
+ includeMempool: true
107
+ })
108
+
109
+ assert.deepEqual(result, { value: 1 })
110
+ assert.isTrue(
111
+ mockAdapters.fullNode.call.calledOnceWithExactly(
112
+ 'gettxout',
113
+ ['txid', 0, true]
114
+ )
115
+ )
116
+ })
117
+ })
118
+
119
+ describe('#verifyTxOutProofs()', () => {
120
+ it('should call adapter for each proof and return aggregated results', async () => {
121
+ const proofs = ['proof-1', 'proof-2']
122
+ mockAdapters.fullNode.call.onFirstCall().resolves(['txid-1'])
123
+ mockAdapters.fullNode.call.onSecondCall().resolves(['txid-2'])
124
+
125
+ const result = await uut.verifyTxOutProofs({ proofs })
126
+
127
+ assert.deepEqual(result, [['txid-1'], ['txid-2']])
128
+ assert.isTrue(
129
+ mockAdapters.fullNode.call.calledWithExactly(
130
+ 'verifytxoutproof',
131
+ ['proof-1'],
132
+ `verifytxoutproof-${proofs[0].slice(0, 16)}`
133
+ )
134
+ )
135
+ })
136
+ })
137
+ })