@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.
- package/README.md +1 -3
- package/dist/cjs/mod.js +1 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/transaction/Transaction.js +5 -3
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/src/transaction/broadcasters/ARC.js +37 -25
- package/dist/cjs/src/transaction/broadcasters/ARC.js.map +1 -1
- package/dist/cjs/src/transaction/broadcasters/DefaultBroadcaster.js +12 -0
- package/dist/cjs/src/transaction/broadcasters/DefaultBroadcaster.js.map +1 -0
- package/dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js +66 -0
- package/dist/cjs/src/transaction/broadcasters/WhatsOnChainBroadcaster.js.map +1 -0
- package/dist/cjs/src/transaction/broadcasters/index.js +5 -5
- package/dist/cjs/src/transaction/broadcasters/index.js.map +1 -1
- package/dist/cjs/src/transaction/chaintrackers/DefaultChainTracker.js +12 -0
- package/dist/cjs/src/transaction/chaintrackers/DefaultChainTracker.js.map +1 -0
- package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js +49 -0
- package/dist/cjs/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -0
- package/dist/cjs/src/transaction/chaintrackers/index.js +11 -0
- package/dist/cjs/src/transaction/chaintrackers/index.js.map +1 -0
- package/dist/cjs/src/transaction/{broadcasters → http}/DefaultHttpClient.js +8 -4
- package/dist/cjs/src/transaction/http/DefaultHttpClient.js.map +1 -0
- package/dist/cjs/src/transaction/http/FetchHttpClient.js +29 -0
- package/dist/cjs/src/transaction/http/FetchHttpClient.js.map +1 -0
- package/dist/cjs/src/transaction/http/HttpClient.js.map +1 -0
- package/dist/cjs/src/transaction/{broadcasters → http}/NodejsHttpClient.js +14 -12
- package/dist/cjs/src/transaction/http/NodejsHttpClient.js.map +1 -0
- package/dist/cjs/src/transaction/http/index.js +10 -0
- package/dist/cjs/src/transaction/http/index.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +5 -3
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/src/transaction/broadcasters/ARC.js +37 -25
- package/dist/esm/src/transaction/broadcasters/ARC.js.map +1 -1
- package/dist/esm/src/transaction/broadcasters/DefaultBroadcaster.js +5 -0
- package/dist/esm/src/transaction/broadcasters/DefaultBroadcaster.js.map +1 -0
- package/dist/esm/src/transaction/broadcasters/WhatsOnChainBroadcaster.js +65 -0
- package/dist/esm/src/transaction/broadcasters/WhatsOnChainBroadcaster.js.map +1 -0
- package/dist/esm/src/transaction/broadcasters/index.js +2 -2
- package/dist/esm/src/transaction/broadcasters/index.js.map +1 -1
- package/dist/esm/src/transaction/chaintrackers/DefaultChainTracker.js +5 -0
- package/dist/esm/src/transaction/chaintrackers/DefaultChainTracker.js.map +1 -0
- package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js +50 -0
- package/dist/esm/src/transaction/chaintrackers/WhatsOnChain.js.map +1 -0
- package/dist/esm/src/transaction/chaintrackers/index.js +3 -0
- package/dist/esm/src/transaction/chaintrackers/index.js.map +1 -0
- package/dist/esm/src/transaction/{broadcasters → http}/DefaultHttpClient.js +8 -5
- package/dist/esm/src/transaction/http/DefaultHttpClient.js.map +1 -0
- package/dist/esm/src/transaction/http/FetchHttpClient.js +26 -0
- package/dist/esm/src/transaction/http/FetchHttpClient.js.map +1 -0
- package/dist/esm/src/transaction/http/HttpClient.js.map +1 -0
- package/dist/esm/src/transaction/http/NodejsHttpClient.js +38 -0
- package/dist/esm/src/transaction/http/NodejsHttpClient.js.map +1 -0
- package/dist/esm/src/transaction/http/index.js +4 -0
- package/dist/esm/src/transaction/http/index.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/transaction/Transaction.d.ts +3 -3
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/src/transaction/broadcasters/ARC.d.ts +23 -8
- package/dist/types/src/transaction/broadcasters/ARC.d.ts.map +1 -1
- package/dist/types/src/transaction/broadcasters/DefaultBroadcaster.d.ts +3 -0
- package/dist/types/src/transaction/broadcasters/DefaultBroadcaster.d.ts.map +1 -0
- package/dist/types/src/transaction/broadcasters/WhatsOnChainBroadcaster.d.ts +26 -0
- package/dist/types/src/transaction/broadcasters/WhatsOnChainBroadcaster.d.ts.map +1 -0
- package/dist/types/src/transaction/broadcasters/index.d.ts +3 -4
- package/dist/types/src/transaction/broadcasters/index.d.ts.map +1 -1
- package/dist/types/src/transaction/chaintrackers/DefaultChainTracker.d.ts +3 -0
- package/dist/types/src/transaction/chaintrackers/DefaultChainTracker.d.ts.map +1 -0
- package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts +28 -0
- package/dist/types/src/transaction/chaintrackers/WhatsOnChain.d.ts.map +1 -0
- package/dist/types/src/transaction/chaintrackers/index.d.ts +4 -0
- package/dist/types/src/transaction/chaintrackers/index.d.ts.map +1 -0
- package/dist/types/src/transaction/http/DefaultHttpClient.d.ts +8 -0
- package/dist/types/src/transaction/http/DefaultHttpClient.d.ts.map +1 -0
- package/dist/types/src/transaction/http/FetchHttpClient.d.ts +31 -0
- package/dist/types/src/transaction/http/FetchHttpClient.d.ts.map +1 -0
- package/dist/types/src/transaction/http/HttpClient.d.ts +43 -0
- package/dist/types/src/transaction/http/HttpClient.d.ts.map +1 -0
- package/dist/types/src/transaction/{broadcasters → http}/NodejsHttpClient.d.ts +2 -2
- package/dist/types/src/transaction/http/NodejsHttpClient.d.ts.map +1 -0
- package/dist/types/src/transaction/http/index.d.ts +7 -0
- package/dist/types/src/transaction/http/index.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/examples/EXAMPLE_BUILDING_CUSTOM_TX_BROADCASTER.md +2 -0
- package/docs/examples/EXAMPLE_COMPLEX_TX.md +2 -4
- package/docs/examples/EXAMPLE_SIMPLE_TX.md +44 -5
- package/docs/examples/EXAMPLE_VERIFYING_BEEF.md +29 -22
- package/docs/examples/EXAMPLE_VERIFYING_ROOTS.md +18 -63
- package/docs/examples/GETTING_STARTED_NODE_CJS.md +2 -4
- package/docs/examples/GETTING_STARTED_REACT.md +2 -4
- package/mod.ts +2 -1
- package/package.json +21 -1
- package/src/transaction/Transaction.ts +5 -3
- package/src/transaction/__tests/Transaction.test.ts +62 -0
- package/src/transaction/broadcasters/ARC.ts +75 -27
- package/src/transaction/broadcasters/DefaultBroadcaster.ts +6 -0
- package/src/transaction/broadcasters/WhatsOnChainBroadcaster.ts +70 -0
- package/src/transaction/broadcasters/__tests/ARC.test.ts +92 -19
- package/src/transaction/broadcasters/__tests/WhatsOnChainBroadcaster.test.ts +165 -0
- package/src/transaction/broadcasters/index.ts +3 -4
- package/src/transaction/chaintrackers/DefaultChainTracker.ts +6 -0
- package/src/transaction/chaintrackers/WhatsOnChain.ts +70 -0
- package/src/transaction/chaintrackers/__tests/WhatsOnChainChainTracker.test.ts +135 -0
- package/src/transaction/chaintrackers/index.ts +3 -0
- package/src/transaction/http/DefaultHttpClient.ts +32 -0
- package/src/transaction/http/FetchHttpClient.ts +50 -0
- package/src/transaction/http/HttpClient.ts +45 -0
- package/src/transaction/http/NodejsHttpClient.ts +55 -0
- package/src/transaction/http/index.ts +6 -0
- package/dist/cjs/src/transaction/broadcasters/DefaultHttpClient.js.map +0 -1
- package/dist/cjs/src/transaction/broadcasters/HttpClient.js.map +0 -1
- package/dist/cjs/src/transaction/broadcasters/NodejsHttpClient.js.map +0 -1
- package/dist/esm/src/transaction/broadcasters/DefaultHttpClient.js.map +0 -1
- package/dist/esm/src/transaction/broadcasters/HttpClient.js.map +0 -1
- package/dist/esm/src/transaction/broadcasters/NodejsHttpClient.js +0 -36
- package/dist/esm/src/transaction/broadcasters/NodejsHttpClient.js.map +0 -1
- package/dist/types/src/transaction/broadcasters/DefaultHttpClient.d.ts +0 -6
- package/dist/types/src/transaction/broadcasters/DefaultHttpClient.d.ts.map +0 -1
- package/dist/types/src/transaction/broadcasters/HttpClient.d.ts +0 -33
- package/dist/types/src/transaction/broadcasters/HttpClient.d.ts.map +0 -1
- package/dist/types/src/transaction/broadcasters/NodejsHttpClient.d.ts.map +0 -1
- package/src/transaction/broadcasters/DefaultHttpClient.ts +0 -29
- package/src/transaction/broadcasters/HttpClient.ts +0 -34
- package/src/transaction/broadcasters/NodejsHttpClient.ts +0 -55
- /package/dist/cjs/src/transaction/{broadcasters → http}/HttpClient.js +0 -0
- /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 "
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 = {
|
|
38
|
+
global.window = {fetch: mockFetch} as any
|
|
35
39
|
|
|
36
|
-
const broadcaster = new ARC(URL
|
|
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
|
|
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,
|
|
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,
|
|
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 = {
|
|
152
|
+
global.window = {fetch: mockFetch} as any
|
|
94
153
|
|
|
95
|
-
const broadcaster = new ARC(URL,
|
|
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
|
-
|
|
168
|
+
data: {
|
|
169
|
+
detail: 'Bad request'
|
|
170
|
+
}
|
|
110
171
|
})
|
|
111
|
-
global.window = { fetch: mockFetch } as any
|
|
112
172
|
|
|
113
|
-
const broadcaster = new ARC(URL,
|
|
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 ===
|
|
127
|
-
|
|
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:
|
|
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 {
|
|
3
|
-
export { default as
|
|
4
|
-
export {
|
|
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,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,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
|
+
}
|