@bsv/sdk 1.0.33 → 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 (130) 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/transaction/Transaction.js +5 -3
  6. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  7. package/dist/cjs/src/transaction/broadcasters/ARC.js +37 -25
  8. package/dist/cjs/src/transaction/broadcasters/ARC.js.map +1 -1
  9. package/dist/cjs/src/transaction/broadcasters/DefaultBroadcaster.js +12 -0
  10. package/dist/cjs/src/transaction/broadcasters/DefaultBroadcaster.js.map +1 -0
  11. package/dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js +66 -0
  12. package/dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js.map +1 -0
  13. package/dist/cjs/src/transaction/broadcasters/index.js +5 -5
  14. package/dist/cjs/src/transaction/broadcasters/index.js.map +1 -1
  15. package/dist/cjs/src/transaction/chaintrackers/DefaultChainTracker.js +12 -0
  16. package/dist/cjs/src/transaction/chaintrackers/DefaultChainTracker.js.map +1 -0
  17. package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js +49 -0
  18. package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -0
  19. package/dist/cjs/src/transaction/chaintrackers/index.js +11 -0
  20. package/dist/cjs/src/transaction/chaintrackers/index.js.map +1 -0
  21. package/dist/cjs/src/transaction/{broadcasters → http}/DefaultHttpClient.js +8 -4
  22. package/dist/cjs/src/transaction/http/DefaultHttpClient.js.map +1 -0
  23. package/dist/cjs/src/transaction/http/FetchHttpClient.js +29 -0
  24. package/dist/cjs/src/transaction/http/FetchHttpClient.js.map +1 -0
  25. package/dist/cjs/src/transaction/http/HttpClient.js.map +1 -0
  26. package/dist/cjs/src/transaction/{broadcasters → http}/NodejsHttpClient.js +14 -12
  27. package/dist/cjs/src/transaction/http/NodejsHttpClient.js.map +1 -0
  28. package/dist/cjs/src/transaction/http/index.js +10 -0
  29. package/dist/cjs/src/transaction/http/index.js.map +1 -0
  30. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  31. package/dist/esm/mod.js +1 -0
  32. package/dist/esm/mod.js.map +1 -1
  33. package/dist/esm/src/transaction/Transaction.js +5 -3
  34. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  35. package/dist/esm/src/transaction/broadcasters/ARC.js +37 -25
  36. package/dist/esm/src/transaction/broadcasters/ARC.js.map +1 -1
  37. package/dist/esm/src/transaction/broadcasters/DefaultBroadcaster.js +5 -0
  38. package/dist/esm/src/transaction/broadcasters/DefaultBroadcaster.js.map +1 -0
  39. package/dist/esm/src/transaction/broadcasters/WhatsOnChainBroadcaster.js +65 -0
  40. package/dist/esm/src/transaction/broadcasters/WhatsOnChainBroadcaster.js.map +1 -0
  41. package/dist/esm/src/transaction/broadcasters/index.js +2 -2
  42. package/dist/esm/src/transaction/broadcasters/index.js.map +1 -1
  43. package/dist/esm/src/transaction/chaintrackers/DefaultChainTracker.js +5 -0
  44. package/dist/esm/src/transaction/chaintrackers/DefaultChainTracker.js.map +1 -0
  45. package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js +50 -0
  46. package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -0
  47. package/dist/esm/src/transaction/chaintrackers/index.js +3 -0
  48. package/dist/esm/src/transaction/chaintrackers/index.js.map +1 -0
  49. package/dist/esm/src/transaction/{broadcasters → http}/DefaultHttpClient.js +8 -5
  50. package/dist/esm/src/transaction/http/DefaultHttpClient.js.map +1 -0
  51. package/dist/esm/src/transaction/http/FetchHttpClient.js +26 -0
  52. package/dist/esm/src/transaction/http/FetchHttpClient.js.map +1 -0
  53. package/dist/esm/src/transaction/http/HttpClient.js.map +1 -0
  54. package/dist/esm/src/transaction/http/NodejsHttpClient.js +38 -0
  55. package/dist/esm/src/transaction/http/NodejsHttpClient.js.map +1 -0
  56. package/dist/esm/src/transaction/http/index.js +4 -0
  57. package/dist/esm/src/transaction/http/index.js.map +1 -0
  58. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  59. package/dist/types/mod.d.ts +1 -0
  60. package/dist/types/mod.d.ts.map +1 -1
  61. package/dist/types/src/transaction/Transaction.d.ts +3 -3
  62. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  63. package/dist/types/src/transaction/broadcasters/ARC.d.ts +23 -8
  64. package/dist/types/src/transaction/broadcasters/ARC.d.ts.map +1 -1
  65. package/dist/types/src/transaction/broadcasters/DefaultBroadcaster.d.ts +3 -0
  66. package/dist/types/src/transaction/broadcasters/DefaultBroadcaster.d.ts.map +1 -0
  67. package/dist/types/src/transaction/broadcasters/WhatsOnChainBroadcaster.d.ts +26 -0
  68. package/dist/types/src/transaction/broadcasters/WhatsOnChainBroadcaster.d.ts.map +1 -0
  69. package/dist/types/src/transaction/broadcasters/index.d.ts +3 -4
  70. package/dist/types/src/transaction/broadcasters/index.d.ts.map +1 -1
  71. package/dist/types/src/transaction/chaintrackers/DefaultChainTracker.d.ts +3 -0
  72. package/dist/types/src/transaction/chaintrackers/DefaultChainTracker.d.ts.map +1 -0
  73. package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts +28 -0
  74. package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts.map +1 -0
  75. package/dist/types/src/transaction/chaintrackers/index.d.ts +4 -0
  76. package/dist/types/src/transaction/chaintrackers/index.d.ts.map +1 -0
  77. package/dist/types/src/transaction/http/DefaultHttpClient.d.ts +8 -0
  78. package/dist/types/src/transaction/http/DefaultHttpClient.d.ts.map +1 -0
  79. package/dist/types/src/transaction/http/FetchHttpClient.d.ts +31 -0
  80. package/dist/types/src/transaction/http/FetchHttpClient.d.ts.map +1 -0
  81. package/dist/types/src/transaction/http/HttpClient.d.ts +43 -0
  82. package/dist/types/src/transaction/http/HttpClient.d.ts.map +1 -0
  83. package/dist/types/src/transaction/{broadcasters → http}/NodejsHttpClient.d.ts +2 -2
  84. package/dist/types/src/transaction/http/NodejsHttpClient.d.ts.map +1 -0
  85. package/dist/types/src/transaction/http/index.d.ts +7 -0
  86. package/dist/types/src/transaction/http/index.d.ts.map +1 -0
  87. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  88. package/docs/examples/EXAMPLE_BUILDING_CUSTOM_TX_BROADCASTER.md +2 -0
  89. package/docs/examples/EXAMPLE_COMPLEX_TX.md +2 -4
  90. package/docs/examples/EXAMPLE_SIMPLE_TX.md +44 -5
  91. package/docs/examples/EXAMPLE_VERIFYING_BEEF.md +29 -22
  92. package/docs/examples/EXAMPLE_VERIFYING_ROOTS.md +18 -63
  93. package/docs/examples/GETTING_STARTED_NODE_CJS.md +2 -4
  94. package/docs/examples/GETTING_STARTED_REACT.md +2 -4
  95. package/mod.ts +2 -1
  96. package/package.json +21 -1
  97. package/src/transaction/Transaction.ts +5 -3
  98. package/src/transaction/__tests/Transaction.test.ts +62 -0
  99. package/src/transaction/broadcasters/ARC.ts +75 -27
  100. package/src/transaction/broadcasters/DefaultBroadcaster.ts +6 -0
  101. package/src/transaction/broadcasters/WhatsOnChainBroadcaster.ts +70 -0
  102. package/src/transaction/broadcasters/__tests/ARC.test.ts +92 -19
  103. package/src/transaction/broadcasters/__tests/WhatsOnChainBroadcaster.test.ts +165 -0
  104. package/src/transaction/broadcasters/index.ts +3 -4
  105. package/src/transaction/chaintrackers/DefaultChainTracker.ts +6 -0
  106. package/src/transaction/chaintrackers/WhatsOnChain.ts +70 -0
  107. package/src/transaction/chaintrackers/__tests/WhatsOnChainChainTracker.test.ts +135 -0
  108. package/src/transaction/chaintrackers/index.ts +3 -0
  109. package/src/transaction/http/DefaultHttpClient.ts +32 -0
  110. package/src/transaction/http/FetchHttpClient.ts +50 -0
  111. package/src/transaction/http/HttpClient.ts +45 -0
  112. package/src/transaction/http/NodejsHttpClient.ts +55 -0
  113. package/src/transaction/http/index.ts +6 -0
  114. package/dist/cjs/src/transaction/broadcasters/DefaultHttpClient.js.map +0 -1
  115. package/dist/cjs/src/transaction/broadcasters/HttpClient.js.map +0 -1
  116. package/dist/cjs/src/transaction/broadcasters/NodejsHttpClient.js.map +0 -1
  117. package/dist/esm/src/transaction/broadcasters/DefaultHttpClient.js.map +0 -1
  118. package/dist/esm/src/transaction/broadcasters/HttpClient.js.map +0 -1
  119. package/dist/esm/src/transaction/broadcasters/NodejsHttpClient.js +0 -36
  120. package/dist/esm/src/transaction/broadcasters/NodejsHttpClient.js.map +0 -1
  121. package/dist/types/src/transaction/broadcasters/DefaultHttpClient.d.ts +0 -6
  122. package/dist/types/src/transaction/broadcasters/DefaultHttpClient.d.ts.map +0 -1
  123. package/dist/types/src/transaction/broadcasters/HttpClient.d.ts +0 -33
  124. package/dist/types/src/transaction/broadcasters/HttpClient.d.ts.map +0 -1
  125. package/dist/types/src/transaction/broadcasters/NodejsHttpClient.d.ts.map +0 -1
  126. package/src/transaction/broadcasters/DefaultHttpClient.ts +0 -29
  127. package/src/transaction/broadcasters/HttpClient.ts +0 -34
  128. package/src/transaction/broadcasters/NodejsHttpClient.ts +0 -55
  129. /package/dist/cjs/src/transaction/{broadcasters → http}/HttpClient.js +0 -0
  130. /package/dist/esm/src/transaction/{broadcasters → http}/HttpClient.js +0 -0
@@ -0,0 +1,70 @@
1
+ import {BroadcastResponse, BroadcastFailure, Broadcaster} from '../Broadcaster.js'
2
+ import Transaction from '../Transaction.js'
3
+ import {HttpClient} from "../http/HttpClient.js";
4
+ import {defaultHttpClient} from "../http/DefaultHttpClient.js";
5
+
6
+ /**
7
+ * Represents an WhatsOnChain transaction broadcaster.
8
+ */
9
+ export default class WhatsOnChainBroadcaster implements Broadcaster {
10
+ readonly network: string
11
+ private readonly URL: string
12
+ private readonly httpClient: HttpClient;
13
+
14
+ /**
15
+ * Constructs an instance of the WhatsOnChain broadcaster.
16
+ *
17
+ * @param {'main' | 'test' | 'stn'} network - The BSV network to use when calling the WhatsOnChain API.
18
+ * @param {HttpClient} httpClient - The HTTP client used to make requests to the API.
19
+ */
20
+ constructor(network: 'main' | 'test' | 'stn' = 'main', httpClient: HttpClient = defaultHttpClient()) {
21
+ this.network = network
22
+ this.URL = `https://api.whatsonchain.com/v1/bsv/${network}/tx/raw`
23
+ this.httpClient = httpClient
24
+ }
25
+
26
+ /**
27
+ * Broadcasts a transaction via WhatsOnChain.
28
+ *
29
+ * @param {Transaction} tx - The transaction to be broadcasted.
30
+ * @returns {Promise<BroadcastResponse | BroadcastFailure>} A promise that resolves to either a success or failure response.
31
+ */
32
+ async broadcast(tx: Transaction): Promise<BroadcastResponse | BroadcastFailure> {
33
+ let rawTx = tx.toHex()
34
+
35
+ const requestOptions = {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'Accept': 'text/plain'
40
+ },
41
+ data: {txhex: rawTx}
42
+ }
43
+
44
+ try {
45
+ const response = await this.httpClient.request<string>(this.URL, requestOptions)
46
+ if (response.ok) {
47
+ const txid = response.data
48
+ return {
49
+ status: 'success',
50
+ txid: txid,
51
+ message: 'broadcast successful'
52
+ }
53
+ } else {
54
+ return {
55
+ status: 'error',
56
+ code: response.status.toString() ?? 'ERR_UNKNOWN',
57
+ description: response.data ?? 'Unknown error'
58
+ }
59
+ }
60
+ } catch (error) {
61
+ return {
62
+ status: 'error',
63
+ code: '500',
64
+ description: typeof error.message === 'string'
65
+ ? error.message
66
+ : 'Internal Server Error'
67
+ }
68
+ }
69
+ }
70
+ }
@@ -1,6 +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 "../NodejsHttpClient";
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";
4
6
 
5
7
  // Mock Transaction
6
8
  jest.mock('../../Transaction', () => {
@@ -15,11 +17,13 @@ jest.mock('../../Transaction', () => {
15
17
 
16
18
  describe('ARC Broadcaster', () => {
17
19
  const URL = 'https://example.com'
18
- const apiKey = 'test_api_key'
19
20
  const successResponse = {
20
- txid: 'mocked_txid',
21
- txStatus: 'success',
22
- extraInfo: 'received'
21
+ status: 200,
22
+ data: {
23
+ txid: 'mocked_txid',
24
+ txStatus: 'success',
25
+ extraInfo: 'received'
26
+ }
23
27
  }
24
28
 
25
29
  let transaction: Transaction
@@ -31,9 +35,9 @@ describe('ARC Broadcaster', () => {
31
35
  it('should broadcast successfully using window.fetch', async () => {
32
36
  // Mocking window.fetch
33
37
  const mockFetch = mockedFetch(successResponse)
34
- global.window = { fetch: mockFetch } as any
38
+ global.window = {fetch: mockFetch} as any
35
39
 
36
- const broadcaster = new ARC(URL, apiKey)
40
+ const broadcaster = new ARC(URL)
37
41
  const response = await broadcaster.broadcast(transaction)
38
42
 
39
43
  expect(mockFetch).toHaveBeenCalled()
@@ -49,7 +53,7 @@ describe('ARC Broadcaster', () => {
49
53
  mockedHttps(successResponse)
50
54
  delete global.window
51
55
 
52
- const broadcaster = new ARC(URL, apiKey)
56
+ const broadcaster = new ARC(URL)
53
57
  const response = await broadcaster.broadcast(transaction)
54
58
 
55
59
  expect(response).toEqual({
@@ -63,7 +67,7 @@ describe('ARC Broadcaster', () => {
63
67
 
64
68
  const mockFetch = mockedFetch(successResponse)
65
69
 
66
- const broadcaster = new ARC(URL, apiKey, { fetch: mockFetch })
70
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
67
71
  const response = await broadcaster.broadcast(transaction)
68
72
 
69
73
  expect(mockFetch).toHaveBeenCalled()
@@ -77,7 +81,7 @@ describe('ARC Broadcaster', () => {
77
81
  it('should broadcast successfully using provided https', async () => {
78
82
 
79
83
  const mockHttps = mockedHttps(successResponse)
80
- const broadcaster = new ARC(URL, apiKey, new NodejsHttpClient(mockHttps))
84
+ const broadcaster = new ARC(URL, {httpClient: new NodejsHttpClient(mockHttps)})
81
85
 
82
86
  const response = await broadcaster.broadcast(transaction)
83
87
 
@@ -88,11 +92,66 @@ describe('ARC Broadcaster', () => {
88
92
  })
89
93
  })
90
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
+
91
150
  it('should handle network errors', async () => {
92
151
  const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'))
93
- global.window = { fetch: mockFetch } as any
152
+ global.window = {fetch: mockFetch} as any
94
153
 
95
- const broadcaster = new ARC(URL, apiKey)
154
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
96
155
  const response = await broadcaster.broadcast(transaction)
97
156
 
98
157
  expect(mockFetch).toHaveBeenCalled()
@@ -106,11 +165,12 @@ describe('ARC Broadcaster', () => {
106
165
  it('should handle non-200 responses', async () => {
107
166
  const mockFetch = mockedFetch({
108
167
  status: '400',
109
- detail: 'Bad request'
168
+ data: {
169
+ detail: 'Bad request'
170
+ }
110
171
  })
111
- global.window = { fetch: mockFetch } as any
112
172
 
113
- const broadcaster = new ARC(URL, apiKey)
173
+ const broadcaster = new ARC(URL, {httpClient: new FetchHttpClient(mockFetch)})
114
174
  const response = await broadcaster.broadcast(transaction)
115
175
 
116
176
  expect(mockFetch).toHaveBeenCalled()
@@ -123,8 +183,17 @@ describe('ARC Broadcaster', () => {
123
183
 
124
184
  function mockedFetch(response) {
125
185
  return jest.fn().mockResolvedValue({
126
- ok: response.status === '200',
127
- json: async () => response
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
128
197
  });
129
198
  }
130
199
 
@@ -133,9 +202,13 @@ describe('ARC Broadcaster', () => {
133
202
  request: (url, options, callback) => {
134
203
  // eslint-disable-next-line
135
204
  callback({
136
- statusCode: 200,
205
+ statusCode: response.status,
206
+ statusMessage: response.status == 200 ? 'OK' : 'Bad request',
207
+ headers: {
208
+ 'content-type': 'application/json'
209
+ },
137
210
  on: (event, handler) => {
138
- if (event === 'data') handler(JSON.stringify(response))
211
+ if (event === 'data') handler(JSON.stringify(response.data))
139
212
  if (event === 'end') handler()
140
213
  }
141
214
  })
@@ -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,5 +1,4 @@
1
1
  export { default as ARC } from './ARC.js'
2
- export type { HttpClient, HttpClientResponse, HttpClientRequestOptions } from './HttpClient.js'
3
- export { default as defaultHttpClient } from './DefaultHttpClient.js'
4
- export { NodejsHttpClient } from './NodejsHttpClient.js'
5
- export type { HttpsNodejs, NodejsHttpClientRequest } from './NodejsHttpClient.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
+ }