@bsv/sdk 1.0.33 → 1.0.36

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 (133) hide show
  1. package/README.md +1 -3
  2. package/dist/cjs/mod.js +2 -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 +2 -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 +2 -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/docs/primitives.md +61 -59
  96. package/docs/script.md +13 -11
  97. package/docs/transaction.md +489 -35
  98. package/mod.ts +3 -1
  99. package/package.json +21 -1
  100. package/src/transaction/Transaction.ts +5 -3
  101. package/src/transaction/__tests/Transaction.test.ts +62 -0
  102. package/src/transaction/broadcasters/ARC.ts +75 -27
  103. package/src/transaction/broadcasters/DefaultBroadcaster.ts +6 -0
  104. package/src/transaction/broadcasters/WhatsOnChainBroadcaster.ts +70 -0
  105. package/src/transaction/broadcasters/__tests/ARC.test.ts +92 -19
  106. package/src/transaction/broadcasters/__tests/WhatsOnChainBroadcaster.test.ts +165 -0
  107. package/src/transaction/broadcasters/index.ts +3 -4
  108. package/src/transaction/chaintrackers/DefaultChainTracker.ts +6 -0
  109. package/src/transaction/chaintrackers/WhatsOnChain.ts +70 -0
  110. package/src/transaction/chaintrackers/__tests/WhatsOnChainChainTracker.test.ts +135 -0
  111. package/src/transaction/chaintrackers/index.ts +3 -0
  112. package/src/transaction/http/DefaultHttpClient.ts +32 -0
  113. package/src/transaction/http/FetchHttpClient.ts +50 -0
  114. package/src/transaction/http/HttpClient.ts +45 -0
  115. package/src/transaction/http/NodejsHttpClient.ts +55 -0
  116. package/src/transaction/http/index.ts +6 -0
  117. package/dist/cjs/src/transaction/broadcasters/DefaultHttpClient.js.map +0 -1
  118. package/dist/cjs/src/transaction/broadcasters/HttpClient.js.map +0 -1
  119. package/dist/cjs/src/transaction/broadcasters/NodejsHttpClient.js.map +0 -1
  120. package/dist/esm/src/transaction/broadcasters/DefaultHttpClient.js.map +0 -1
  121. package/dist/esm/src/transaction/broadcasters/HttpClient.js.map +0 -1
  122. package/dist/esm/src/transaction/broadcasters/NodejsHttpClient.js +0 -36
  123. package/dist/esm/src/transaction/broadcasters/NodejsHttpClient.js.map +0 -1
  124. package/dist/types/src/transaction/broadcasters/DefaultHttpClient.d.ts +0 -6
  125. package/dist/types/src/transaction/broadcasters/DefaultHttpClient.d.ts.map +0 -1
  126. package/dist/types/src/transaction/broadcasters/HttpClient.d.ts +0 -33
  127. package/dist/types/src/transaction/broadcasters/HttpClient.d.ts.map +0 -1
  128. package/dist/types/src/transaction/broadcasters/NodejsHttpClient.d.ts.map +0 -1
  129. package/src/transaction/broadcasters/DefaultHttpClient.ts +0 -29
  130. package/src/transaction/broadcasters/HttpClient.ts +0 -34
  131. package/src/transaction/broadcasters/NodejsHttpClient.ts +0 -55
  132. /package/dist/cjs/src/transaction/{broadcasters → http}/HttpClient.js +0 -0
  133. /package/dist/esm/src/transaction/{broadcasters → http}/HttpClient.js +0 -0
@@ -10,6 +10,8 @@ import { Broadcaster, BroadcastResponse, BroadcastFailure } from './Broadcaster.
10
10
  import MerklePath from './MerklePath.js'
11
11
  import Spend from '../script/Spend.js'
12
12
  import ChainTracker from './ChainTracker.js'
13
+ import { defaultBroadcaster } from './broadcasters/DefaultBroadcaster.js'
14
+ import { defaultChainTracker } from './chaintrackers/DefaultChainTracker.js'
13
15
 
14
16
  /**
15
17
  * Represents a complete Bitcoin transaction. This class encapsulates all the details
@@ -382,7 +384,7 @@ export default class Transaction {
382
384
  * @param broadcaster The Broadcaster instance wwhere the transaction will be sent
383
385
  * @returns A BroadcastResponse or BroadcastFailure from the Broadcaster
384
386
  */
385
- async broadcast (broadcaster: Broadcaster): Promise<BroadcastResponse | BroadcastFailure> {
387
+ async broadcast (broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure> {
386
388
  return await broadcaster.broadcast(this)
387
389
  }
388
390
 
@@ -533,11 +535,11 @@ export default class Transaction {
533
535
  /**
534
536
  * Verifies the legitimacy of the Bitcoin transaction according to the rules of SPV by ensuring all the input transactions link back to valid block headers, the chain of spends for all inputs are valid, and the sum of inputs is not less than the sum of outputs.
535
537
  *
536
- * @param chainTracker - An instance of ChainTracker, a Bitcoin block header tracker. If the value is set to 'scripts only', headers will not be verified.
538
+ * @param chainTracker - An instance of ChainTracker, a Bitcoin block header tracker. If the value is set to 'scripts only', headers will not be verified. If not provided then the default chain tracker will be used.
537
539
  *
538
540
  * @returns Whether the transaction is valid according to the rules of SPV.
539
541
  */
540
- async verify (chainTracker: ChainTracker | 'scripts only'): Promise<boolean> {
542
+ async verify (chainTracker: ChainTracker | 'scripts only' = defaultChainTracker()): Promise<boolean> {
541
543
  // If the transaction has a valid merkle path, verification is complete.
542
544
  if (typeof this.merklePath === 'object' && chainTracker !== 'scripts only') {
543
545
  const proofValid = await this.merklePath.verify(
@@ -16,6 +16,7 @@ import validTransactions from './tx.valid.vectors'
16
16
  import bigTX from './bigtx.vectors'
17
17
 
18
18
  const BRC62Hex = '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
19
+ const MerkleRootFromBEEF = 'bb6f640cc4ee56bf38eb5a1969ac0c16caa2d3d202b22bf3735d10eec0ca6e00'
19
20
 
20
21
  describe('Transaction', () => {
21
22
  const txIn = {
@@ -354,6 +355,40 @@ describe('Transaction', () => {
354
355
  })
355
356
 
356
357
  describe('Broadcast', () => {
358
+ it('Broadcasts with the default Broadcaster instance', async () => {
359
+ const mockedFetch = jest.fn().mockResolvedValue({
360
+ ok: true,
361
+ status: 200,
362
+ statusText: 'OK',
363
+ headers: {
364
+ get(key: string) {
365
+ if (key === 'Content-Type') {
366
+ return 'application/json'
367
+ }
368
+ }
369
+ },
370
+ json: async () => ({
371
+ txid: 'mocked_txid',
372
+ txStatus: 'success',
373
+ extraInfo: 'received'
374
+ })
375
+ });
376
+
377
+ (global as any).window = {fetch: mockedFetch} as any
378
+
379
+ const tx = new Transaction()
380
+ const rv = await tx.broadcast()
381
+
382
+ expect(mockedFetch).toHaveBeenCalled()
383
+ const url = (mockedFetch as jest.Mock).mock.calls[0][0] as string
384
+ expect(url).toEqual('https://arc.taal.com/v1/tx')
385
+ expect(rv).toEqual({
386
+ status: 'success',
387
+ txid: 'mocked_txid',
388
+ message: 'success received'
389
+ })
390
+ })
391
+
357
392
  it('Broadcasts with the provided Broadcaster instance', async () => {
358
393
  const mockBroadcast = jest.fn(() => 'MOCK_RV')
359
394
  const tx = new Transaction()
@@ -391,6 +426,33 @@ describe('Transaction', () => {
391
426
  const verified = await tx.verify(alwaysYesChainTracker)
392
427
  expect(verified).toBe(true)
393
428
  })
429
+
430
+ it('Verifies the transaction from the BEEF spec with a default chain tracker', async () => {
431
+ const mockFetch = jest.fn().mockResolvedValue({
432
+ ok: true,
433
+ status: 200,
434
+ statusText: 'OK',
435
+ headers: {
436
+ get(key: string) {
437
+ if (key === 'Content-Type') {
438
+ return 'application/json'
439
+ }
440
+ }
441
+ },
442
+ json: async () => ({
443
+ merkleroot: MerkleRootFromBEEF,
444
+ })
445
+ });
446
+ (global as any).window = {fetch: mockFetch}
447
+
448
+
449
+ const tx = Transaction.fromHexBEEF(BRC62Hex)
450
+
451
+ const verified = await tx.verify()
452
+
453
+ expect(mockFetch).toHaveBeenCalled()
454
+ expect(verified).toBe(true)
455
+ })
394
456
  })
395
457
 
396
458
  describe('vectors: a 1mb transaction', () => {
@@ -1,38 +1,69 @@
1
- import { BroadcastResponse, BroadcastFailure, Broadcaster } from '../Broadcaster.js'
1
+ import {BroadcastResponse, BroadcastFailure, Broadcaster} from '../Broadcaster.js'
2
2
  import Transaction from '../Transaction.js'
3
- import {HttpClient} from "./HttpClient.js";
4
- import defaultHttpClient from "./DefaultHttpClient.js";
3
+ import {HttpClient, HttpClientRequestOptions} from "../http/HttpClient.js";
4
+ import {defaultHttpClient} from "../http/DefaultHttpClient.js";
5
+ import Random from "../../primitives/Random.js";
6
+ import {toHex} from "../../primitives/utils.js";
7
+
8
+ /** Configuration options for the ARC broadcaster. */
9
+ export interface ArcConfig {
10
+ /** Authentication token for the ARC API */
11
+ apiKey?: string
12
+ /** Deployment id used annotating api calls in XDeployment-ID header - this value will be randomly generated if not set */
13
+ deploymentId?: string
14
+ /** The HTTP client used to make requests to the ARC API. */
15
+ httpClient?: HttpClient
16
+ }
17
+
18
+
19
+ function defaultDeploymentId() {
20
+ return `ts-sdk-${toHex(Random(16))}`;
21
+ }
5
22
 
6
23
  /**
7
24
  * Represents an ARC transaction broadcaster.
8
25
  */
9
26
  export default class ARC implements Broadcaster {
10
- URL: string
11
- apiKey: string
12
- private httpClient: HttpClient;
27
+ readonly URL: string
28
+ readonly apiKey: string | undefined
29
+ readonly deploymentId: string
30
+ private readonly httpClient: HttpClient;
13
31
 
32
+ /**
33
+ * Constructs an instance of the ARC broadcaster.
34
+ *
35
+ * @param {string} URL - The URL endpoint for the ARC API.
36
+ * @param {ArcConfig} config - Configuration options for the ARC broadcaster.
37
+ */
38
+ constructor(URL: string, config?: ArcConfig)
14
39
  /**
15
40
  * Constructs an instance of the ARC broadcaster.
16
41
  *
17
42
  * @param {string} URL - The URL endpoint for the ARC API.
18
43
  * @param {string} apiKey - The API key used for authorization with the ARC API.
19
- * @param {HttpClient} httpClient - The HTTP client used to make requests to the ARC API.
20
44
  */
21
- constructor (URL: string, apiKey: string, httpClient: HttpClient = defaultHttpClient()) {
45
+ constructor(URL: string, apiKey?: string)
46
+
47
+ constructor(URL: string, config?: string | ArcConfig) {
22
48
  this.URL = URL
23
- this.apiKey = apiKey
24
- this.httpClient = httpClient
49
+ if (typeof config === 'string') {
50
+ this.apiKey = config
51
+ this.httpClient = defaultHttpClient()
52
+ } else {
53
+ const {apiKey, deploymentId, httpClient} = config ?? {} as ArcConfig
54
+ this.httpClient = httpClient ?? defaultHttpClient()
55
+ this.deploymentId = deploymentId ?? defaultDeploymentId()
56
+ this.apiKey = apiKey
57
+ }
25
58
  }
26
59
 
27
60
  /**
28
61
  * Broadcasts a transaction via ARC.
29
- * This method will attempt to use `window.fetch` if available (in browser environments).
30
- * If running in a Node.js environment, it falls back to using the Node.js `https` module.
31
62
  *
32
63
  * @param {Transaction} tx - The transaction to be broadcasted.
33
64
  * @returns {Promise<BroadcastResponse | BroadcastFailure>} A promise that resolves to either a success or failure response.
34
65
  */
35
- async broadcast (tx: Transaction): Promise<BroadcastResponse | BroadcastFailure> {
66
+ async broadcast(tx: Transaction): Promise<BroadcastResponse | BroadcastFailure> {
36
67
  let rawTx
37
68
  try {
38
69
  rawTx = tx.toHexEF()
@@ -42,30 +73,28 @@ export default class ARC implements Broadcaster {
42
73
  } else {
43
74
  throw error
44
75
  }
45
- }
46
- const requestOptions = {
76
+ }
77
+
78
+ const requestOptions: HttpClientRequestOptions = {
47
79
  method: 'POST',
48
- headers: {
49
- 'Content-Type': 'application/json',
50
- Authorization: `Bearer ${this.apiKey}`
51
- },
52
- body: JSON.stringify({ rawTx })
80
+ headers: this.requestHeaders(),
81
+ data: {rawTx}
53
82
  }
54
83
 
55
84
  try {
56
- const response = await this.httpClient.fetch(`${this.URL}/v1/tx`, requestOptions)
57
- const data = await response.json()
58
- if (data.txid as boolean || response.ok as boolean || response.statusCode === 200) {
85
+ const response = await this.httpClient.request<ArcResponse>(`${this.URL}/v1/tx`, requestOptions)
86
+ if (response.ok) {
87
+ const {txid, extraInfo, txStatus} = response.data
59
88
  return {
60
89
  status: 'success',
61
- txid: data.txid,
62
- message: data?.txStatus + ' ' + data?.extraInfo
90
+ txid: txid,
91
+ message: `${txStatus} ${extraInfo}`
63
92
  }
64
93
  } else {
65
94
  return {
66
95
  status: 'error',
67
- code: data.status as boolean ? data.status : 'ERR_UNKNOWN',
68
- description: data.detail as boolean ? data.detail : 'Unknown error'
96
+ code: response.status.toString() ?? 'ERR_UNKNOWN',
97
+ description: response.data?.detail ?? 'Unknown error'
69
98
  }
70
99
  }
71
100
  } catch (error) {
@@ -78,4 +107,23 @@ export default class ARC implements Broadcaster {
78
107
  }
79
108
  }
80
109
  }
110
+
111
+ private requestHeaders() {
112
+ const headers: Record<string, string> = {
113
+ 'Content-Type': 'application/json',
114
+ 'XDeployment-ID': this.deploymentId,
115
+ }
116
+
117
+ if (this.apiKey) {
118
+ headers['Authorization'] = `Bearer ${this.apiKey}`
119
+ }
120
+
121
+ return headers
122
+ }
123
+ }
124
+
125
+ interface ArcResponse {
126
+ txid: string
127
+ extraInfo: string
128
+ txStatus: string
81
129
  }
@@ -0,0 +1,6 @@
1
+ import {Broadcaster} from "../Broadcaster.js";
2
+ import ARC from "./ARC.js";
3
+
4
+ export function defaultBroadcaster(): Broadcaster {
5
+ return new ARC('https://arc.taal.com')
6
+ }
@@ -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
  })