@bsv/sdk 1.4.18 → 1.4.20

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/docs/kvstore.md CHANGED
@@ -14,7 +14,7 @@ Allows setting, getting, and removing key-value pairs, with optional encryption.
14
14
 
15
15
  ```ts
16
16
  export default class LocalKVStore {
17
- constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore-default", encrypt = true, originator?: string)
17
+ constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string)
18
18
  async get(key: string, defaultValue: string | undefined = undefined): Promise<string | undefined>
19
19
  async set(key: string, value: string): Promise<OutpointString>
20
20
  async remove(key: string): Promise<string[]>
@@ -28,7 +28,7 @@ See also: [OutpointString](./wallet.md#type-outpointstring), [WalletClient](./wa
28
28
  Creates an instance of the localKVStore.
29
29
 
30
30
  ```ts
31
- constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore-default", encrypt = true, originator?: string)
31
+ constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string)
32
32
  ```
33
33
  See also: [WalletClient](./wallet.md#class-walletclient), [WalletInterface](./wallet.md#interface-walletinterface), [encrypt](./messages.md#variable-encrypt)
34
34
 
@@ -37,7 +37,7 @@ Argument Details
37
37
  + **wallet**
38
38
  + The wallet interface to use. Defaults to a new WalletClient instance.
39
39
  + **context**
40
- + The context (basket) for namespacing keys. Defaults to 'kvstore-default'.
40
+ + The context (basket) for namespacing keys. Defaults to 'kvstore default'.
41
41
  + **encrypt**
42
42
  + Whether to encrypt values. Defaults to true.
43
43
  + **originator**
package/docs/storage.md CHANGED
@@ -8,6 +8,8 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
8
8
  | --- |
9
9
  | [DownloadResult](#interface-downloadresult) |
10
10
  | [DownloaderConfig](#interface-downloaderconfig) |
11
+ | [FindFileData](#interface-findfiledata) |
12
+ | [RenewFileResult](#interface-renewfileresult) |
11
13
  | [UploadFileResult](#interface-uploadfileresult) |
12
14
  | [UploadableFile](#interface-uploadablefile) |
13
15
  | [UploaderConfig](#interface-uploaderconfig) |
@@ -38,6 +40,34 @@ export interface DownloaderConfig {
38
40
 
39
41
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
40
42
 
43
+ ---
44
+ ### Interface: FindFileData
45
+
46
+ ```ts
47
+ export interface FindFileData {
48
+ name: string;
49
+ size: string;
50
+ mimeType: string;
51
+ expiryTime: number;
52
+ }
53
+ ```
54
+
55
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
56
+
57
+ ---
58
+ ### Interface: RenewFileResult
59
+
60
+ ```ts
61
+ export interface RenewFileResult {
62
+ status: string;
63
+ prevExpiryTime?: number;
64
+ newExpiryTime?: number;
65
+ amount?: number;
66
+ }
67
+ ```
68
+
69
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
70
+
41
71
  ---
42
72
  ### Interface: UploadFileResult
43
73
 
@@ -107,6 +137,12 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
107
137
  ---
108
138
  ### Class: StorageUploader
109
139
 
140
+ The StorageUploader class provides client-side methods for:
141
+ - Uploading files with a specified retention period
142
+ - Finding file metadata by UHRP URL
143
+ - Listing all user uploads
144
+ - Renewing an existing advertisement's expiry time
145
+
110
146
  ```ts
111
147
  export class StorageUploader {
112
148
  constructor(config: UploaderConfig)
@@ -114,10 +150,65 @@ export class StorageUploader {
114
150
  file: UploadableFile;
115
151
  retentionPeriod: number;
116
152
  }): Promise<UploadFileResult>
153
+ public async findFile(uhrpUrl: string): Promise<FindFileData>
154
+ public async listUploads(): Promise<any>
155
+ public async renewFile(uhrpUrl: string, additionalMinutes: number): Promise<RenewFileResult>
117
156
  }
118
157
  ```
119
158
 
120
- See also: [UploadFileResult](./storage.md#interface-uploadfileresult), [UploadableFile](./storage.md#interface-uploadablefile), [UploaderConfig](./storage.md#interface-uploaderconfig)
159
+ See also: [FindFileData](./storage.md#interface-findfiledata), [RenewFileResult](./storage.md#interface-renewfileresult), [UploadFileResult](./storage.md#interface-uploadfileresult), [UploadableFile](./storage.md#interface-uploadablefile), [UploaderConfig](./storage.md#interface-uploaderconfig)
160
+
161
+ #### Constructor
162
+
163
+ Creates a new StorageUploader instance.
164
+
165
+ ```ts
166
+ constructor(config: UploaderConfig)
167
+ ```
168
+ See also: [UploaderConfig](./storage.md#interface-uploaderconfig)
169
+
170
+ Argument Details
171
+
172
+ + **config**
173
+ + An object containing the storage server's URL and a wallet interface
174
+
175
+ #### Method findFile
176
+
177
+ Retrieves metadata for a file matching the given UHRP URL from the `/find` route.
178
+
179
+ ```ts
180
+ public async findFile(uhrpUrl: string): Promise<FindFileData>
181
+ ```
182
+ See also: [FindFileData](./storage.md#interface-findfiledata)
183
+
184
+ Returns
185
+
186
+ An object with file name, size, MIME type, and expiry time
187
+
188
+ Argument Details
189
+
190
+ + **uhrpUrl**
191
+ + The UHRP URL, e.g. "uhrp://abcd..."
192
+
193
+ Throws
194
+
195
+ If the server or the route returns an error
196
+
197
+ #### Method listUploads
198
+
199
+ Lists all advertisements belonging to the user from the `/list` route.
200
+
201
+ ```ts
202
+ public async listUploads(): Promise<any>
203
+ ```
204
+
205
+ Returns
206
+
207
+ The array of uploads returned by the server
208
+
209
+ Throws
210
+
211
+ If the server or the route returns an error
121
212
 
122
213
  #### Method publishFile
123
214
 
@@ -138,18 +229,37 @@ See also: [UploadFileResult](./storage.md#interface-uploadfileresult), [Uploadab
138
229
 
139
230
  Returns
140
231
 
141
- An object indicating whether the file was published successfully and the resulting UHRP URL.
232
+ An object with the file's UHRP URL
233
+
234
+ Throws
235
+
236
+ If the server or upload step returns a non-OK response
237
+
238
+ #### Method renewFile
239
+
240
+ Renews the hosting time for an existing file advertisement identified by uhrpUrl.
241
+ Calls the `/renew` route to add `additionalMinutes` to the GCS customTime
242
+ and re-mint the advertisement token on-chain.
243
+
244
+ ```ts
245
+ public async renewFile(uhrpUrl: string, additionalMinutes: number): Promise<RenewFileResult>
246
+ ```
247
+ See also: [RenewFileResult](./storage.md#interface-renewfileresult)
248
+
249
+ Returns
250
+
251
+ An object with the new and previous expiry times, plus any cost
142
252
 
143
253
  Argument Details
144
254
 
145
- + **params.file**
146
- + An object describing the file’s data (number[] array of bytes) and mime type.
147
- + **params.retentionPeriod**
148
- + Number of minutes to keep the file hosted.
255
+ + **uhrpUrl**
256
+ + The UHRP URL of the file (e.g., "uhrp://abcd1234...")
257
+ + **additionalMinutes**
258
+ + The number of minutes to extend
149
259
 
150
260
  Throws
151
261
 
152
- If either the upload info request or the subsequent file upload request fails (non-OK HTTP status).
262
+ If the request fails or the server returns an error
153
263
 
154
264
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
155
265
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.4.18",
3
+ "version": "1.4.20",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -17,15 +17,48 @@ export interface UploadFileResult {
17
17
  uhrpURL: string
18
18
  }
19
19
 
20
+ export interface FindFileData {
21
+ name: string
22
+ size: string
23
+ mimeType: string
24
+ expiryTime: number
25
+ }
26
+
27
+ export interface RenewFileResult {
28
+ status: string
29
+ prevExpiryTime?: number
30
+ newExpiryTime?: number
31
+ amount?: number
32
+ }
33
+
34
+ /**
35
+ * The StorageUploader class provides client-side methods for:
36
+ * - Uploading files with a specified retention period
37
+ * - Finding file metadata by UHRP URL
38
+ * - Listing all user uploads
39
+ * - Renewing an existing advertisement's expiry time
40
+ */
20
41
  export class StorageUploader {
21
42
  private readonly authFetch: AuthFetch
22
43
  private readonly baseURL: string
23
44
 
45
+ /**
46
+ * Creates a new StorageUploader instance.
47
+ * @param {UploaderConfig} config - An object containing the storage server's URL and a wallet interface
48
+ */
24
49
  constructor (config: UploaderConfig) {
25
50
  this.baseURL = config.storageURL
26
51
  this.authFetch = new AuthFetch(config.wallet)
27
52
  }
28
53
 
54
+ /**
55
+ * Requests information from the server to upload a file (including presigned URL and headers).
56
+ * @private
57
+ * @param {number} fileSize - The size of the file, in bytes
58
+ * @param {number} retentionPeriod - The desired hosting time, in minutes
59
+ * @returns {Promise<{ uploadURL: string; requiredHeaders: Record<string, string>; amount?: number }>}
60
+ * @throws {Error} If the server returns a non-OK response or an error status
61
+ */
29
62
  private async getUploadInfo (
30
63
  fileSize: number,
31
64
  retentionPeriod: number
@@ -61,6 +94,15 @@ export class StorageUploader {
61
94
  }
62
95
  }
63
96
 
97
+ /**
98
+ * Performs the actual file upload (HTTP PUT) to the presigned URL returned by the server.
99
+ * @private
100
+ * @param {string} uploadURL - The presigned URL where the file is to be uploaded
101
+ * @param {UploadableFile} file - The file to upload, including its raw data and MIME type
102
+ * @param {Record<string, string>} requiredHeaders - Additional headers required by the server (e.g. content-length)
103
+ * @returns {Promise<UploadFileResult>} An object indicating whether publishing was successful and the resulting UHRP URL
104
+ * @throws {Error} If the server returns a non-OK response
105
+ */
64
106
  private async uploadFile (
65
107
  uploadURL: string,
66
108
  file: UploadableFile,
@@ -88,20 +130,19 @@ export class StorageUploader {
88
130
  }
89
131
 
90
132
  /**
91
- * Publishes a file to the storage server with the specified retention period.
92
- *
93
- * This will:
94
- * 1. Request an upload URL from the server.
95
- * 2. Perform an HTTP PUT to upload the file’s raw bytes.
96
- * 3. Return a UHRP URL referencing the file once published.
97
- *
98
- * @param params.file - An object describing the file’s data (number[] array of bytes) and mime type.
99
- * @param params.retentionPeriod - Number of minutes to keep the file hosted.
100
- *
101
- * @returns An object indicating whether the file was published successfully and the resulting UHRP URL.
102
- *
103
- * @throws If either the upload info request or the subsequent file upload request fails (non-OK HTTP status).
104
- */
133
+ * Publishes a file to the storage server with the specified retention period.
134
+ *
135
+ * This will:
136
+ * 1. Request an upload URL from the server.
137
+ * 2. Perform an HTTP PUT to upload the file’s raw bytes.
138
+ * 3. Return a UHRP URL referencing the file once published.
139
+ *
140
+ * @param {Object} params
141
+ * @param {UploadableFile} params.file - The file data + type
142
+ * @param {number} params.retentionPeriod - Number of minutes to host the file
143
+ * @returns {Promise<UploadFileResult>} An object with the file's UHRP URL
144
+ * @throws {Error} If the server or upload step returns a non-OK response
145
+ */
105
146
  public async publishFile (params: {
106
147
  file: UploadableFile
107
148
  retentionPeriod: number
@@ -112,4 +153,105 @@ export class StorageUploader {
112
153
  const { uploadURL, requiredHeaders } = await this.getUploadInfo(fileSize, retentionPeriod)
113
154
  return await this.uploadFile(uploadURL, file, requiredHeaders)
114
155
  }
156
+
157
+ /**
158
+ * Retrieves metadata for a file matching the given UHRP URL from the `/find` route.
159
+ * @param {string} uhrpUrl - The UHRP URL, e.g. "uhrp://abcd..."
160
+ * @returns {Promise<FindFileData>} An object with file name, size, MIME type, and expiry time
161
+ * @throws {Error} If the server or the route returns an error
162
+ */
163
+ public async findFile (uhrpUrl: string): Promise<FindFileData> {
164
+ const url = new URL(`${this.baseURL}/find`)
165
+ url.searchParams.set('uhrpUrl', uhrpUrl)
166
+
167
+ const response = await this.authFetch.fetch(url.toString(), {
168
+ method: 'GET'
169
+ })
170
+ if (!response.ok) {
171
+ throw new Error(`findFile request failed: HTTP ${response.status}`)
172
+ }
173
+
174
+ const data = await response.json() as {
175
+ status: string
176
+ data: { name: string, size: string, mimeType: string, expiryTime: number }
177
+ code?: string
178
+ description?: string
179
+ }
180
+
181
+ if (data.status === 'error') {
182
+ const errCode = data.code ?? 'unknown-code'
183
+ const errDesc = data.description ?? 'no-description'
184
+ throw new Error(`findFile returned an error: ${errCode} - ${errDesc}`)
185
+ }
186
+ return data.data
187
+ }
188
+
189
+ /**
190
+ * Lists all advertisements belonging to the user from the `/list` route.
191
+ * @returns {Promise<any>} The array of uploads returned by the server
192
+ * @throws {Error} If the server or the route returns an error
193
+ */
194
+ public async listUploads (): Promise<any> {
195
+ const url = `${this.baseURL}/list`
196
+ const response = await this.authFetch.fetch(url, {
197
+ method: 'GET'
198
+ })
199
+ if (!response.ok) {
200
+ throw new Error(`listUploads request failed: HTTP ${response.status}`)
201
+ }
202
+
203
+ const data = await response.json()
204
+ if (data.status === 'error') {
205
+ const errCode = data.code as string ?? 'unknown-code'
206
+ const errDesc = data.description as string ?? 'no-description'
207
+ throw new Error(`listUploads returned an error: ${errCode} - ${errDesc}`)
208
+ }
209
+ return data.uploads
210
+ }
211
+
212
+ /**
213
+ * Renews the hosting time for an existing file advertisement identified by uhrpUrl.
214
+ * Calls the `/renew` route to add `additionalMinutes` to the GCS customTime
215
+ * and re-mint the advertisement token on-chain.
216
+ *
217
+ * @param {string} uhrpUrl - The UHRP URL of the file (e.g., "uhrp://abcd1234...")
218
+ * @param {number} additionalMinutes - The number of minutes to extend
219
+ * @returns {Promise<RenewFileResult>} An object with the new and previous expiry times, plus any cost
220
+ * @throws {Error} If the request fails or the server returns an error
221
+ */
222
+ public async renewFile (uhrpUrl: string, additionalMinutes: number): Promise<RenewFileResult> {
223
+ const url = `${this.baseURL}/renew`
224
+ const body = { uhrpUrl, additionalMinutes }
225
+
226
+ const response = await this.authFetch.fetch(url, {
227
+ method: 'POST',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: JSON.stringify(body)
230
+ })
231
+ if (!response.ok) {
232
+ throw new Error(`renewFile request failed: HTTP ${response.status}`)
233
+ }
234
+
235
+ const data = await response.json() as {
236
+ status: string
237
+ prevExpiryTime?: number
238
+ newExpiryTime?: number
239
+ amount?: number
240
+ code?: string
241
+ description?: string
242
+ }
243
+
244
+ if (data.status === 'error') {
245
+ const errCode = data.code ?? 'unknown-code'
246
+ const errDesc = data.description ?? 'no-description'
247
+ throw new Error(`renewFile returned an error: ${errCode} - ${errDesc}`)
248
+ }
249
+
250
+ return {
251
+ status: data.status,
252
+ prevExpiryTime: data.prevExpiryTime,
253
+ newExpiryTime: data.newExpiryTime,
254
+ amount: data.amount
255
+ }
256
+ }
115
257
  }
@@ -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
  })
@@ -5,3 +5,4 @@ export * from './WalletWireCalls.js'
5
5
  export { default as WalletWireTransceiver } from './WalletWireTransceiver.js'
6
6
  export { default as WalletWireProcessor } from './WalletWireProcessor.js'
7
7
  export { default as HTTPWalletWire } from './HTTPWalletWire.js'
8
+ export { default as HTTPWalletJSON } from './HTTPWalletJSON.js'