@bsv/sdk 1.0.32 → 1.0.34

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 (138) hide show
  1. package/README.md +1 -3
  2. package/dist/cjs/mod.js +1 -0
  3. package/dist/cjs/mod.js.map +1 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/src/primitives/PublicKey.js +36 -0
  6. package/dist/cjs/src/primitives/PublicKey.js.map +1 -1
  7. package/dist/cjs/src/primitives/Signature.js +6 -6
  8. package/dist/cjs/src/primitives/Signature.js.map +1 -1
  9. package/dist/cjs/src/script/Script.js.map +1 -1
  10. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  11. package/dist/cjs/src/transaction/Transaction.js +5 -3
  12. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  13. package/dist/cjs/src/transaction/broadcasters/ARC.js +39 -57
  14. package/dist/cjs/src/transaction/broadcasters/ARC.js.map +1 -1
  15. package/dist/cjs/src/transaction/broadcasters/DefaultBroadcaster.js +12 -0
  16. package/dist/cjs/src/transaction/broadcasters/DefaultBroadcaster.js.map +1 -0
  17. package/dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js +66 -0
  18. package/dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js.map +1 -0
  19. package/dist/cjs/src/transaction/broadcasters/index.js +5 -1
  20. package/dist/cjs/src/transaction/broadcasters/index.js.map +1 -1
  21. package/dist/cjs/src/transaction/chaintrackers/DefaultChainTracker.js +12 -0
  22. package/dist/cjs/src/transaction/chaintrackers/DefaultChainTracker.js.map +1 -0
  23. package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js +49 -0
  24. package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -0
  25. package/dist/cjs/src/transaction/chaintrackers/index.js +11 -0
  26. package/dist/cjs/src/transaction/chaintrackers/index.js.map +1 -0
  27. package/dist/cjs/src/transaction/http/DefaultHttpClient.js +37 -0
  28. package/dist/cjs/src/transaction/http/DefaultHttpClient.js.map +1 -0
  29. package/dist/cjs/src/transaction/http/FetchHttpClient.js +29 -0
  30. package/dist/cjs/src/transaction/http/FetchHttpClient.js.map +1 -0
  31. package/dist/cjs/src/transaction/http/HttpClient.js +3 -0
  32. package/dist/cjs/src/transaction/http/HttpClient.js.map +1 -0
  33. package/dist/cjs/src/transaction/http/NodejsHttpClient.js +41 -0
  34. package/dist/cjs/src/transaction/http/NodejsHttpClient.js.map +1 -0
  35. package/dist/cjs/src/transaction/http/index.js +10 -0
  36. package/dist/cjs/src/transaction/http/index.js.map +1 -0
  37. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  38. package/dist/esm/mod.js +1 -0
  39. package/dist/esm/mod.js.map +1 -1
  40. package/dist/esm/src/primitives/PublicKey.js +36 -0
  41. package/dist/esm/src/primitives/PublicKey.js.map +1 -1
  42. package/dist/esm/src/primitives/Signature.js +6 -6
  43. package/dist/esm/src/primitives/Signature.js.map +1 -1
  44. package/dist/esm/src/script/Script.js.map +1 -1
  45. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  46. package/dist/esm/src/transaction/Transaction.js +5 -3
  47. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  48. package/dist/esm/src/transaction/broadcasters/ARC.js +37 -57
  49. package/dist/esm/src/transaction/broadcasters/ARC.js.map +1 -1
  50. package/dist/esm/src/transaction/broadcasters/DefaultBroadcaster.js +5 -0
  51. package/dist/esm/src/transaction/broadcasters/DefaultBroadcaster.js.map +1 -0
  52. package/dist/esm/src/transaction/broadcasters/WhatsOnChainBroadcaster.js +65 -0
  53. package/dist/esm/src/transaction/broadcasters/WhatsOnChainBroadcaster.js.map +1 -0
  54. package/dist/esm/src/transaction/broadcasters/index.js +2 -0
  55. package/dist/esm/src/transaction/broadcasters/index.js.map +1 -1
  56. package/dist/esm/src/transaction/chaintrackers/DefaultChainTracker.js +5 -0
  57. package/dist/esm/src/transaction/chaintrackers/DefaultChainTracker.js.map +1 -0
  58. package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js +50 -0
  59. package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -0
  60. package/dist/esm/src/transaction/chaintrackers/index.js +3 -0
  61. package/dist/esm/src/transaction/chaintrackers/index.js.map +1 -0
  62. package/dist/esm/src/transaction/http/DefaultHttpClient.js +33 -0
  63. package/dist/esm/src/transaction/http/DefaultHttpClient.js.map +1 -0
  64. package/dist/esm/src/transaction/http/FetchHttpClient.js +26 -0
  65. package/dist/esm/src/transaction/http/FetchHttpClient.js.map +1 -0
  66. package/dist/esm/src/transaction/http/HttpClient.js +2 -0
  67. package/dist/esm/src/transaction/http/HttpClient.js.map +1 -0
  68. package/dist/esm/src/transaction/http/NodejsHttpClient.js +38 -0
  69. package/dist/esm/src/transaction/http/NodejsHttpClient.js.map +1 -0
  70. package/dist/esm/src/transaction/http/index.js +4 -0
  71. package/dist/esm/src/transaction/http/index.js.map +1 -0
  72. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  73. package/dist/types/mod.d.ts +1 -0
  74. package/dist/types/mod.d.ts.map +1 -1
  75. package/dist/types/src/primitives/PublicKey.d.ts +18 -0
  76. package/dist/types/src/primitives/PublicKey.d.ts.map +1 -1
  77. package/dist/types/src/primitives/Signature.d.ts +3 -3
  78. package/dist/types/src/script/Script.d.ts.map +1 -1
  79. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  80. package/dist/types/src/transaction/Transaction.d.ts +7 -7
  81. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  82. package/dist/types/src/transaction/broadcasters/ARC.d.ts +23 -7
  83. package/dist/types/src/transaction/broadcasters/ARC.d.ts.map +1 -1
  84. package/dist/types/src/transaction/broadcasters/DefaultBroadcaster.d.ts +3 -0
  85. package/dist/types/src/transaction/broadcasters/DefaultBroadcaster.d.ts.map +1 -0
  86. package/dist/types/src/transaction/broadcasters/WhatsOnChainBroadcaster.d.ts +26 -0
  87. package/dist/types/src/transaction/broadcasters/WhatsOnChainBroadcaster.d.ts.map +1 -0
  88. package/dist/types/src/transaction/broadcasters/index.d.ts +3 -0
  89. package/dist/types/src/transaction/broadcasters/index.d.ts.map +1 -1
  90. package/dist/types/src/transaction/chaintrackers/DefaultChainTracker.d.ts +3 -0
  91. package/dist/types/src/transaction/chaintrackers/DefaultChainTracker.d.ts.map +1 -0
  92. package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts +28 -0
  93. package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts.map +1 -0
  94. package/dist/types/src/transaction/chaintrackers/index.d.ts +4 -0
  95. package/dist/types/src/transaction/chaintrackers/index.d.ts.map +1 -0
  96. package/dist/types/src/transaction/http/DefaultHttpClient.d.ts +8 -0
  97. package/dist/types/src/transaction/http/DefaultHttpClient.d.ts.map +1 -0
  98. package/dist/types/src/transaction/http/FetchHttpClient.d.ts +31 -0
  99. package/dist/types/src/transaction/http/FetchHttpClient.d.ts.map +1 -0
  100. package/dist/types/src/transaction/http/HttpClient.d.ts +43 -0
  101. package/dist/types/src/transaction/http/HttpClient.d.ts.map +1 -0
  102. package/dist/types/src/transaction/http/NodejsHttpClient.d.ts +21 -0
  103. package/dist/types/src/transaction/http/NodejsHttpClient.d.ts.map +1 -0
  104. package/dist/types/src/transaction/http/index.d.ts +7 -0
  105. package/dist/types/src/transaction/http/index.d.ts.map +1 -0
  106. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  107. package/docs/examples/EXAMPLE_BUILDING_CUSTOM_TX_BROADCASTER.md +2 -0
  108. package/docs/examples/EXAMPLE_COMPLEX_TX.md +2 -4
  109. package/docs/examples/EXAMPLE_SIMPLE_TX.md +83 -2
  110. package/docs/examples/EXAMPLE_VERIFYING_BEEF.md +29 -22
  111. package/docs/examples/EXAMPLE_VERIFYING_ROOTS.md +18 -63
  112. package/docs/examples/GETTING_STARTED_NODE_CJS.md +2 -4
  113. package/docs/examples/GETTING_STARTED_REACT.md +2 -4
  114. package/docs/primitives.md +36 -3
  115. package/docs/transaction.md +48 -0
  116. package/mod.ts +2 -1
  117. package/package.json +21 -1
  118. package/src/primitives/PublicKey.ts +39 -0
  119. package/src/primitives/Signature.ts +6 -6
  120. package/src/script/Script.ts +19 -19
  121. package/src/transaction/MerklePath.ts +9 -9
  122. package/src/transaction/Transaction.ts +13 -13
  123. package/src/transaction/__tests/Transaction.test.ts +62 -0
  124. package/src/transaction/broadcasters/ARC.ts +71 -57
  125. package/src/transaction/broadcasters/DefaultBroadcaster.ts +6 -0
  126. package/src/transaction/broadcasters/WhatsOnChainBroadcaster.ts +70 -0
  127. package/src/transaction/broadcasters/__tests/ARC.test.ts +152 -41
  128. package/src/transaction/broadcasters/__tests/WhatsOnChainBroadcaster.test.ts +165 -0
  129. package/src/transaction/broadcasters/index.ts +3 -0
  130. package/src/transaction/chaintrackers/DefaultChainTracker.ts +6 -0
  131. package/src/transaction/chaintrackers/WhatsOnChain.ts +70 -0
  132. package/src/transaction/chaintrackers/__tests/WhatsOnChainChainTracker.test.ts +135 -0
  133. package/src/transaction/chaintrackers/index.ts +3 -0
  134. package/src/transaction/http/DefaultHttpClient.ts +32 -0
  135. package/src/transaction/http/FetchHttpClient.ts +50 -0
  136. package/src/transaction/http/HttpClient.ts +45 -0
  137. package/src/transaction/http/NodejsHttpClient.ts +55 -0
  138. package/src/transaction/http/index.ts +6 -0
@@ -1,5 +1,8 @@
1
1
  import ARC from '../../../../dist/cjs/src/transaction/broadcasters/ARC.js'
2
2
  import Transaction from '../../../../dist/cjs/src/transaction/Transaction.js'
3
+ import {NodejsHttpClient} from "../../../../dist/cjs/src/transaction/http/NodejsHttpClient.js";
4
+ import {FetchHttpClient} from "../../../../dist/cjs/src/transaction/http/FetchHttpClient.js";
5
+ import {HttpClientRequestOptions} from "../../http";
3
6
 
4
7
  // Mock Transaction
5
8
  jest.mock('../../Transaction', () => {
@@ -14,27 +17,27 @@ jest.mock('../../Transaction', () => {
14
17
 
15
18
  describe('ARC Broadcaster', () => {
16
19
  const URL = 'https://example.com'
17
- const apiKey = 'test_api_key'
18
- let broadcaster: ARC
20
+ const successResponse = {
21
+ status: 200,
22
+ data: {
23
+ txid: 'mocked_txid',
24
+ txStatus: 'success',
25
+ extraInfo: 'received'
26
+ }
27
+ }
28
+
19
29
  let transaction: Transaction
20
30
 
21
31
  beforeEach(() => {
22
- broadcaster = new ARC(URL, apiKey)
23
32
  transaction = new Transaction()
24
33
  })
25
34
 
26
35
  it('should broadcast successfully using window.fetch', async () => {
27
36
  // Mocking window.fetch
28
- const mockFetch = jest.fn().mockResolvedValue({
29
- ok: true,
30
- json: async () => await Promise.resolve({
31
- txid: 'mocked_txid',
32
- txStatus: 'success',
33
- extraInfo: 'received'
34
- })
35
- })
36
- global.window = { fetch: mockFetch } as any
37
+ const mockFetch = mockedFetch(successResponse)
38
+ global.window = {fetch: mockFetch} as any
37
39
 
40
+ const broadcaster = new ARC(URL)
38
41
  const response = await broadcaster.broadcast(transaction)
39
42
 
40
43
  expect(mockFetch).toHaveBeenCalled()
@@ -47,29 +50,10 @@ describe('ARC Broadcaster', () => {
47
50
 
48
51
  it('should broadcast successfully using Node.js https', async () => {
49
52
  // Mocking Node.js https module
50
- jest.mock('https', () => ({
51
- request: (url, options, callback) => {
52
- // eslint-disable-next-line
53
- callback({
54
- statusCode: 200,
55
- on: (event, handler) => {
56
- if (event === 'data') handler(JSON.stringify({
57
- txid: 'mocked_txid',
58
- txStatus: 'success',
59
- extraInfo: 'received'
60
- }))
61
- if (event === 'end') handler()
62
- }
63
- })
64
- return {
65
- on: jest.fn(),
66
- write: jest.fn(),
67
- end: jest.fn()
68
- }
69
- }
70
- }))
71
-
53
+ mockedHttps(successResponse)
72
54
  delete global.window
55
+
56
+ const broadcaster = new ARC(URL)
73
57
  const response = await broadcaster.broadcast(transaction)
74
58
 
75
59
  expect(response).toEqual({
@@ -79,10 +63,95 @@ describe('ARC Broadcaster', () => {
79
63
  })
80
64
  })
81
65
 
66
+ it('should broadcast successfully using provided fetch', async () => {
67
+
68
+ const mockFetch = mockedFetch(successResponse)
69
+
70
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
71
+ const response = await broadcaster.broadcast(transaction)
72
+
73
+ expect(mockFetch).toHaveBeenCalled()
74
+ expect(response).toEqual({
75
+ status: 'success',
76
+ txid: 'mocked_txid',
77
+ message: 'success received'
78
+ })
79
+ })
80
+
81
+ it('should broadcast successfully using provided https', async () => {
82
+
83
+ const mockHttps = mockedHttps(successResponse)
84
+ const broadcaster = new ARC(URL, {httpClient: new NodejsHttpClient(mockHttps)})
85
+
86
+ const response = await broadcaster.broadcast(transaction)
87
+
88
+ expect(response).toEqual({
89
+ status: 'success',
90
+ txid: 'mocked_txid',
91
+ message: 'success received'
92
+ })
93
+ })
94
+
95
+ it('should send default request headers when broadcasting', async () => {
96
+ const mockFetch = mockedFetch(successResponse)
97
+
98
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
99
+ await broadcaster.broadcast(transaction)
100
+
101
+ const {headers} = (mockFetch as jest.Mock).mock.calls[0][1] as HttpClientRequestOptions
102
+
103
+ expect(headers['Content-Type']).toEqual('application/json')
104
+ expect(headers['XDeployment-ID']).toBeDefined()
105
+ expect(headers['XDeployment-ID']).toMatch(/ts-sdk-.*/)
106
+ expect(headers['Authorization']).toBeUndefined()
107
+ })
108
+
109
+ it('should send authorization header when api key is provided', async () => {
110
+ const mockFetch = mockedFetch(successResponse)
111
+ const apiKey = 'mainnet_1234567890'
112
+
113
+ const broadcaster = new ARC(URL, {apiKey, httpClient: new FetchHttpClient(mockFetch)})
114
+ await broadcaster.broadcast(transaction)
115
+
116
+ const {headers} = (mockFetch as jest.Mock).mock.calls[0][1] as HttpClientRequestOptions
117
+
118
+ expect(headers['XDeployment-ID']).toBeDefined()
119
+ expect(headers['XDeployment-ID']).toMatch(/ts-sdk-.*/)
120
+ expect(headers['Authorization']).toEqual(`Bearer ${apiKey}`)
121
+ })
122
+
123
+ it('should handle api key as second argument', async () => {
124
+ const mockFetch = mockedFetch(successResponse)
125
+ global.window = {fetch: mockFetch} as any
126
+
127
+ const apiKey = 'mainnet_1234567890'
128
+
129
+ const broadcaster = new ARC(URL, apiKey)
130
+ await broadcaster.broadcast(transaction)
131
+
132
+ const {headers} = (mockFetch as jest.Mock).mock.calls[0][1] as HttpClientRequestOptions
133
+
134
+ expect(headers['Authorization']).toEqual(`Bearer ${apiKey}`)
135
+ })
136
+
137
+
138
+ it('should send provided deployment id', async () => {
139
+ const mockFetch = mockedFetch(successResponse)
140
+ const deploymentId = 'custom_deployment_id'
141
+
142
+ const broadcaster = new ARC(URL, {deploymentId, httpClient: new FetchHttpClient(mockFetch)})
143
+ await broadcaster.broadcast(transaction)
144
+
145
+ const {headers} = (mockFetch as jest.Mock).mock.calls[0][1] as HttpClientRequestOptions
146
+
147
+ expect(headers['XDeployment-ID']).toEqual(deploymentId)
148
+ })
149
+
82
150
  it('should handle network errors', async () => {
83
151
  const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'))
84
- global.window = { fetch: mockFetch } as any
152
+ global.window = {fetch: mockFetch} as any
85
153
 
154
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
86
155
  const response = await broadcaster.broadcast(transaction)
87
156
 
88
157
  expect(mockFetch).toHaveBeenCalled()
@@ -94,15 +163,14 @@ describe('ARC Broadcaster', () => {
94
163
  })
95
164
 
96
165
  it('should handle non-200 responses', async () => {
97
- const mockFetch = jest.fn().mockResolvedValue({
98
- ok: false,
99
- json: async () => await Promise.resolve({
100
- status: '400',
166
+ const mockFetch = mockedFetch({
167
+ status: '400',
168
+ data: {
101
169
  detail: 'Bad request'
102
- })
170
+ }
103
171
  })
104
- global.window = { fetch: mockFetch } as any
105
172
 
173
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
106
174
  const response = await broadcaster.broadcast(transaction)
107
175
 
108
176
  expect(mockFetch).toHaveBeenCalled()
@@ -112,4 +180,47 @@ describe('ARC Broadcaster', () => {
112
180
  description: 'Bad request'
113
181
  })
114
182
  })
183
+
184
+ function mockedFetch(response) {
185
+ return jest.fn().mockResolvedValue({
186
+ ok: response.status === 200,
187
+ status: response.status,
188
+ statusText: response.status === 200 ? 'OK' : 'Bad request',
189
+ headers: {
190
+ get(key: string) {
191
+ if (key === 'Content-Type') {
192
+ return 'application/json'
193
+ }
194
+ }
195
+ },
196
+ json: async () => response.data
197
+ });
198
+ }
199
+
200
+ function mockedHttps(response) {
201
+ const https = {
202
+ request: (url, options, callback) => {
203
+ // eslint-disable-next-line
204
+ callback({
205
+ statusCode: response.status,
206
+ statusMessage: response.status == 200 ? 'OK' : 'Bad request',
207
+ headers: {
208
+ 'content-type': 'application/json'
209
+ },
210
+ on: (event, handler) => {
211
+ if (event === 'data') handler(JSON.stringify(response.data))
212
+ if (event === 'end') handler()
213
+ }
214
+ })
215
+ return {
216
+ on: jest.fn(),
217
+ write: jest.fn(),
218
+ end: jest.fn()
219
+ }
220
+ }
221
+ }
222
+ jest.mock('https', () => https)
223
+ return https
224
+ }
115
225
  })
226
+
@@ -0,0 +1,165 @@
1
+ import Transaction from '../../../../dist/cjs/src/transaction/Transaction.js'
2
+ import {NodejsHttpClient} from "../../../../dist/cjs/src/transaction/http/NodejsHttpClient.js";
3
+ import WhatsOnChainBroadcaster from "../../../../dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js";
4
+ import {FetchHttpClient} from "../../../../dist/cjs/src/transaction/http/FetchHttpClient.js";
5
+
6
+ // Mock Transaction
7
+ jest.mock('../../Transaction', () => {
8
+ return {
9
+ default: jest.fn().mockImplementation(() => {
10
+ return {
11
+ toHex: () => 'mocked_transaction_hex'
12
+ }
13
+ })
14
+ }
15
+ })
16
+
17
+ describe('WhatsOnChainBroadcaster', () => {
18
+ const network = 'main'
19
+ const successResponse = {
20
+ status: 200,
21
+ data: 'mocked_txid'
22
+ }
23
+
24
+ let transaction: Transaction
25
+
26
+ beforeEach(() => {
27
+ transaction = new Transaction()
28
+ })
29
+
30
+ it('should broadcast successfully using window.fetch', async () => {
31
+ // Mocking window.fetch
32
+ const mockFetch = mockedFetch(successResponse)
33
+ global.window = { fetch: mockFetch } as any
34
+
35
+ const broadcaster = new WhatsOnChainBroadcaster(network)
36
+ const response = await broadcaster.broadcast(transaction)
37
+
38
+ expect(mockFetch).toHaveBeenCalled()
39
+ expect(response).toEqual({
40
+ status: 'success',
41
+ txid: 'mocked_txid',
42
+ message: 'broadcast successful'
43
+ })
44
+ })
45
+
46
+ it('should broadcast successfully using Node.js https', async () => {
47
+ // Mocking Node.js https module
48
+ mockedHttps(successResponse)
49
+ delete global.window
50
+
51
+ const broadcaster = new WhatsOnChainBroadcaster(network)
52
+ const response = await broadcaster.broadcast(transaction)
53
+
54
+ expect(response).toEqual({
55
+ status: 'success',
56
+ txid: 'mocked_txid',
57
+ message: 'broadcast successful'
58
+ })
59
+ })
60
+
61
+ it('should broadcast successfully using provided fetch', async () => {
62
+
63
+ const mockFetch = mockedFetch(successResponse)
64
+
65
+ const broadcaster = new WhatsOnChainBroadcaster(network, new FetchHttpClient(mockFetch))
66
+ const response = await broadcaster.broadcast(transaction)
67
+
68
+ expect(mockFetch).toHaveBeenCalled()
69
+ expect(response).toEqual({
70
+ status: 'success',
71
+ txid: 'mocked_txid',
72
+ message: 'broadcast successful'
73
+ })
74
+ })
75
+
76
+ it('should broadcast successfully using provided https', async () => {
77
+
78
+ const mockHttps = mockedHttps(successResponse)
79
+ const broadcaster = new WhatsOnChainBroadcaster(network, new NodejsHttpClient(mockHttps))
80
+
81
+ const response = await broadcaster.broadcast(transaction)
82
+
83
+ expect(response).toEqual({
84
+ status: 'success',
85
+ txid: 'mocked_txid',
86
+ message: 'broadcast successful'
87
+ })
88
+ })
89
+
90
+ it('should handle network errors', async () => {
91
+ const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'))
92
+ global.window = { fetch: mockFetch } as any
93
+
94
+ const broadcaster = new WhatsOnChainBroadcaster(network)
95
+ const response = await broadcaster.broadcast(transaction)
96
+
97
+ expect(mockFetch).toHaveBeenCalled()
98
+ expect(response).toEqual({
99
+ status: 'error',
100
+ code: '500',
101
+ description: 'Network error'
102
+ })
103
+ })
104
+
105
+ it('should handle non-200 responses', async () => {
106
+ const mockFetch = mockedFetch({
107
+ status: 400,
108
+ data: 'Bad request'
109
+ })
110
+ global.window = { fetch: mockFetch } as any
111
+
112
+ const broadcaster = new WhatsOnChainBroadcaster(network)
113
+ const response = await broadcaster.broadcast(transaction)
114
+
115
+ expect(mockFetch).toHaveBeenCalled()
116
+ expect(response).toEqual({
117
+ status: 'error',
118
+ code: '400',
119
+ description: 'Bad request'
120
+ })
121
+ })
122
+
123
+ function mockedFetch(response) {
124
+ return jest.fn().mockResolvedValue({
125
+ ok: response.status === 200,
126
+ status: response.status,
127
+ statusText: response.status === 200 ? 'OK' : 'Bad request',
128
+ headers: {
129
+ get(key: string) {
130
+ if (key === 'Content-Type') {
131
+ return 'text/plain'
132
+ }
133
+ }
134
+ },
135
+ text: async () => response.data
136
+ });
137
+ }
138
+
139
+ function mockedHttps(response) {
140
+ const https = {
141
+ request: (url, options, callback) => {
142
+ // eslint-disable-next-line
143
+ callback({
144
+ statusCode: response.status,
145
+ statusMessage: response.status == 200 ? 'OK' : 'Bad request',
146
+ headers: {
147
+ 'content-type': 'text/plain'
148
+ },
149
+ on: (event, handler) => {
150
+ if (event === 'data') handler(response.data)
151
+ if (event === 'end') handler()
152
+ }
153
+ })
154
+ return {
155
+ on: jest.fn(),
156
+ write: jest.fn(),
157
+ end: jest.fn()
158
+ }
159
+ }
160
+ }
161
+ jest.mock('https', () => https)
162
+ return https
163
+ }
164
+ })
165
+
@@ -1 +1,4 @@
1
1
  export { default as ARC } from './ARC.js'
2
+ export type { ArcConfig } from './ARC.js'
3
+ export { default as WhatsOnChainBroadcaster } from './WhatsOnChainBroadcaster.js'
4
+ export { defaultBroadcaster } from './DefaultBroadcaster.js'
@@ -0,0 +1,6 @@
1
+ import WhatsOnChain from "./WhatsOnChain.js";
2
+ import ChainTracker from "../ChainTracker.js";
3
+
4
+ export function defaultChainTracker(): ChainTracker {
5
+ return new WhatsOnChain()
6
+ }
@@ -0,0 +1,70 @@
1
+ import ChainTracker from "../ChainTracker.js";
2
+ import {HttpClient} from "../http/HttpClient.js";
3
+ import {defaultHttpClient} from "../http/DefaultHttpClient.js";
4
+
5
+ /** Configuration options for the WhatsOnChain ChainTracker. */
6
+ export interface WhatsOnChainConfig {
7
+ /** Authentication token for the WhatsOnChain API */
8
+ apiKey?: string
9
+ /** The HTTP client used to make requests to the API. */
10
+ httpClient?: HttpClient
11
+ }
12
+
13
+ interface WhatsOnChainBlockHeader {
14
+ merkleroot: string
15
+ }
16
+
17
+ /**
18
+ * Represents a chain tracker based on What's On Chain .
19
+ */
20
+ export default class WhatsOnChain implements ChainTracker {
21
+ readonly network: string
22
+ readonly apiKey: string
23
+ private readonly URL: string
24
+ private readonly httpClient: HttpClient
25
+
26
+ /**
27
+ * Constructs an instance of the WhatsOnChain ChainTracker.
28
+ *
29
+ * @param {'main' | 'test' | 'stn'} network - The BSV network to use when calling the WhatsOnChain API.
30
+ * @param {WhatsOnChainConfig} config - Configuration options for the WhatsOnChain ChainTracker.
31
+ */
32
+ constructor(network: 'main' | 'test' | 'stn' = 'main', config: WhatsOnChainConfig = {}) {
33
+ const {apiKey, httpClient} = config
34
+ this.network = network
35
+ this.URL = `https://api.whatsonchain.com/v1/bsv/${network}`
36
+ this.httpClient = httpClient ?? defaultHttpClient()
37
+ this.apiKey = apiKey
38
+ }
39
+
40
+ async isValidRootForHeight(root: string, height: number): Promise<boolean> {
41
+ const requestOptions = {
42
+ method: 'GET',
43
+ headers: this.getHeaders()
44
+ }
45
+
46
+ const response = await this.httpClient.request<WhatsOnChainBlockHeader>(`${this.URL}/block/${height}/header`, requestOptions)
47
+ if (response.ok) {
48
+ const { merkleroot} = response.data
49
+ return merkleroot === root
50
+ } else if (response.status === 404) {
51
+ return false
52
+ } else {
53
+ throw new Error(`Failed to verify merkleroot for height ${height} because of an error: ${JSON.stringify(response.data)} `)
54
+ }
55
+ }
56
+
57
+ private getHeaders() {
58
+ const headers: Record<string, string> = {
59
+ 'Accept': 'application/json',
60
+ }
61
+
62
+ if (this.apiKey) {
63
+ headers['Authorization'] = this.apiKey
64
+ }
65
+
66
+ return headers
67
+ }
68
+ }
69
+
70
+
@@ -0,0 +1,135 @@
1
+ import {NodejsHttpClient} from "../../../../dist/cjs/src/transaction/http/NodejsHttpClient.js";
2
+ import WhatsOnChain from "../../../../dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js";
3
+ import {FetchHttpClient} from "../../../../dist/cjs/src/transaction/http/FetchHttpClient.js";
4
+
5
+
6
+ describe('WhatsOnChain ChainTracker', () => {
7
+ const network = 'main'
8
+ const height = 123456
9
+ const merkleroot = 'mocked_merkleroot'
10
+
11
+ const successResponse = {
12
+ status: 200,
13
+ data: {
14
+ merkleroot
15
+ }
16
+ }
17
+
18
+
19
+
20
+ it('should verify merkleroot successfully using window.fetch', async () => {
21
+ // Mocking window.fetch
22
+ const mockFetch = mockedFetch(successResponse)
23
+ global.window = { fetch: mockFetch } as any
24
+
25
+ const chainTracker = new WhatsOnChain(network)
26
+ const response = await chainTracker.isValidRootForHeight(merkleroot, height)
27
+
28
+ expect(mockFetch).toHaveBeenCalled()
29
+ expect(response).toEqual(true)
30
+ })
31
+
32
+ it('should verify merkleroot successfully using Node.js https', async () => {
33
+ // Mocking Node.js https module
34
+ mockedHttps(successResponse)
35
+ delete global.window
36
+
37
+ const chainTracker = new WhatsOnChain(network)
38
+ const response = await chainTracker.isValidRootForHeight(merkleroot, height)
39
+
40
+ expect(response).toEqual(true)
41
+ })
42
+
43
+ it('should verify merkleroot successfully using provided window.fetch', async () => {
44
+ const mockFetch = mockedFetch(successResponse)
45
+
46
+ const chainTracker = new WhatsOnChain(network, {httpClient: new FetchHttpClient(mockFetch)})
47
+ const response = await chainTracker.isValidRootForHeight(merkleroot, height)
48
+
49
+ expect(mockFetch).toHaveBeenCalled()
50
+ expect(response).toEqual(true)
51
+ })
52
+
53
+ it('should verify merkleroot successfully using provided Node.js https', async () => {
54
+ const mockHttps = mockedHttps(successResponse)
55
+
56
+ const chainTracker = new WhatsOnChain(network, {httpClient: new NodejsHttpClient(mockHttps)})
57
+ const response = await chainTracker.isValidRootForHeight(merkleroot, height)
58
+
59
+ expect(response).toEqual(true)
60
+ })
61
+
62
+ it('should respond with invalid root for height when block for height is not found', async () => {
63
+ const mockFetch = mockedFetch({
64
+ status: 404,
65
+ data: "not found"
66
+ })
67
+
68
+ const chainTracker = new WhatsOnChain(network, {httpClient: new FetchHttpClient(mockFetch)})
69
+ const response = await chainTracker.isValidRootForHeight(merkleroot, height)
70
+
71
+ expect(response).toEqual(false)
72
+ })
73
+
74
+ it('should handle network errors', async () => {
75
+ const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'))
76
+
77
+ const chainTracker = new WhatsOnChain(network, {httpClient: new FetchHttpClient(mockFetch)})
78
+
79
+ await expect(chainTracker.isValidRootForHeight(merkleroot, height)).rejects.toThrow('Network error')
80
+ })
81
+
82
+ it('should throw error when received error response', async () => {
83
+ const mockFetch = mockedFetch({
84
+ status: 401,
85
+ data: { error: 'Unauthorized' }
86
+ })
87
+
88
+ const chainTracker = new WhatsOnChain(network, {httpClient: new FetchHttpClient(mockFetch)})
89
+
90
+ await expect(chainTracker.isValidRootForHeight(merkleroot, height)).rejects.toThrow(/Failed to verify merkleroot for height \d+ because of an error: .*/)
91
+ })
92
+
93
+ function mockedFetch(response) {
94
+ return jest.fn().mockResolvedValue({
95
+ ok: response.status === 200,
96
+ status: response.status,
97
+ statusText: response.status === 200 ? 'OK' : 'Bad request',
98
+ headers: {
99
+ get(key: string) {
100
+ if (key === 'Content-Type') {
101
+ return 'application/json'
102
+ }
103
+ }
104
+ },
105
+ json: async () => response.data
106
+ });
107
+ }
108
+
109
+ function mockedHttps(response) {
110
+ const https = {
111
+ request: (url, options, callback) => {
112
+ // eslint-disable-next-line
113
+ callback({
114
+ statusCode: response.status,
115
+ statusMessage: response.status == 200 ? 'OK' : 'Bad request',
116
+ headers: {
117
+ 'content-type': 'application/json'
118
+ },
119
+ on: (event, handler) => {
120
+ if (event === 'data') handler(JSON.stringify(response.data))
121
+ if (event === 'end') handler()
122
+ }
123
+ })
124
+ return {
125
+ on: jest.fn(),
126
+ write: jest.fn(),
127
+ end: jest.fn()
128
+ }
129
+ }
130
+ }
131
+ jest.mock('https', () => https)
132
+ return https
133
+ }
134
+ })
135
+
@@ -0,0 +1,3 @@
1
+ export { default as WhatsOnChain } from './WhatsOnChain.js'
2
+ export type { WhatsOnChainConfig } from './WhatsOnChain.js'
3
+ export { defaultChainTracker } from './DefaultChainTracker.js'
@@ -0,0 +1,32 @@
1
+ import { HttpClient, HttpClientResponse } from './HttpClient.js';
2
+ import { NodejsHttpClient } from './NodejsHttpClient.js';
3
+ import { FetchHttpClient } from './FetchHttpClient.js';
4
+
5
+ /**
6
+ * Returns a default HttpClient implementation based on the environment that it is run on.
7
+ * This method will attempt to use `window.fetch` if available (in browser environments).
8
+ * If running in a Node.js environment, it falls back to using the Node.js `https` module
9
+ */
10
+ export function defaultHttpClient(): HttpClient {
11
+ const noHttpClient: HttpClient = {
12
+ request(..._): Promise<HttpClientResponse> {
13
+ throw new Error('No method available to perform HTTP request');
14
+ },
15
+ };
16
+
17
+ if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
18
+ // Use fetch in a browser environment
19
+ return new FetchHttpClient(window.fetch);
20
+ } else if (typeof require !== 'undefined') {
21
+ // Use Node.js https module
22
+ // eslint-disable-next-line
23
+ try {
24
+ const https = require('https');
25
+ return new NodejsHttpClient(https);
26
+ } catch (e) {
27
+ return noHttpClient;
28
+ }
29
+ } else {
30
+ return noHttpClient;
31
+ }
32
+ }