@bsv/sdk 1.4.17 → 1.4.19

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 (59) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/kvstore/LocalKVStore.js +152 -141
  3. package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -1
  4. package/dist/cjs/src/storage/StorageUploader.js +122 -14
  5. package/dist/cjs/src/storage/StorageUploader.js.map +1 -1
  6. package/dist/cjs/src/storage/__test/StorageUploader.test.js +85 -14
  7. package/dist/cjs/src/storage/__test/StorageUploader.test.js.map +1 -1
  8. package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js +29 -0
  9. package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
  10. package/dist/cjs/src/wallet/WalletError.js +4 -3
  11. package/dist/cjs/src/wallet/WalletError.js.map +1 -1
  12. package/dist/cjs/src/wallet/index.js +4 -1
  13. package/dist/cjs/src/wallet/index.js.map +1 -1
  14. package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js +13 -6
  15. package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/src/kvstore/LocalKVStore.js +151 -141
  18. package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -1
  19. package/dist/esm/src/storage/StorageUploader.js +119 -14
  20. package/dist/esm/src/storage/StorageUploader.js.map +1 -1
  21. package/dist/esm/src/storage/__test/StorageUploader.test.js +85 -14
  22. package/dist/esm/src/storage/__test/StorageUploader.test.js.map +1 -1
  23. package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js +31 -0
  24. package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
  25. package/dist/esm/src/wallet/WalletError.js +3 -2
  26. package/dist/esm/src/wallet/WalletError.js.map +1 -1
  27. package/dist/esm/src/wallet/index.js +2 -0
  28. package/dist/esm/src/wallet/index.js.map +1 -1
  29. package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js +13 -6
  30. package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/src/kvstore/LocalKVStore.d.ts +10 -4
  33. package/dist/types/src/kvstore/LocalKVStore.d.ts.map +1 -1
  34. package/dist/types/src/storage/StorageUploader.d.ts +77 -14
  35. package/dist/types/src/storage/StorageUploader.d.ts.map +1 -1
  36. package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts +23 -0
  37. package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts.map +1 -0
  38. package/dist/types/src/wallet/Wallet.interfaces.d.ts +22 -0
  39. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
  40. package/dist/types/src/wallet/WalletError.d.ts +4 -3
  41. package/dist/types/src/wallet/WalletError.d.ts.map +1 -1
  42. package/dist/types/src/wallet/index.d.ts +1 -0
  43. package/dist/types/src/wallet/index.d.ts.map +1 -1
  44. package/dist/types/src/wallet/substrates/HTTPWalletJSON.d.ts.map +1 -1
  45. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  46. package/dist/umd/bundle.js +1 -1
  47. package/docs/kvstore.md +9 -8
  48. package/docs/storage.md +117 -7
  49. package/docs/wallet.md +146 -38
  50. package/package.json +1 -1
  51. package/src/kvstore/LocalKVStore.ts +156 -151
  52. package/src/kvstore/__tests/LocalKVStore.test.ts +104 -193
  53. package/src/storage/StorageUploader.ts +156 -14
  54. package/src/storage/__test/StorageUploader.test.ts +134 -15
  55. package/src/wallet/WERR_REVIEW_ACTIONS.ts +30 -0
  56. package/src/wallet/Wallet.interfaces.ts +24 -0
  57. package/src/wallet/WalletError.ts +4 -2
  58. package/src/wallet/index.ts +2 -0
  59. package/src/wallet/substrates/HTTPWalletJSON.ts +12 -6
@@ -2,7 +2,9 @@ import { StorageUploader } from '../StorageUploader.js'
2
2
  import * as StorageUtils from '../StorageUtils.js'
3
3
  import WalletClient from '../../wallet/WalletClient.js'
4
4
 
5
- // A helper for converting a string to a number[] of UTF-8 bytes
5
+ /**
6
+ * A helper for converting a string to a number[] of UTF-8 bytes
7
+ */
6
8
  function stringToUtf8Array(str: string): number[] {
7
9
  return Array.from(new TextEncoder().encode(str))
8
10
  }
@@ -10,16 +12,24 @@ function stringToUtf8Array(str: string): number[] {
10
12
  describe('StorageUploader Tests', () => {
11
13
  let uploader: StorageUploader
12
14
  let walletClient: WalletClient
15
+
16
+ // We'll have TWO spies:
17
+ let authFetchSpy: jest.SpiedFunction<typeof global.fetch>
13
18
  let globalFetchSpy: jest.SpiedFunction<typeof global.fetch>
14
19
 
15
20
  beforeEach(() => {
16
21
  walletClient = new WalletClient('json-api', 'non-admin.com')
17
-
18
22
  uploader = new StorageUploader({
19
23
  storageURL: 'https://example.test.system',
20
24
  wallet: walletClient
21
25
  })
22
26
 
27
+ // 1) Spy on the "authFetch.fetch" calls for /find, /list, /renew
28
+ authFetchSpy = jest
29
+ .spyOn(uploader['authFetch'], 'fetch')
30
+ .mockResolvedValue(new Response(null, { status: 200 }))
31
+
32
+ // 2) Spy on the global "fetch" calls for file upload (uploadFile)
23
33
  globalFetchSpy = jest
24
34
  .spyOn(global, 'fetch')
25
35
  .mockResolvedValue(new Response(null, { status: 200 }))
@@ -34,47 +44,156 @@ describe('StorageUploader Tests', () => {
34
44
 
35
45
  // Mock out getUploadInfo so we can control the returned upload/public URLs
36
46
  jest.spyOn(uploader as any, 'getUploadInfo').mockResolvedValue({
37
- uploadURL: 'https://example-upload.com/put',
47
+ uploadURL: 'https://example-upload.com/put'
38
48
  })
39
49
 
40
50
  const result = await uploader.publishFile({
41
- file: {
42
- data,
43
- type: 'text/plain'
44
- },
51
+ file: { data, type: 'text/plain' },
45
52
  retentionPeriod: 7
46
53
  })
47
54
 
48
- // We expect exactly one PUT request
55
+ // This direct upload uses global.fetch, not authFetch
49
56
  expect(globalFetchSpy).toHaveBeenCalledTimes(1)
57
+
50
58
  // Check the result
51
59
  expect(StorageUtils.isValidURL(result.uhrpURL)).toBe(true)
52
60
  expect(result.published).toBe(true)
53
61
 
54
62
  const url = StorageUtils.getHashFromURL(result.uhrpURL)
55
- const firstFour = url.slice(0, 4).map(b => b.toString(16).padStart(2, '0')).join('')
63
+ const firstFour = url.slice(0, 4)
64
+ .map(b => b.toString(16).padStart(2, '0'))
65
+ .join('')
56
66
  expect(firstFour).toHaveLength(8)
57
67
  })
58
68
 
59
69
  it('should throw if the upload fails with HTTP 500', async () => {
60
- // Force the fetch to fail
70
+ // Force the direct upload (global fetch) to fail
61
71
  globalFetchSpy.mockResolvedValueOnce(new Response(null, { status: 500 }))
62
72
 
63
73
  // Also mock getUploadInfo
64
74
  jest.spyOn(uploader as any, 'getUploadInfo').mockResolvedValue({
65
- uploadURL: 'https://example-upload.com/put',
75
+ uploadURL: 'https://example-upload.com/put'
66
76
  })
67
77
 
68
78
  const failingData = stringToUtf8Array('failing data')
69
79
 
70
80
  await expect(
71
81
  uploader.publishFile({
72
- file: {
73
- data: failingData,
74
- type: 'text/plain'
75
- },
82
+ file: { data: failingData, type: 'text/plain' },
76
83
  retentionPeriod: 30
77
84
  })
78
85
  ).rejects.toThrow('File upload failed: HTTP 500')
79
86
  })
87
+
88
+ it('should find a file and return metadata', async () => {
89
+ // This route goes through authFetch, not global fetch
90
+ authFetchSpy.mockResolvedValueOnce(
91
+ new Response(
92
+ JSON.stringify({
93
+ status: 'success',
94
+ data: {
95
+ name: 'cdn/abc123',
96
+ size: '1024',
97
+ mimeType: 'text/plain',
98
+ expiryTime: 123456
99
+ }
100
+ }),
101
+ { status: 200 }
102
+ )
103
+ )
104
+
105
+ const fileData = await uploader.findFile('uhrp://some-hash')
106
+ expect(authFetchSpy).toHaveBeenCalledTimes(1)
107
+ expect(fileData.name).toBe('cdn/abc123')
108
+ expect(fileData.size).toBe('1024')
109
+ expect(fileData.mimeType).toBe('text/plain')
110
+ expect(fileData.expiryTime).toBe(123456)
111
+ })
112
+
113
+ it('should throw an error if findFile returns an error status', async () => {
114
+ authFetchSpy.mockResolvedValueOnce(
115
+ new Response(
116
+ JSON.stringify({ status: 'error', code: 'ERR_NOT_FOUND', description: 'File not found' }),
117
+ { status: 200 }
118
+ )
119
+ )
120
+
121
+ await expect(uploader.findFile('uhrp://unknown-hash'))
122
+ .rejects
123
+ .toThrow('findFile returned an error: ERR_NOT_FOUND - File not found')
124
+ })
125
+
126
+ it('should list user uploads successfully', async () => {
127
+ // /list uses authFetch
128
+ const mockUploads = [
129
+ { uhrpUrl: 'uhrp://hash1', expiryTime: 111111 },
130
+ { uhrpUrl: 'uhrp://hash2', expiryTime: 222222 }
131
+ ]
132
+ authFetchSpy.mockResolvedValueOnce(
133
+ new Response(
134
+ JSON.stringify({ status: 'success', uploads: mockUploads }),
135
+ { status: 200 }
136
+ )
137
+ )
138
+
139
+ const result = await uploader.listUploads()
140
+ expect(authFetchSpy).toHaveBeenCalledTimes(1)
141
+ expect(result).toEqual(mockUploads)
142
+ })
143
+
144
+ it('should throw an error if listUploads returns an error', async () => {
145
+ authFetchSpy.mockResolvedValueOnce(
146
+ new Response(
147
+ JSON.stringify({ status: 'error', code: 'ERR_INTERNAL', description: 'Something broke' }),
148
+ { status: 200 }
149
+ )
150
+ )
151
+
152
+ await expect(uploader.listUploads()).rejects.toThrow(
153
+ 'listUploads returned an error: ERR_INTERNAL - Something broke'
154
+ )
155
+ })
156
+
157
+ it('should renew a file and return the new expiry info', async () => {
158
+ // /renew uses authFetch
159
+ authFetchSpy.mockResolvedValueOnce(
160
+ new Response(
161
+ JSON.stringify({
162
+ status: 'success',
163
+ prevExpiryTime: 123,
164
+ newExpiryTime: 456,
165
+ amount: 99
166
+ }),
167
+ { status: 200 }
168
+ )
169
+ )
170
+
171
+ const renewal = await uploader.renewFile('uhrp://some-hash', 30)
172
+ expect(authFetchSpy).toHaveBeenCalledTimes(1)
173
+ expect(renewal.status).toBe('success')
174
+ expect(renewal.prevExpiryTime).toBe(123)
175
+ expect(renewal.newExpiryTime).toBe(456)
176
+ expect(renewal.amount).toBe(99)
177
+ })
178
+
179
+ it('should throw an error if renewFile returns error status JSON', async () => {
180
+ authFetchSpy.mockResolvedValueOnce(
181
+ new Response(
182
+ JSON.stringify({ status: 'error', code: 'ERR_CANT_RENEW', description: 'Failed to renew' }),
183
+ { status: 200 }
184
+ )
185
+ )
186
+
187
+ await expect(uploader.renewFile('uhrp://some-other-hash', 15))
188
+ .rejects
189
+ .toThrow('renewFile returned an error: ERR_CANT_RENEW - Failed to renew')
190
+ })
191
+
192
+ it('should throw if renewFile request fails with non-200 status', async () => {
193
+ authFetchSpy.mockResolvedValueOnce(new Response(null, { status: 404 }))
194
+
195
+ await expect(uploader.renewFile('uhrp://ghost', 10))
196
+ .rejects
197
+ .toThrow('renewFile request failed: HTTP 404')
198
+ })
80
199
  })
@@ -0,0 +1,30 @@
1
+ import { AtomicBEEF, OutpointString, ReviewActionResult, SendWithResult, TXIDHexString } from './Wallet.interfaces.js'
2
+
3
+ /**
4
+ * When a `createAction` or `signAction` is completed in undelayed mode (`acceptDelayedBroadcast`: false),
5
+ * any unsucccessful result will return the results by way of this exception to ensure attention is
6
+ * paid to processing errors.
7
+ */
8
+ export class WERR_REVIEW_ACTIONS extends Error {
9
+ code: number
10
+ isError: boolean = true
11
+
12
+ /**
13
+ * All parameters correspond to their comparable `createAction` or `signSction` results
14
+ * with the exception of `reviewActionResults`;
15
+ * which contains more details, particularly for double spend results.
16
+ */
17
+ constructor (
18
+ public reviewActionResults: ReviewActionResult[],
19
+ public sendWithResults: SendWithResult[],
20
+ public txid?: TXIDHexString,
21
+ public tx?: AtomicBEEF,
22
+ public noSendChange?: OutpointString[]
23
+ ) {
24
+ super('Undelayed createAction or signAction results require review.')
25
+ this.code = 5
26
+ this.name = this.constructor.name
27
+ }
28
+ }
29
+
30
+ export default WERR_REVIEW_ACTIONS
@@ -303,6 +303,30 @@ export interface SendWithResult {
303
303
  status: SendWithResultStatus
304
304
  }
305
305
 
306
+ /**
307
+ * Indicates status of a new Action following a `createAction` or `signAction` in immediate mode:
308
+ * When `acceptDelayedBroadcast` is falses.
309
+ *
310
+ * 'success': The action has been broadcast and accepted by the bitcoin processing network.
311
+ * 'doulbeSpend': The action has been confirmed to double spend one or more inputs, and by the "first-seen-rule" is the loosing transaction.
312
+ * 'invalidTx': The action was rejected by the processing network as an invalid bitcoin transaction.
313
+ * 'serviceError': The broadcast services are currently unable to reach the bitcoin network. The action is now queued for delayed retries.
314
+ */
315
+ export type ReviewActionResultStatus = 'success' | 'doubleSpend' | 'serviceError' | 'invalidTx'
316
+
317
+ export interface ReviewActionResult {
318
+ txid: TXIDHexString
319
+ status: ReviewActionResultStatus
320
+ /**
321
+ * Any competing txids reported for this txid, valid when status is 'doubleSpend'.
322
+ */
323
+ competingTxs?: string[]
324
+ /**
325
+ * Merged beef of competingTxs, valid when status is 'doubleSpend'.
326
+ */
327
+ competingBeef?: number[]
328
+ }
329
+
306
330
  export interface SignableTransaction {
307
331
  tx: AtomicBEEF
308
332
  reference: Base64String
@@ -16,12 +16,14 @@ export class WalletError extends Error {
16
16
  }
17
17
 
18
18
  // NOTE: Enum values must not exceed the UInt8 range (0–255)
19
- enum walletErrors {
19
+ export enum walletErrors {
20
20
  unknownError = 1,
21
21
  unsupportedAction = 2,
22
22
  invalidHmac = 3,
23
23
  invalidSignature = 4,
24
+ reviewActions = 5,
24
25
  }
25
26
 
26
- export default walletErrors
27
27
  export type WalletErrorCode = keyof typeof walletErrors
28
+
29
+ export default WalletError
@@ -3,6 +3,8 @@ export * from './KeyDeriver.js'
3
3
  export { default as CachedKeyDeriver } from './CachedKeyDeriver.js'
4
4
  export { default as ProtoWallet } from './ProtoWallet.js'
5
5
  export { default as WalletClient } from './WalletClient.js'
6
+ // Is this an error? should it be 'walletErrors', the enum not the class?
6
7
  export { default as WalletErrors } from './WalletError.js'
8
+ export { default as WERR_REVIEW_ACTIONS } from './WERR_REVIEW_ACTIONS.js'
7
9
  export * from './WalletError.js'
8
10
  export * from './substrates/index.js'
@@ -33,8 +33,9 @@ import {
33
33
  SecurityLevel,
34
34
  SignActionArgs,
35
35
  SignActionResult,
36
- VersionString7To30Bytes
36
+ VersionString7To30Bytes,
37
37
  } from '../Wallet.interfaces.js'
38
+ import { WERR_REVIEW_ACTIONS } from '../WERR_REVIEW_ACTIONS.js'
38
39
 
39
40
  export default class HTTPWalletJSON implements WalletInterface {
40
41
  baseUrl: string
@@ -68,12 +69,17 @@ export default class HTTPWalletJSON implements WalletInterface {
68
69
 
69
70
  // Check the HTTP status on the original response
70
71
  if (!res.ok) {
71
- const err = {
72
- call,
73
- args,
74
- message: data.message ?? `HTTP Client error ${res.status}`
72
+ if (res.status === 400 && data.isError && data.code === 5) {
73
+ const err = new WERR_REVIEW_ACTIONS(data.reviewActionResults, data.sendWithResults, data.txid, data.tx, data.noSendChange)
74
+ throw err
75
+ } else {
76
+ const err = {
77
+ call,
78
+ args,
79
+ message: data.message ?? `HTTP Client error ${res.status}`
80
+ }
81
+ throw new Error(JSON.stringify(err))
75
82
  }
76
- throw new Error(JSON.stringify(err))
77
83
  }
78
84
  return data
79
85
  }