@solana-mobile/dapp-store-cli 0.15.0 → 0.16.0
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/lib/CliUtils.js +1 -1
- package/lib/package.json +3 -3
- package/lib/upload/CachedStorageDriver.js +194 -29
- package/lib/upload/TurboStorageDriver.js +17 -17
- package/lib/upload/__tests__/CachedStorageDriver.test.js +437 -0
- package/lib/upload/__tests__/TurboStorageDriver.test.js +17 -0
- package/lib/upload/__tests__/contentGateway.test.js +17 -0
- package/lib/upload/contentGateway.js +23 -0
- package/package.json +3 -3
- package/src/CliUtils.ts +1 -1
- package/src/upload/CachedStorageDriver.ts +93 -13
- package/src/upload/TurboStorageDriver.ts +22 -16
- package/src/upload/__tests__/CachedStorageDriver.test.ts +246 -0
- package/src/upload/__tests__/TurboStorageDriver.test.ts +15 -0
- package/src/upload/__tests__/contentGateway.test.ts +31 -0
- package/src/upload/contentGateway.ts +37 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
|
|
2
|
+
try {
|
|
3
|
+
var info = gen[key](arg);
|
|
4
|
+
var value = info.value;
|
|
5
|
+
} catch (error) {
|
|
6
|
+
reject(error);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (info.done) {
|
|
10
|
+
resolve(value);
|
|
11
|
+
} else {
|
|
12
|
+
Promise.resolve(value).then(_next, _throw);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function _async_to_generator(fn) {
|
|
16
|
+
return function() {
|
|
17
|
+
var self = this, args = arguments;
|
|
18
|
+
return new Promise(function(resolve, reject) {
|
|
19
|
+
var gen = fn.apply(self, args);
|
|
20
|
+
function _next(value) {
|
|
21
|
+
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
|
|
22
|
+
}
|
|
23
|
+
function _throw(err) {
|
|
24
|
+
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
|
|
25
|
+
}
|
|
26
|
+
_next(undefined);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function _define_property(obj, key, value) {
|
|
31
|
+
if (key in obj) {
|
|
32
|
+
Object.defineProperty(obj, key, {
|
|
33
|
+
value: value,
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
obj[key] = value;
|
|
40
|
+
}
|
|
41
|
+
return obj;
|
|
42
|
+
}
|
|
43
|
+
function _ts_generator(thisArg, body) {
|
|
44
|
+
var f, y, t, _ = {
|
|
45
|
+
label: 0,
|
|
46
|
+
sent: function() {
|
|
47
|
+
if (t[0] & 1) throw t[1];
|
|
48
|
+
return t[1];
|
|
49
|
+
},
|
|
50
|
+
trys: [],
|
|
51
|
+
ops: []
|
|
52
|
+
}, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
53
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() {
|
|
54
|
+
return this;
|
|
55
|
+
}), g;
|
|
56
|
+
function verb(n) {
|
|
57
|
+
return function(v) {
|
|
58
|
+
return step([
|
|
59
|
+
n,
|
|
60
|
+
v
|
|
61
|
+
]);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function step(op) {
|
|
65
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
66
|
+
while(g && (g = 0, op[0] && (_ = 0)), _)try {
|
|
67
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
68
|
+
if (y = 0, t) op = [
|
|
69
|
+
op[0] & 2,
|
|
70
|
+
t.value
|
|
71
|
+
];
|
|
72
|
+
switch(op[0]){
|
|
73
|
+
case 0:
|
|
74
|
+
case 1:
|
|
75
|
+
t = op;
|
|
76
|
+
break;
|
|
77
|
+
case 4:
|
|
78
|
+
_.label++;
|
|
79
|
+
return {
|
|
80
|
+
value: op[1],
|
|
81
|
+
done: false
|
|
82
|
+
};
|
|
83
|
+
case 5:
|
|
84
|
+
_.label++;
|
|
85
|
+
y = op[1];
|
|
86
|
+
op = [
|
|
87
|
+
0
|
|
88
|
+
];
|
|
89
|
+
continue;
|
|
90
|
+
case 7:
|
|
91
|
+
op = _.ops.pop();
|
|
92
|
+
_.trys.pop();
|
|
93
|
+
continue;
|
|
94
|
+
default:
|
|
95
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
|
|
96
|
+
_ = 0;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
|
|
100
|
+
_.label = op[1];
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (op[0] === 6 && _.label < t[1]) {
|
|
104
|
+
_.label = t[1];
|
|
105
|
+
t = op;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (t && _.label < t[2]) {
|
|
109
|
+
_.label = t[2];
|
|
110
|
+
_.ops.push(op);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (t[2]) _.ops.pop();
|
|
114
|
+
_.trys.pop();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
op = body.call(thisArg, _);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
op = [
|
|
120
|
+
6,
|
|
121
|
+
e
|
|
122
|
+
];
|
|
123
|
+
y = 0;
|
|
124
|
+
} finally{
|
|
125
|
+
f = t = 0;
|
|
126
|
+
}
|
|
127
|
+
if (op[0] & 5) throw op[1];
|
|
128
|
+
return {
|
|
129
|
+
value: op[0] ? op[1] : void 0,
|
|
130
|
+
done: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
import fs from "fs";
|
|
135
|
+
import os from "os";
|
|
136
|
+
import path from "path";
|
|
137
|
+
import { createHash } from "crypto";
|
|
138
|
+
import { jest } from "@jest/globals";
|
|
139
|
+
import { CachedStorageDriver } from "../CachedStorageDriver";
|
|
140
|
+
describe("CachedStorageDriver", function() {
|
|
141
|
+
var originalCwd = process.cwd();
|
|
142
|
+
var tempDir;
|
|
143
|
+
beforeEach(function() {
|
|
144
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cached-storage-driver-"));
|
|
145
|
+
process.chdir(tempDir);
|
|
146
|
+
});
|
|
147
|
+
afterEach(function() {
|
|
148
|
+
process.chdir(originalCwd);
|
|
149
|
+
fs.rmSync(tempDir, {
|
|
150
|
+
recursive: true,
|
|
151
|
+
force: true
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
test("normalizes a cached legacy arweave URL and persists the rewritten manifest", function() {
|
|
155
|
+
return _async_to_generator(function() {
|
|
156
|
+
var file, hash, manifestPath, uploadCalls, storageDriver, driver, rewrittenManifest;
|
|
157
|
+
return _ts_generator(this, function(_state) {
|
|
158
|
+
switch(_state.label){
|
|
159
|
+
case 0:
|
|
160
|
+
file = makeMetaplexFile("icon.png", Buffer.from("cached-asset"));
|
|
161
|
+
hash = hashBuffer(file.buffer);
|
|
162
|
+
manifestPath = ".asset-manifest.json";
|
|
163
|
+
fs.writeFileSync(path.join(tempDir, manifestPath), JSON.stringify({
|
|
164
|
+
assets: _define_property({}, file.fileName, {
|
|
165
|
+
path: file.fileName,
|
|
166
|
+
sha256: hash,
|
|
167
|
+
uri: "https://arweave.net/legacy-id"
|
|
168
|
+
})
|
|
169
|
+
}, null, 2));
|
|
170
|
+
uploadCalls = 0;
|
|
171
|
+
storageDriver = {
|
|
172
|
+
getUploadPrice: function(_bytes) {
|
|
173
|
+
return _async_to_generator(function() {
|
|
174
|
+
return _ts_generator(this, function(_state) {
|
|
175
|
+
throw new Error("getUploadPrice should not be called in this test");
|
|
176
|
+
});
|
|
177
|
+
})();
|
|
178
|
+
},
|
|
179
|
+
upload: function(_file) {
|
|
180
|
+
return _async_to_generator(function() {
|
|
181
|
+
return _ts_generator(this, function(_state) {
|
|
182
|
+
uploadCalls += 1;
|
|
183
|
+
throw new Error("upload should not be called in this test");
|
|
184
|
+
});
|
|
185
|
+
})();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
driver = new CachedStorageDriver(storageDriver, {
|
|
189
|
+
assetManifestPath: manifestPath
|
|
190
|
+
});
|
|
191
|
+
return [
|
|
192
|
+
4,
|
|
193
|
+
expect(driver.upload(file)).resolves.toBe("https://dappstorecontent.com/legacy-id")
|
|
194
|
+
];
|
|
195
|
+
case 1:
|
|
196
|
+
_state.sent();
|
|
197
|
+
expect(uploadCalls).toBe(0);
|
|
198
|
+
rewrittenManifest = JSON.parse(fs.readFileSync(path.join(tempDir, manifestPath), "utf-8"));
|
|
199
|
+
expect(rewrittenManifest.schema_version).toBe("0.1");
|
|
200
|
+
expect(rewrittenManifest.assets[file.fileName].uri).toBe("https://dappstorecontent.com/legacy-id");
|
|
201
|
+
return [
|
|
202
|
+
2
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
})();
|
|
207
|
+
});
|
|
208
|
+
test("supports absolute asset manifest paths", function() {
|
|
209
|
+
return _async_to_generator(function() {
|
|
210
|
+
var file, hash, manifestPath, uploadCalls, storageDriver, driver, rewrittenManifest;
|
|
211
|
+
return _ts_generator(this, function(_state) {
|
|
212
|
+
switch(_state.label){
|
|
213
|
+
case 0:
|
|
214
|
+
file = makeMetaplexFile("absolute-icon.png", Buffer.from("cached-asset"));
|
|
215
|
+
hash = hashBuffer(file.buffer);
|
|
216
|
+
manifestPath = path.join(tempDir, ".asset-manifest.json");
|
|
217
|
+
fs.writeFileSync(manifestPath, JSON.stringify({
|
|
218
|
+
assets: _define_property({}, file.fileName, {
|
|
219
|
+
path: file.fileName,
|
|
220
|
+
sha256: hash,
|
|
221
|
+
uri: "https://arweave.net/absolute-id"
|
|
222
|
+
})
|
|
223
|
+
}, null, 2));
|
|
224
|
+
uploadCalls = 0;
|
|
225
|
+
storageDriver = {
|
|
226
|
+
getUploadPrice: function(_bytes) {
|
|
227
|
+
return _async_to_generator(function() {
|
|
228
|
+
return _ts_generator(this, function(_state) {
|
|
229
|
+
throw new Error("getUploadPrice should not be called in this test");
|
|
230
|
+
});
|
|
231
|
+
})();
|
|
232
|
+
},
|
|
233
|
+
upload: function(_file) {
|
|
234
|
+
return _async_to_generator(function() {
|
|
235
|
+
return _ts_generator(this, function(_state) {
|
|
236
|
+
uploadCalls += 1;
|
|
237
|
+
throw new Error("upload should not be called in this test");
|
|
238
|
+
});
|
|
239
|
+
})();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
driver = new CachedStorageDriver(storageDriver, {
|
|
243
|
+
assetManifestPath: manifestPath
|
|
244
|
+
});
|
|
245
|
+
return [
|
|
246
|
+
4,
|
|
247
|
+
expect(driver.upload(file)).resolves.toBe("https://dappstorecontent.com/absolute-id")
|
|
248
|
+
];
|
|
249
|
+
case 1:
|
|
250
|
+
_state.sent();
|
|
251
|
+
expect(uploadCalls).toBe(0);
|
|
252
|
+
rewrittenManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
253
|
+
expect(rewrittenManifest.assets[file.fileName].uri).toBe("https://dappstorecontent.com/absolute-id");
|
|
254
|
+
return [
|
|
255
|
+
2
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
})();
|
|
260
|
+
});
|
|
261
|
+
test("normalizes fresh uploads before persisting them in the manifest", function() {
|
|
262
|
+
return _async_to_generator(function() {
|
|
263
|
+
var file, manifestPath, uploadCalls, storageDriver, driver, persistedManifest;
|
|
264
|
+
return _ts_generator(this, function(_state) {
|
|
265
|
+
switch(_state.label){
|
|
266
|
+
case 0:
|
|
267
|
+
file = makeMetaplexFile("banner.png", Buffer.from("fresh-asset"));
|
|
268
|
+
manifestPath = ".asset-manifest.json";
|
|
269
|
+
uploadCalls = 0;
|
|
270
|
+
storageDriver = {
|
|
271
|
+
getUploadPrice: function(_bytes) {
|
|
272
|
+
return _async_to_generator(function() {
|
|
273
|
+
return _ts_generator(this, function(_state) {
|
|
274
|
+
throw new Error("getUploadPrice should not be called in this test");
|
|
275
|
+
});
|
|
276
|
+
})();
|
|
277
|
+
},
|
|
278
|
+
upload: function(_file) {
|
|
279
|
+
return _async_to_generator(function() {
|
|
280
|
+
return _ts_generator(this, function(_state) {
|
|
281
|
+
uploadCalls += 1;
|
|
282
|
+
return [
|
|
283
|
+
2,
|
|
284
|
+
"https://arweave.com/fresh-id"
|
|
285
|
+
];
|
|
286
|
+
});
|
|
287
|
+
})();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
driver = new CachedStorageDriver(storageDriver, {
|
|
291
|
+
assetManifestPath: manifestPath
|
|
292
|
+
});
|
|
293
|
+
return [
|
|
294
|
+
4,
|
|
295
|
+
expect(driver.upload(file)).resolves.toBe("https://dappstorecontent.com/fresh-id")
|
|
296
|
+
];
|
|
297
|
+
case 1:
|
|
298
|
+
_state.sent();
|
|
299
|
+
expect(uploadCalls).toBe(1);
|
|
300
|
+
persistedManifest = JSON.parse(fs.readFileSync(path.join(tempDir, manifestPath), "utf-8"));
|
|
301
|
+
expect(persistedManifest.schema_version).toBe("0.1");
|
|
302
|
+
expect(persistedManifest.assets[file.fileName].uri).toBe("https://dappstorecontent.com/fresh-id");
|
|
303
|
+
return [
|
|
304
|
+
2
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
})();
|
|
309
|
+
});
|
|
310
|
+
test("normalizes inline metadata uploads without caching them", function() {
|
|
311
|
+
return _async_to_generator(function() {
|
|
312
|
+
var file, uploadCalls, storageDriver, driver;
|
|
313
|
+
return _ts_generator(this, function(_state) {
|
|
314
|
+
switch(_state.label){
|
|
315
|
+
case 0:
|
|
316
|
+
file = makeMetaplexFile("inline.json", Buffer.from("{}"));
|
|
317
|
+
uploadCalls = 0;
|
|
318
|
+
storageDriver = {
|
|
319
|
+
getUploadPrice: function(_bytes) {
|
|
320
|
+
return _async_to_generator(function() {
|
|
321
|
+
return _ts_generator(this, function(_state) {
|
|
322
|
+
throw new Error("getUploadPrice should not be called in this test");
|
|
323
|
+
});
|
|
324
|
+
})();
|
|
325
|
+
},
|
|
326
|
+
upload: function(_file) {
|
|
327
|
+
return _async_to_generator(function() {
|
|
328
|
+
return _ts_generator(this, function(_state) {
|
|
329
|
+
uploadCalls += 1;
|
|
330
|
+
return [
|
|
331
|
+
2,
|
|
332
|
+
"https://arweave.net/metadata-id"
|
|
333
|
+
];
|
|
334
|
+
});
|
|
335
|
+
})();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
driver = new CachedStorageDriver(storageDriver, {
|
|
339
|
+
assetManifestPath: ".asset-manifest.json"
|
|
340
|
+
});
|
|
341
|
+
return [
|
|
342
|
+
4,
|
|
343
|
+
expect(driver.upload(file)).resolves.toBe("https://dappstorecontent.com/metadata-id")
|
|
344
|
+
];
|
|
345
|
+
case 1:
|
|
346
|
+
_state.sent();
|
|
347
|
+
expect(uploadCalls).toBe(1);
|
|
348
|
+
expect(fs.existsSync(path.join(tempDir, ".asset-manifest.json"))).toBe(false);
|
|
349
|
+
return [
|
|
350
|
+
2
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
})();
|
|
355
|
+
});
|
|
356
|
+
test("returns a normalized cached URL even when manifest rewrite persistence fails", function() {
|
|
357
|
+
return _async_to_generator(function() {
|
|
358
|
+
var file, hash, manifestPath, writeFileSpy, warnSpy, storageDriver, driver;
|
|
359
|
+
return _ts_generator(this, function(_state) {
|
|
360
|
+
switch(_state.label){
|
|
361
|
+
case 0:
|
|
362
|
+
file = makeMetaplexFile("icon.png", Buffer.from("cached-asset"));
|
|
363
|
+
hash = hashBuffer(file.buffer);
|
|
364
|
+
manifestPath = ".asset-manifest.json";
|
|
365
|
+
fs.writeFileSync(path.join(tempDir, manifestPath), JSON.stringify({
|
|
366
|
+
assets: _define_property({}, file.fileName, {
|
|
367
|
+
path: file.fileName,
|
|
368
|
+
sha256: hash,
|
|
369
|
+
uri: "https://arweave.net/rewrite-failure-id"
|
|
370
|
+
})
|
|
371
|
+
}, null, 2));
|
|
372
|
+
writeFileSpy = jest.spyOn(fs.promises, "writeFile").mockRejectedValueOnce(new Error("disk full"));
|
|
373
|
+
warnSpy = jest.spyOn(console, "warn").mockImplementation(function() {
|
|
374
|
+
return undefined;
|
|
375
|
+
});
|
|
376
|
+
storageDriver = {
|
|
377
|
+
getUploadPrice: function(_bytes) {
|
|
378
|
+
return _async_to_generator(function() {
|
|
379
|
+
return _ts_generator(this, function(_state) {
|
|
380
|
+
throw new Error("getUploadPrice should not be called in this test");
|
|
381
|
+
});
|
|
382
|
+
})();
|
|
383
|
+
},
|
|
384
|
+
upload: function(_file) {
|
|
385
|
+
return _async_to_generator(function() {
|
|
386
|
+
return _ts_generator(this, function(_state) {
|
|
387
|
+
throw new Error("upload should not be called in this test");
|
|
388
|
+
});
|
|
389
|
+
})();
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
driver = new CachedStorageDriver(storageDriver, {
|
|
393
|
+
assetManifestPath: manifestPath
|
|
394
|
+
});
|
|
395
|
+
_state.label = 1;
|
|
396
|
+
case 1:
|
|
397
|
+
_state.trys.push([
|
|
398
|
+
1,
|
|
399
|
+
,
|
|
400
|
+
3,
|
|
401
|
+
4
|
|
402
|
+
]);
|
|
403
|
+
return [
|
|
404
|
+
4,
|
|
405
|
+
expect(driver.upload(file)).resolves.toBe("https://dappstorecontent.com/rewrite-failure-id")
|
|
406
|
+
];
|
|
407
|
+
case 2:
|
|
408
|
+
_state.sent();
|
|
409
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to rewrite .asset-manifest.json; continuing with normalized URL"));
|
|
410
|
+
return [
|
|
411
|
+
3,
|
|
412
|
+
4
|
|
413
|
+
];
|
|
414
|
+
case 3:
|
|
415
|
+
writeFileSpy.mockRestore();
|
|
416
|
+
warnSpy.mockRestore();
|
|
417
|
+
return [
|
|
418
|
+
7
|
|
419
|
+
];
|
|
420
|
+
case 4:
|
|
421
|
+
return [
|
|
422
|
+
2
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
})();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
var makeMetaplexFile = function(fileName, buffer) {
|
|
430
|
+
return {
|
|
431
|
+
fileName: fileName,
|
|
432
|
+
buffer: buffer
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
var hashBuffer = function(buffer) {
|
|
436
|
+
return createHash("sha256").update(buffer).digest("base64");
|
|
437
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getTurboServiceConfig } from "../TurboStorageDriver";
|
|
2
|
+
describe("TurboStorageDriver", function() {
|
|
3
|
+
test("uses the latest Turbo SDK service config shape for devnet", function() {
|
|
4
|
+
expect(getTurboServiceConfig("devnet")).toEqual({
|
|
5
|
+
gatewayUrl: "https://api.devnet.solana.com",
|
|
6
|
+
uploadServiceConfig: {
|
|
7
|
+
url: "https://upload.ardrive.dev"
|
|
8
|
+
},
|
|
9
|
+
paymentServiceConfig: {
|
|
10
|
+
url: "https://payment.ardrive.dev"
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
test("relies on the SDK defaults for mainnet service URLs", function() {
|
|
15
|
+
expect(getTurboServiceConfig("mainnet")).toEqual({});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { buildPublicContentUrl, normalizePublicContentUrl } from "../contentGateway";
|
|
2
|
+
describe("contentGateway", function() {
|
|
3
|
+
test("builds a mainnet dappstorecontent URL from an upload id", function() {
|
|
4
|
+
expect(buildPublicContentUrl("abc123", "mainnet")).toBe("https://dappstorecontent.com/abc123");
|
|
5
|
+
});
|
|
6
|
+
test("builds a devnet Turbo raw URL from an upload id", function() {
|
|
7
|
+
expect(buildPublicContentUrl("abc123", "devnet")).toBe("https://turbo.ardrive.dev/raw/abc123");
|
|
8
|
+
});
|
|
9
|
+
test("normalizes arweave.net and arweave.com URLs to dappstorecontent", function() {
|
|
10
|
+
expect(normalizePublicContentUrl("https://arweave.net/abc123")).toBe("https://dappstorecontent.com/abc123");
|
|
11
|
+
expect(normalizePublicContentUrl("https://www.arweave.com/abc123?ext=png")).toBe("https://dappstorecontent.com/abc123?ext=png");
|
|
12
|
+
});
|
|
13
|
+
test("leaves non-arweave URLs unchanged", function() {
|
|
14
|
+
expect(normalizePublicContentUrl("https://turbo.ardrive.dev/raw/abc123")).toBe("https://turbo.ardrive.dev/raw/abc123");
|
|
15
|
+
expect(normalizePublicContentUrl("not-a-url")).toBe("not-a-url");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
var MAINNET_PUBLIC_CONTENT_GATEWAY = "https://dappstorecontent.com";
|
|
2
|
+
var DEVNET_PUBLIC_CONTENT_GATEWAY = "https://turbo.ardrive.dev/raw";
|
|
3
|
+
var LEGACY_MAINNET_CONTENT_HOSTS = new Set([
|
|
4
|
+
"arweave.net",
|
|
5
|
+
"www.arweave.net",
|
|
6
|
+
"arweave.com",
|
|
7
|
+
"www.arweave.com"
|
|
8
|
+
]);
|
|
9
|
+
export var buildPublicContentUrl = function(id, network) {
|
|
10
|
+
var gateway = network === "devnet" ? DEVNET_PUBLIC_CONTENT_GATEWAY : MAINNET_PUBLIC_CONTENT_GATEWAY;
|
|
11
|
+
return "".concat(gateway, "/").concat(id);
|
|
12
|
+
};
|
|
13
|
+
export var normalizePublicContentUrl = function(url) {
|
|
14
|
+
try {
|
|
15
|
+
var parsed = new URL(url);
|
|
16
|
+
if (!LEGACY_MAINNET_CONTENT_HOSTS.has(parsed.host.toLowerCase())) {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
return "".concat(MAINNET_PUBLIC_CONTENT_GATEWAY).concat(parsed.pathname).concat(parsed.search).concat(parsed.hash);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana-mobile/dapp-store-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"ts-node": "^10.9.1"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@ardrive/turbo-sdk": "^1.
|
|
47
|
+
"@ardrive/turbo-sdk": "^1.41.0",
|
|
48
48
|
"@aws-sdk/client-s3": "^3.321.1",
|
|
49
49
|
"@metaplex-foundation/js-plugin-aws": "^0.20.0",
|
|
50
|
-
"@solana-mobile/dapp-store-publishing-tools": "0.
|
|
50
|
+
"@solana-mobile/dapp-store-publishing-tools": "0.16.0",
|
|
51
51
|
"@solana/web3.js": "1.92.1",
|
|
52
52
|
"@types/semver": "^7.3.13",
|
|
53
53
|
"ajv": "^8.11.0",
|
package/src/CliUtils.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { awsStorage } from "@metaplex-foundation/js-plugin-aws";
|
|
|
17
17
|
import { S3StorageManager } from "./config/index.js";
|
|
18
18
|
|
|
19
19
|
export class Constants {
|
|
20
|
-
static CLI_VERSION = "0.
|
|
20
|
+
static CLI_VERSION = "0.16.0";
|
|
21
21
|
static CONFIG_FILE_NAME = "config.yaml";
|
|
22
22
|
static DEFAULT_RPC_DEVNET = "https://api.devnet.solana.com";
|
|
23
23
|
static DEFAULT_PRIORITY_FEE = 500000;
|
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import type { MetaplexFile, StorageDriver } from "@metaplex-foundation/js";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
|
+
import { normalizePublicContentUrl } from "./contentGateway.js";
|
|
5
6
|
|
|
6
7
|
type URI = string;
|
|
7
8
|
|
|
@@ -44,11 +45,83 @@ export class CachedStorageDriver implements StorageDriver {
|
|
|
44
45
|
return this.storageDriver.getUploadPrice(bytes);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
private resolveAssetManifestPath(filename = this.assetManifestPath): string {
|
|
49
|
+
return path.resolve(process.cwd(), filename);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private normalizeAsset(
|
|
53
|
+
filename: string,
|
|
54
|
+
asset: unknown
|
|
55
|
+
): Asset | undefined {
|
|
56
|
+
if (!asset || typeof asset !== "object") return;
|
|
57
|
+
|
|
58
|
+
const candidate = asset as Partial<Asset>;
|
|
59
|
+
const pathValue =
|
|
60
|
+
typeof candidate.path === "string" ? candidate.path : filename;
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
typeof candidate.sha256 !== "string" ||
|
|
64
|
+
typeof candidate.uri !== "string"
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
path: pathValue,
|
|
71
|
+
sha256: candidate.sha256,
|
|
72
|
+
uri: candidate.uri,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private normalizeAssetManifest(
|
|
77
|
+
assetManifest: Partial<AssetManifestSchema> | undefined
|
|
78
|
+
): AssetManifestSchema | undefined {
|
|
79
|
+
if (!assetManifest || typeof assetManifest !== "object") return;
|
|
80
|
+
|
|
81
|
+
const assets: Record<string, Asset> = {};
|
|
82
|
+
const assetEntries =
|
|
83
|
+
assetManifest.assets && typeof assetManifest.assets === "object"
|
|
84
|
+
? Object.entries(assetManifest.assets)
|
|
85
|
+
: [];
|
|
86
|
+
|
|
87
|
+
for (const [filename, asset] of assetEntries) {
|
|
88
|
+
const normalizedAsset = this.normalizeAsset(filename, asset);
|
|
89
|
+
if (normalizedAsset) {
|
|
90
|
+
assets[filename] = normalizedAsset;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
schema_version:
|
|
96
|
+
typeof assetManifest.schema_version === "string"
|
|
97
|
+
? assetManifest.schema_version
|
|
98
|
+
: CachedStorageDriver.SCHEMA_VERSION,
|
|
99
|
+
assets,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async writeAssetManifest(): Promise<void> {
|
|
104
|
+
const normalizedAssetManifest = this.normalizeAssetManifest(
|
|
105
|
+
this.assetManifest
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!normalizedAssetManifest) {
|
|
109
|
+
throw new Error("Asset manifest is not serializable");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.assetManifest = normalizedAssetManifest;
|
|
113
|
+
await fs.promises.writeFile(
|
|
114
|
+
this.resolveAssetManifestPath(),
|
|
115
|
+
JSON.stringify(this.assetManifest, null, 2),
|
|
116
|
+
"utf-8"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
47
120
|
loadAssetManifest(filename: string): AssetManifestSchema | undefined {
|
|
48
121
|
try {
|
|
49
|
-
return
|
|
50
|
-
fs.readFileSync(filename, "utf-8")
|
|
51
|
-
)
|
|
122
|
+
return this.normalizeAssetManifest(
|
|
123
|
+
JSON.parse(fs.readFileSync(this.resolveAssetManifestPath(filename), "utf-8"))
|
|
124
|
+
);
|
|
52
125
|
} catch (error) {
|
|
53
126
|
console.warn(`Failed opening ${filename}; initializing with a blank asset manifest`);
|
|
54
127
|
return;
|
|
@@ -65,20 +138,32 @@ export class CachedStorageDriver implements StorageDriver {
|
|
|
65
138
|
async upload(file: MetaplexFile): Promise<string> {
|
|
66
139
|
// `inline.json` is the NFT-related metadata. This data is not stable so we'll skip the caching step
|
|
67
140
|
if (file.fileName === "inline.json") {
|
|
68
|
-
return await this.storageDriver.upload(file);
|
|
141
|
+
return normalizePublicContentUrl(await this.storageDriver.upload(file));
|
|
69
142
|
}
|
|
70
143
|
const hash = createHash("sha256").update(file.buffer).digest("base64");
|
|
71
144
|
|
|
72
145
|
const uploadedAsset = this.uploadedAsset(file.fileName, { sha256: hash });
|
|
73
146
|
if (uploadedAsset) {
|
|
147
|
+
const normalizedUri = normalizePublicContentUrl(uploadedAsset.uri);
|
|
148
|
+
if (normalizedUri !== uploadedAsset.uri) {
|
|
149
|
+
uploadedAsset.uri = normalizedUri;
|
|
150
|
+
try {
|
|
151
|
+
await this.writeAssetManifest();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
console.warn(
|
|
155
|
+
`Failed to rewrite ${this.assetManifestPath}; continuing with normalized URL: ${message}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
74
159
|
console.log(
|
|
75
|
-
`Asset ${file.fileName} already uploaded at ${
|
|
160
|
+
`Asset ${file.fileName} already uploaded at ${normalizedUri}`
|
|
76
161
|
);
|
|
77
|
-
return
|
|
162
|
+
return normalizedUri;
|
|
78
163
|
}
|
|
79
164
|
|
|
80
165
|
console.log(`Uploading ${file.fileName}`);
|
|
81
|
-
const uri = await this.storageDriver.upload(file);
|
|
166
|
+
const uri = normalizePublicContentUrl(await this.storageDriver.upload(file));
|
|
82
167
|
|
|
83
168
|
this.assetManifest.assets[file.fileName] = {
|
|
84
169
|
path: file.fileName,
|
|
@@ -86,12 +171,7 @@ export class CachedStorageDriver implements StorageDriver {
|
|
|
86
171
|
uri,
|
|
87
172
|
};
|
|
88
173
|
|
|
89
|
-
await
|
|
90
|
-
path.join(process.cwd(), this.assetManifestPath),
|
|
91
|
-
// Something is really weird, I can't seem to stringify `this.assetManifest` straight-up. Here be dragons
|
|
92
|
-
JSON.stringify({ assets: { ...this.assetManifest.assets } }, null, 2),
|
|
93
|
-
"utf-8"
|
|
94
|
-
);
|
|
174
|
+
await this.writeAssetManifest();
|
|
95
175
|
console.log(`${file.fileName} uploaded at ${uri}`)
|
|
96
176
|
|
|
97
177
|
return uri;
|