@zoralabs/coins-sdk 0.5.1 → 0.5.2

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.
@@ -10,6 +10,45 @@ global.fetch = mockFetch;
10
10
  console.log = vi.fn();
11
11
  console.error = vi.fn();
12
12
 
13
+ function mockJwtResponse(jwt = "test-jwt-token") {
14
+ return {
15
+ ok: true,
16
+ headers: new Headers({
17
+ "Content-Type": "application/json",
18
+ "Content-Length": "1000",
19
+ }),
20
+ json: async () => ({
21
+ createUploadJwtFromApiKey: jwt,
22
+ }),
23
+ };
24
+ }
25
+
26
+ function mockUploadResponse(
27
+ cid = "bafybeiguslukdujd22p7ix53rcszgbg4ine464g33zk2st3lnjpx4uvmri",
28
+ ) {
29
+ return {
30
+ ok: true,
31
+ status: 200,
32
+ headers: new Headers({
33
+ "Content-Type": "application/json",
34
+ }),
35
+ json: async () => ({
36
+ cid,
37
+ size: 100,
38
+ mimeType: "image/png",
39
+ }),
40
+ };
41
+ }
42
+
43
+ function mock401Response() {
44
+ return {
45
+ ok: false,
46
+ status: 401,
47
+ statusText: "Unauthorized",
48
+ text: async () => "Unauthorized",
49
+ };
50
+ }
51
+
13
52
  describe("ZoraUploader", () => {
14
53
  beforeEach(() => {
15
54
  vi.clearAllMocks();
@@ -31,36 +70,15 @@ describe("ZoraUploader", () => {
31
70
 
32
71
  describe("upload", () => {
33
72
  it("should successfully upload a file with the zora uploader", async () => {
34
- // Mock JWT token creation response
35
73
  mockFetch
36
- .mockResolvedValueOnce({
37
- ok: true,
38
- headers: new Headers({
39
- "Content-Type": "application/json",
40
- "Content-Length": "1000",
41
- }),
42
- json: async () => ({
43
- createUploadJwtFromApiKey: "test-jwt-token",
44
- }),
45
- })
46
- .mockResolvedValueOnce({
47
- ok: true,
48
- headers: new Headers({
49
- "Content-Type": "application/json",
50
- }),
51
- json: async () => ({
52
- cid: "bafybeiguslukdujd22p7ix53rcszgbg4ine464g33zk2st3lnjpx4uvmri",
53
- size: 100,
54
- mimeType: "image/png",
55
- }),
56
- });
74
+ .mockResolvedValueOnce(mockJwtResponse())
75
+ .mockResolvedValueOnce(mockUploadResponse());
57
76
 
58
77
  setApiKey("test-api-key");
59
78
  const uploader = new ZoraUploader("0x123");
60
79
  const file = new File(["test"], "test.png", { type: "image/png" });
61
80
  const result = await uploader.upload(file);
62
81
 
63
- // Verify fetch was called with correct parameters
64
82
  expect(mockFetch).toHaveBeenCalledTimes(2);
65
83
  expect(mockFetch).toHaveBeenCalledWith(
66
84
  "https://ipfs-uploader.zora.co/api/v0/add?cid-version=1",
@@ -72,7 +90,6 @@ describe("ZoraUploader", () => {
72
90
  }),
73
91
  );
74
92
 
75
- // Verify result
76
93
  expect(result).toEqual({
77
94
  url: "ipfs://bafybeiguslukdujd22p7ix53rcszgbg4ine464g33zk2st3lnjpx4uvmri",
78
95
  size: 100,
@@ -80,23 +97,65 @@ describe("ZoraUploader", () => {
80
97
  });
81
98
  });
82
99
 
83
- it("should throw an error if upload fails", async () => {
84
- // Mock failed response
100
+ it("should throw an error if upload fails with non-401 status", async () => {
101
+ mockFetch.mockResolvedValueOnce(mockJwtResponse()).mockResolvedValueOnce({
102
+ ok: false,
103
+ status: 400,
104
+ statusText: "Bad Request",
105
+ text: async () => "Invalid file",
106
+ });
107
+
108
+ setApiKey("test-api-key");
109
+ const uploader = new ZoraUploader("0x123");
110
+ const file = new File(["test"], "test.png", { type: "image/png" });
111
+
112
+ await expect(uploader.upload(file)).rejects.toThrow(
113
+ "Failed to upload file",
114
+ );
115
+
116
+ // Should NOT retry on non-401 errors
117
+ expect(mockFetch).toHaveBeenCalledTimes(2); // 1 JWT + 1 upload
118
+ });
119
+
120
+ it("should retry once on 401 with a fresh JWT", async () => {
121
+ mockFetch
122
+ // First JWT fetch
123
+ .mockResolvedValueOnce(mockJwtResponse("stale-jwt"))
124
+ // First upload returns 401
125
+ .mockResolvedValueOnce(mock401Response())
126
+ // Second JWT fetch (refresh)
127
+ .mockResolvedValueOnce(mockJwtResponse("fresh-jwt"))
128
+ // Retry upload succeeds
129
+ .mockResolvedValueOnce(mockUploadResponse());
130
+
131
+ setApiKey("test-api-key");
132
+ const uploader = new ZoraUploader("0x123");
133
+ const file = new File(["test"], "test.png", { type: "image/png" });
134
+ const result = await uploader.upload(file);
135
+
136
+ expect(mockFetch).toHaveBeenCalledTimes(4); // 2 JWT + 2 upload
137
+
138
+ // Verify the retry used the fresh JWT
139
+ const lastUploadCall = mockFetch.mock.calls[3];
140
+ expect(lastUploadCall[1].headers.Authorization).toBe("Bearer fresh-jwt");
141
+
142
+ expect(result).toEqual({
143
+ url: "ipfs://bafybeiguslukdujd22p7ix53rcszgbg4ine464g33zk2st3lnjpx4uvmri",
144
+ size: 100,
145
+ mimeType: "image/png",
146
+ });
147
+ });
148
+
149
+ it("should throw if retry after 401 also fails", async () => {
85
150
  mockFetch
86
- .mockResolvedValueOnce({
87
- ok: true,
88
- headers: new Headers({
89
- "Content-Type": "application/json",
90
- "Content-Length": "1000",
91
- }),
92
- json: async () => ({
93
- createUploadJwtFromApiKey: "test-jwt-token",
94
- }),
95
- })
151
+ .mockResolvedValueOnce(mockJwtResponse("stale-jwt"))
152
+ .mockResolvedValueOnce(mock401Response())
153
+ .mockResolvedValueOnce(mockJwtResponse("also-bad-jwt"))
96
154
  .mockResolvedValueOnce({
97
155
  ok: false,
98
- statusText: "Bad Request",
99
- text: async () => "Invalid file",
156
+ status: 403,
157
+ statusText: "Forbidden",
158
+ text: async () => "Forbidden",
100
159
  });
101
160
 
102
161
  setApiKey("test-api-key");
@@ -107,17 +166,78 @@ describe("ZoraUploader", () => {
107
166
  "Failed to upload file",
108
167
  );
109
168
 
110
- // Verify fetch was called with correct parameters
111
- expect(mockFetch).toHaveBeenCalledTimes(2);
112
- expect(mockFetch).toHaveBeenCalledWith(
113
- "https://ipfs-uploader.zora.co/api/v0/add?cid-version=1",
114
- expect.objectContaining({
115
- method: "POST",
116
- headers: expect.objectContaining({
117
- Authorization: "Bearer test-jwt-token",
118
- }),
119
- }),
120
- );
169
+ expect(mockFetch).toHaveBeenCalledTimes(4);
170
+ });
171
+
172
+ it("should reuse cached JWT for subsequent uploads", async () => {
173
+ mockFetch
174
+ .mockResolvedValueOnce(mockJwtResponse())
175
+ .mockResolvedValueOnce(mockUploadResponse())
176
+ .mockResolvedValueOnce(mockUploadResponse());
177
+
178
+ setApiKey("test-api-key");
179
+ const uploader = new ZoraUploader("0x123");
180
+ const file1 = new File(["test1"], "test1.png", { type: "image/png" });
181
+ const file2 = new File(["test2"], "test2.png", { type: "image/png" });
182
+
183
+ await uploader.upload(file1);
184
+ await uploader.upload(file2);
185
+
186
+ // Only 1 JWT fetch + 2 uploads
187
+ expect(mockFetch).toHaveBeenCalledTimes(3);
188
+ });
189
+
190
+ it("should pass signal to upload request", async () => {
191
+ mockFetch
192
+ .mockResolvedValueOnce(mockJwtResponse())
193
+ .mockResolvedValueOnce(mockUploadResponse());
194
+
195
+ setApiKey("test-api-key");
196
+ const uploader = new ZoraUploader("0x123");
197
+ const file = new File(["test"], "test.png", { type: "image/png" });
198
+ const controller = new AbortController();
199
+
200
+ await uploader.upload(file, { signal: controller.signal });
201
+
202
+ const uploadCall = mockFetch.mock.calls[1];
203
+ expect(uploadCall[1].signal).toBe(controller.signal);
204
+ });
205
+
206
+ it("should pass timeout as AbortSignal to upload request", async () => {
207
+ mockFetch
208
+ .mockResolvedValueOnce(mockJwtResponse())
209
+ .mockResolvedValueOnce(mockUploadResponse());
210
+
211
+ setApiKey("test-api-key");
212
+ const uploader = new ZoraUploader("0x123");
213
+ const file = new File(["test"], "test.png", { type: "image/png" });
214
+
215
+ await uploader.upload(file, { timeout: 30_000 });
216
+
217
+ const uploadCall = mockFetch.mock.calls[1];
218
+ expect(uploadCall[1].signal).toBeDefined();
219
+ });
220
+
221
+ it("should pass combined signal when both signal and timeout are provided", async () => {
222
+ mockFetch
223
+ .mockResolvedValueOnce(mockJwtResponse())
224
+ .mockResolvedValueOnce(mockUploadResponse());
225
+
226
+ setApiKey("test-api-key");
227
+ const uploader = new ZoraUploader("0x123");
228
+ const file = new File(["test"], "test.png", { type: "image/png" });
229
+ const controller = new AbortController();
230
+
231
+ await uploader.upload(file, {
232
+ signal: controller.signal,
233
+ timeout: 30_000,
234
+ });
235
+
236
+ const uploadCall = mockFetch.mock.calls[1];
237
+ // When both signal and timeout are provided, AbortSignal.any() is used
238
+ // so the resulting signal is different from the original
239
+ expect(uploadCall[1].signal).toBeDefined();
240
+ expect(uploadCall[1].signal).not.toBe(controller.signal);
121
241
  });
122
242
  });
123
243
 
@@ -13,11 +13,21 @@ export type UploadResult = {
13
13
  mimeType: string | undefined;
14
14
  };
15
15
 
16
+ /**
17
+ * Options for file upload operations
18
+ */
19
+ export type UploadOptions = {
20
+ /** AbortSignal to cancel the upload */
21
+ signal?: AbortSignal;
22
+ /** Timeout in milliseconds for the upload request (no default) */
23
+ timeout?: number;
24
+ };
25
+
16
26
  /**
17
27
  * Interface for file uploaders (IPFS, Arweave, etc.)
18
28
  */
19
29
  export interface Uploader {
20
- upload(file: File): Promise<UploadResult>;
30
+ upload(file: File, options?: UploadOptions): Promise<UploadResult>;
21
31
  }
22
32
 
23
33
  export type CreateMetadataParameters = {