create-dig-app 0.3.0 → 0.3.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.
- package/README.md +50 -42
- package/lib/cli.js +18 -8
- package/lib/index.js +5 -6
- package/lib/nft-cli.js +13 -12
- package/lib/nft-metadata.js +16 -5
- package/lib/templates.js +7 -8
- package/package.json +6 -3
- package/templates/dapp-window-chia/src/App.jsx +4 -4
- package/templates/dapp-window-chia/src/styles.css +35 -11
- package/templates/next-static/app/globals.css +18 -7
- package/templates/next-static/app/page.jsx +2 -2
- package/templates/nft-collection/collection.json +1 -3
- package/templates/nft-collection/scripts/dig-nft.mjs +145 -62
- package/templates/nft-collection/src/styles.css +53 -14
- package/templates/nft-drop/src/App.jsx +5 -5
- package/templates/nft-drop/src/styles.css +31 -10
- package/templates/static-site/src/index.html +3 -3
- package/templates/static-site/src/styles.css +18 -7
- package/templates/vite-react/src/App.jsx +2 -2
- package/templates/vite-react/src/styles.css +18 -7
- package/templates-ts/dapp-window-chia/src/App.tsx +4 -4
- package/templates-ts/dapp-window-chia/src/styles.css +35 -11
- package/templates-ts/dapp-window-chia/src/wallet.ts +1 -5
- package/templates-ts/next-static/app/globals.css +18 -7
- package/templates-ts/next-static/app/page.tsx +2 -2
- package/templates-ts/nft-drop/src/App.tsx +5 -5
- package/templates-ts/nft-drop/src/mint.ts +1 -5
- package/templates-ts/nft-drop/src/styles.css +31 -10
- package/templates-ts/vite-react/src/App.tsx +2 -2
- package/templates-ts/vite-react/src/styles.css +18 -7
|
@@ -23,14 +23,7 @@
|
|
|
23
23
|
// hash: metadata_hash = sha256(canonical_json_bytes); data/license_hash = sha256(bytes)
|
|
24
24
|
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
|
-
import {
|
|
27
|
-
existsSync,
|
|
28
|
-
mkdirSync,
|
|
29
|
-
readFileSync,
|
|
30
|
-
readdirSync,
|
|
31
|
-
statSync,
|
|
32
|
-
writeFileSync,
|
|
33
|
-
} from "node:fs";
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
34
27
|
import { basename, dirname, extname, join } from "node:path";
|
|
35
28
|
import { fileURLToPath } from "node:url";
|
|
36
29
|
|
|
@@ -52,7 +45,10 @@ function attrValue(v) {
|
|
|
52
45
|
|
|
53
46
|
function normalizeAttributes(attrs) {
|
|
54
47
|
return (attrs ?? [])
|
|
55
|
-
.map((a) => ({
|
|
48
|
+
.map((a) => ({
|
|
49
|
+
trait_type: String(a?.trait_type ?? a?.traitType ?? "").trim(),
|
|
50
|
+
value: attrValue(a?.value),
|
|
51
|
+
}))
|
|
56
52
|
.filter((a) => a.trait_type !== "");
|
|
57
53
|
}
|
|
58
54
|
|
|
@@ -84,7 +80,8 @@ function buildChip0007Metadata(input) {
|
|
|
84
80
|
if (attributes.length > 0) md.attributes = attributes;
|
|
85
81
|
if (input?.series_number != null) md.series_number = Number(input.series_number);
|
|
86
82
|
if (input?.series_total != null) md.series_total = Number(input.series_total);
|
|
87
|
-
if (input?.minting_tool != null && String(input.minting_tool) !== "")
|
|
83
|
+
if (input?.minting_tool != null && String(input.minting_tool) !== "")
|
|
84
|
+
md.minting_tool = String(input.minting_tool);
|
|
88
85
|
return md;
|
|
89
86
|
}
|
|
90
87
|
|
|
@@ -119,7 +116,9 @@ const FILE_KEYS = new Set(["file", "filename", "image", "media", "asset"]);
|
|
|
119
116
|
const DESC_KEYS = new Set(["description", "desc"]);
|
|
120
117
|
|
|
121
118
|
function classifyKey(key) {
|
|
122
|
-
const k = String(key ?? "")
|
|
119
|
+
const k = String(key ?? "")
|
|
120
|
+
.trim()
|
|
121
|
+
.toLowerCase();
|
|
123
122
|
if (NAME_KEYS.has(k)) return "name";
|
|
124
123
|
if (FILE_KEYS.has(k)) return "file";
|
|
125
124
|
if (DESC_KEYS.has(k)) return "description";
|
|
@@ -134,58 +133,79 @@ function splitCsvLine(line) {
|
|
|
134
133
|
const ch = line[i];
|
|
135
134
|
if (inQuotes) {
|
|
136
135
|
if (ch === '"') {
|
|
137
|
-
if (line[i + 1] === '"') {
|
|
138
|
-
|
|
136
|
+
if (line[i + 1] === '"') {
|
|
137
|
+
cur += '"';
|
|
138
|
+
i++;
|
|
139
|
+
} else inQuotes = false;
|
|
139
140
|
} else cur += ch;
|
|
140
141
|
} else if (ch === '"') inQuotes = true;
|
|
141
|
-
else if (ch === ",") {
|
|
142
|
-
|
|
142
|
+
else if (ch === ",") {
|
|
143
|
+
out.push(cur);
|
|
144
|
+
cur = "";
|
|
145
|
+
} else cur += ch;
|
|
143
146
|
}
|
|
144
147
|
out.push(cur);
|
|
145
148
|
return out.map((c) => c.trim());
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
function recordToItem(rec) {
|
|
149
|
-
let name = "",
|
|
152
|
+
let name = "",
|
|
153
|
+
description,
|
|
154
|
+
file;
|
|
150
155
|
const attributes = [];
|
|
151
156
|
for (const [col, raw] of Object.entries(rec)) {
|
|
152
157
|
const role = classifyKey(col);
|
|
153
158
|
const val = raw == null ? "" : String(raw).trim();
|
|
154
|
-
if (role === "name") {
|
|
155
|
-
|
|
156
|
-
else if (role === "
|
|
157
|
-
|
|
159
|
+
if (role === "name") {
|
|
160
|
+
if (!name) name = val;
|
|
161
|
+
} else if (role === "file") {
|
|
162
|
+
if (!file && val) file = val;
|
|
163
|
+
} else if (role === "description") {
|
|
164
|
+
if (!description && val) description = val;
|
|
165
|
+
} else if (val !== "") attributes.push({ trait_type: col, value: val });
|
|
158
166
|
}
|
|
159
167
|
if (!name) throw new Error("Every item needs a name (a `name` or `title` column).");
|
|
160
168
|
return { name, description: description || undefined, file: file || undefined, attributes };
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
function parseTraitsCsv(text) {
|
|
164
|
-
const lines = String(text ?? "")
|
|
172
|
+
const lines = String(text ?? "")
|
|
173
|
+
.split(/\r?\n/)
|
|
174
|
+
.filter((l) => l.trim() !== "");
|
|
165
175
|
if (lines.length < 2) return [];
|
|
166
176
|
const header = splitCsvLine(lines[0]);
|
|
167
177
|
const items = [];
|
|
168
178
|
for (let i = 1; i < lines.length; i++) {
|
|
169
179
|
const cells = splitCsvLine(lines[i]);
|
|
170
180
|
const rec = {};
|
|
171
|
-
header.forEach((h, j) => {
|
|
181
|
+
header.forEach((h, j) => {
|
|
182
|
+
rec[h] = cells[j] ?? "";
|
|
183
|
+
});
|
|
172
184
|
items.push(recordToItem(rec));
|
|
173
185
|
}
|
|
174
186
|
return items;
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
function jsonObjToItem(obj) {
|
|
178
|
-
let name = "",
|
|
190
|
+
let name = "",
|
|
191
|
+
description,
|
|
192
|
+
file;
|
|
179
193
|
let attributes = [];
|
|
180
194
|
for (const [key, raw] of Object.entries(obj ?? {})) {
|
|
181
195
|
const k = String(key).toLowerCase();
|
|
182
|
-
if (k === "attributes" && Array.isArray(raw)) {
|
|
196
|
+
if (k === "attributes" && Array.isArray(raw)) {
|
|
197
|
+
attributes = normalizeAttributes(raw);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
183
200
|
const role = classifyKey(key);
|
|
184
201
|
const val = raw == null ? "" : String(raw).trim();
|
|
185
|
-
if (role === "name") {
|
|
186
|
-
|
|
187
|
-
else if (role === "
|
|
188
|
-
|
|
202
|
+
if (role === "name") {
|
|
203
|
+
if (!name) name = val;
|
|
204
|
+
} else if (role === "file") {
|
|
205
|
+
if (!file && val) file = val;
|
|
206
|
+
} else if (role === "description") {
|
|
207
|
+
if (!description && val) description = val;
|
|
208
|
+
} else if (val !== "") attributes.push({ trait_type: key, value: val });
|
|
189
209
|
}
|
|
190
210
|
if (!name) throw new Error("Every item needs a name (a `name` or `title` field).");
|
|
191
211
|
return { name, description: description || undefined, file: file || undefined, attributes };
|
|
@@ -205,7 +225,9 @@ function itemsFromImages(fileNames) {
|
|
|
205
225
|
return (fileNames ?? []).map((file) => {
|
|
206
226
|
const stem = String(file).replace(/\.[^.]+$/, "");
|
|
207
227
|
const name = stem
|
|
208
|
-
.replace(/[_-]+/g, " ")
|
|
228
|
+
.replace(/[_-]+/g, " ")
|
|
229
|
+
.replace(/\s+/g, " ")
|
|
230
|
+
.trim()
|
|
209
231
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
210
232
|
return { name: name || stem, file, attributes: [] };
|
|
211
233
|
});
|
|
@@ -214,7 +236,9 @@ function itemsFromImages(fileNames) {
|
|
|
214
236
|
function capsuleResourceUris({ storeId, root, resource }) {
|
|
215
237
|
const sid = String(storeId ?? "").trim();
|
|
216
238
|
const r = String(root ?? "").trim();
|
|
217
|
-
const res = String(resource ?? "")
|
|
239
|
+
const res = String(resource ?? "")
|
|
240
|
+
.trim()
|
|
241
|
+
.replace(/^\/+/, "");
|
|
218
242
|
const urn = `urn:dig:chia:${sid}${r ? `:${r}` : ""}/${res}`;
|
|
219
243
|
const https = `https://${sid}.${CAPSULE_HTTPS_GATEWAY}/${res}`;
|
|
220
244
|
return { urn, https, uris: [urn, https] };
|
|
@@ -225,11 +249,13 @@ const LICENSES = {
|
|
|
225
249
|
title: "CC0 1.0 — public domain dedication",
|
|
226
250
|
text: ({ holder = "the creator", year = new Date().getFullYear() } = {}) =>
|
|
227
251
|
[
|
|
228
|
-
"CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
|
|
252
|
+
"CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
|
|
253
|
+
"",
|
|
229
254
|
`The work associated with this NFT is dedicated to the public domain by ${holder} (${year}).`,
|
|
230
255
|
"To the extent possible under law, the creator has waived all copyright and related or",
|
|
231
256
|
"neighboring rights to this work. You can copy, modify, distribute and perform the work, even",
|
|
232
|
-
"for commercial purposes, all without asking permission.",
|
|
257
|
+
"for commercial purposes, all without asking permission.",
|
|
258
|
+
"",
|
|
233
259
|
"SPDX-License-Identifier: CC0-1.0",
|
|
234
260
|
"Full text: https://creativecommons.org/publicdomain/zero/1.0/legalcode",
|
|
235
261
|
].join("\n") + "\n",
|
|
@@ -238,10 +264,13 @@ const LICENSES = {
|
|
|
238
264
|
title: "CC BY 4.0 — attribution required",
|
|
239
265
|
text: ({ holder = "the creator", year = new Date().getFullYear() } = {}) =>
|
|
240
266
|
[
|
|
241
|
-
"Creative Commons Attribution 4.0 International (CC BY 4.0)",
|
|
242
|
-
|
|
267
|
+
"Creative Commons Attribution 4.0 International (CC BY 4.0)",
|
|
268
|
+
"",
|
|
269
|
+
`Copyright (c) ${year} ${holder}.`,
|
|
270
|
+
"",
|
|
243
271
|
"You are free to share and adapt this work for any purpose, even commercially, provided you give",
|
|
244
|
-
"appropriate credit, provide a link to the license, and indicate if changes were made.",
|
|
272
|
+
"appropriate credit, provide a link to the license, and indicate if changes were made.",
|
|
273
|
+
"",
|
|
245
274
|
"SPDX-License-Identifier: CC-BY-4.0",
|
|
246
275
|
"Full text: https://creativecommons.org/licenses/by/4.0/legalcode",
|
|
247
276
|
].join("\n") + "\n",
|
|
@@ -250,8 +279,10 @@ const LICENSES = {
|
|
|
250
279
|
title: "All rights reserved — no reuse without permission",
|
|
251
280
|
text: ({ holder = "the creator", year = new Date().getFullYear() } = {}) =>
|
|
252
281
|
[
|
|
253
|
-
"All Rights Reserved",
|
|
254
|
-
|
|
282
|
+
"All Rights Reserved",
|
|
283
|
+
"",
|
|
284
|
+
`Copyright (c) ${year} ${holder}. All rights reserved.`,
|
|
285
|
+
"",
|
|
255
286
|
"No part of the work associated with this NFT may be reproduced, distributed, or transmitted in",
|
|
256
287
|
"any form or by any means without the prior written permission of the copyright holder, except",
|
|
257
288
|
"for the holder's own personal display of the NFT they own.",
|
|
@@ -261,12 +292,15 @@ const LICENSES = {
|
|
|
261
292
|
title: "Limited commercial license — holder may use commercially",
|
|
262
293
|
text: ({ holder = "the creator", year = new Date().getFullYear() } = {}) =>
|
|
263
294
|
[
|
|
264
|
-
"Limited Commercial License (DIG Commercial 1.0)",
|
|
265
|
-
|
|
295
|
+
"Limited Commercial License (DIG Commercial 1.0)",
|
|
296
|
+
"",
|
|
297
|
+
`Copyright (c) ${year} ${holder}.`,
|
|
298
|
+
"",
|
|
266
299
|
"The verified owner of this NFT is granted a worldwide, non-exclusive license to use, reproduce,",
|
|
267
300
|
"and display the associated artwork for commercial purposes, up to gross revenues of US$100,000",
|
|
268
301
|
"per year attributable to the artwork. This license transfers with ownership of the NFT and",
|
|
269
|
-
"terminates on transfer. All other rights are reserved by the copyright holder.",
|
|
302
|
+
"terminates on transfer. All other rights are reserved by the copyright holder.",
|
|
303
|
+
"",
|
|
270
304
|
"SPDX-License-Identifier: LicenseRef-DIG-Commercial-1.0",
|
|
271
305
|
].join("\n") + "\n",
|
|
272
306
|
},
|
|
@@ -280,12 +314,16 @@ function licenseText(id, opts = {}) {
|
|
|
280
314
|
}
|
|
281
315
|
|
|
282
316
|
class ValidationError extends Error {
|
|
283
|
-
constructor(message) {
|
|
317
|
+
constructor(message) {
|
|
318
|
+
super(message);
|
|
319
|
+
this.name = "ValidationError";
|
|
320
|
+
}
|
|
284
321
|
}
|
|
285
322
|
|
|
286
323
|
function validateMetadata(md, checks = {}) {
|
|
287
324
|
if (!md || typeof md !== "object") throw new ValidationError("metadata must be an object");
|
|
288
|
-
if (md.format !== CHIP0007_FORMAT)
|
|
325
|
+
if (md.format !== CHIP0007_FORMAT)
|
|
326
|
+
throw new ValidationError(`format must be "${CHIP0007_FORMAT}" (got ${JSON.stringify(md.format)})`);
|
|
289
327
|
if (typeof md.name !== "string" || md.name === "") throw new ValidationError("name is required");
|
|
290
328
|
if ("attributes" in md) {
|
|
291
329
|
if (!Array.isArray(md.attributes)) throw new ValidationError("attributes must be an array");
|
|
@@ -297,19 +335,27 @@ function validateMetadata(md, checks = {}) {
|
|
|
297
335
|
}
|
|
298
336
|
if ("collection" in md) {
|
|
299
337
|
const c = md.collection;
|
|
300
|
-
if (typeof c?.id !== "string" || typeof c?.name !== "string")
|
|
338
|
+
if (typeof c?.id !== "string" || typeof c?.name !== "string")
|
|
339
|
+
throw new ValidationError("collection must have a string id and name");
|
|
301
340
|
}
|
|
302
341
|
if (checks.metadata_hash != null) {
|
|
303
342
|
const got = metadataHashHex(md);
|
|
304
|
-
if (got !== String(checks.metadata_hash).toLowerCase())
|
|
343
|
+
if (got !== String(checks.metadata_hash).toLowerCase())
|
|
344
|
+
throw new ValidationError(`metadata_hash mismatch: computed ${got}, expected ${checks.metadata_hash}`);
|
|
305
345
|
}
|
|
306
346
|
if (checks.media?.data_hash != null && checks.media?.data_bytes != null) {
|
|
307
347
|
const got = sha256Hex(checks.media.data_bytes);
|
|
308
|
-
if (got !== String(checks.media.data_hash).toLowerCase())
|
|
348
|
+
if (got !== String(checks.media.data_hash).toLowerCase())
|
|
349
|
+
throw new ValidationError(
|
|
350
|
+
`data_hash mismatch: image bytes hash to ${got}, expected ${checks.media.data_hash}`,
|
|
351
|
+
);
|
|
309
352
|
}
|
|
310
353
|
if (checks.license?.license_hash != null && checks.license?.license_bytes != null) {
|
|
311
354
|
const got = sha256Hex(checks.license.license_bytes);
|
|
312
|
-
if (got !== String(checks.license.license_hash).toLowerCase())
|
|
355
|
+
if (got !== String(checks.license.license_hash).toLowerCase())
|
|
356
|
+
throw new ValidationError(
|
|
357
|
+
`license_hash mismatch: license bytes hash to ${got}, expected ${checks.license.license_hash}`,
|
|
358
|
+
);
|
|
313
359
|
}
|
|
314
360
|
}
|
|
315
361
|
|
|
@@ -332,7 +378,8 @@ function listImages(root) {
|
|
|
332
378
|
const files = readdirSync(dir)
|
|
333
379
|
.filter((f) => statSync(join(dir, f)).isFile() && IMAGE_EXT.has(extname(f).toLowerCase()))
|
|
334
380
|
.sort();
|
|
335
|
-
if (files.length === 0)
|
|
381
|
+
if (files.length === 0)
|
|
382
|
+
throw new Error("no images found in images/ — add your item art (png/jpg/gif/webp/svg) first.");
|
|
336
383
|
return files;
|
|
337
384
|
}
|
|
338
385
|
|
|
@@ -358,7 +405,8 @@ function readGeneratedLicense(root, collection, storeId = PLACEHOLDER_STORE_ID)
|
|
|
358
405
|
}
|
|
359
406
|
|
|
360
407
|
function generateMetadata(root, opts = {}) {
|
|
361
|
-
|
|
408
|
+
// Empty/whitespace storeId falls back to the placeholder — never an empty-store-id URI. (#1065)
|
|
409
|
+
const storeId = opts.storeId && String(opts.storeId).trim() ? opts.storeId : PLACEHOLDER_STORE_ID;
|
|
362
410
|
const collection = loadCollection(root);
|
|
363
411
|
const imageFiles = listImages(root);
|
|
364
412
|
const items = resolveItems(root, imageFiles);
|
|
@@ -386,9 +434,12 @@ function generateMetadata(root, opts = {}) {
|
|
|
386
434
|
description: item.description || undefined,
|
|
387
435
|
attributes: md.attributes || [],
|
|
388
436
|
media: {
|
|
389
|
-
data_uris: data.uris,
|
|
390
|
-
|
|
391
|
-
|
|
437
|
+
data_uris: data.uris,
|
|
438
|
+
data_hash: dataHash,
|
|
439
|
+
metadata_uris: meta.uris,
|
|
440
|
+
metadata_hash: metadataHash,
|
|
441
|
+
license_uris: license ? license.uris : [],
|
|
442
|
+
license_hash: license ? license.hash : null,
|
|
392
443
|
},
|
|
393
444
|
});
|
|
394
445
|
}
|
|
@@ -401,7 +452,8 @@ function generateLicense(root) {
|
|
|
401
452
|
const collection = loadCollection(root);
|
|
402
453
|
const id = collection?.license;
|
|
403
454
|
if (!id) throw new Error('collection.json has no `license` field — set one (e.g. "cc0").');
|
|
404
|
-
if (!LICENSES[id])
|
|
455
|
+
if (!LICENSES[id])
|
|
456
|
+
throw new Error(`Unknown license "${id}". Available: ${Object.keys(LICENSES).join(", ")}.`);
|
|
405
457
|
const holder = collection.creator || collection.name || "the creator";
|
|
406
458
|
const text = licenseText(id, { holder, year: new Date().getFullYear() });
|
|
407
459
|
const licensesDir = join(root, "licenses");
|
|
@@ -422,14 +474,20 @@ function validateProject(root) {
|
|
|
422
474
|
const manifestPath = join(root, "items.json");
|
|
423
475
|
if (!existsSync(manifestPath)) throw new Error("items.json not found — run the metadata generator first.");
|
|
424
476
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
425
|
-
if (!Array.isArray(manifest) || manifest.length === 0)
|
|
477
|
+
if (!Array.isArray(manifest) || manifest.length === 0)
|
|
478
|
+
throw new Error("items.json is empty — nothing to validate.");
|
|
426
479
|
const metadataDir = join(root, "metadata");
|
|
427
|
-
const metaFiles = existsSync(metadataDir)
|
|
480
|
+
const metaFiles = existsSync(metadataDir)
|
|
481
|
+
? readdirSync(metadataDir)
|
|
482
|
+
.filter((f) => f.endsWith(".json"))
|
|
483
|
+
.sort()
|
|
484
|
+
: [];
|
|
428
485
|
for (const f of metaFiles) {
|
|
429
486
|
const raw = readFileSync(join(metadataDir, f), "utf8");
|
|
430
487
|
const md = JSON.parse(raw);
|
|
431
488
|
validateMetadata(md);
|
|
432
|
-
if (raw.trim() !== canonicalJson(md))
|
|
489
|
+
if (raw.trim() !== canonicalJson(md))
|
|
490
|
+
throw new Error(`metadata/${f} is not in canonical form (re-run the metadata generator).`);
|
|
433
491
|
}
|
|
434
492
|
let checked = 0;
|
|
435
493
|
for (const item of manifest) {
|
|
@@ -437,7 +495,8 @@ function validateProject(root) {
|
|
|
437
495
|
const md = buildChip0007Metadata({ name: item.name, attributes: item.attributes });
|
|
438
496
|
if (media.metadata_hash != null) {
|
|
439
497
|
const matches = metaFiles.some(
|
|
440
|
-
(f) =>
|
|
498
|
+
(f) =>
|
|
499
|
+
metadataHashHex(JSON.parse(readFileSync(join(metadataDir, f), "utf8"))) === media.metadata_hash,
|
|
441
500
|
);
|
|
442
501
|
if (!matches && metadataHashHex(md) !== media.metadata_hash) {
|
|
443
502
|
throw new Error(`item "${item.name}": metadata_hash does not match any metadata/*.json`);
|
|
@@ -453,7 +512,9 @@ function validateProject(root) {
|
|
|
453
512
|
if (licFile && media.license_hash != null) {
|
|
454
513
|
const licPath = join(root, licFile);
|
|
455
514
|
if (!existsSync(licPath)) throw new Error(`item "${item.name}": license not found at ${licFile}`);
|
|
456
|
-
validateMetadata(md, {
|
|
515
|
+
validateMetadata(md, {
|
|
516
|
+
license: { license_hash: media.license_hash, license_bytes: readFileSync(licPath) },
|
|
517
|
+
});
|
|
457
518
|
}
|
|
458
519
|
checked++;
|
|
459
520
|
}
|
|
@@ -471,18 +532,34 @@ function flagValue(args, name) {
|
|
|
471
532
|
return undefined;
|
|
472
533
|
}
|
|
473
534
|
|
|
535
|
+
/** True if `name` appears in the argv slice as a flag (with or without a value). */
|
|
536
|
+
function flagPresent(args, name) {
|
|
537
|
+
return args.some((a) => a === name || a.startsWith(`${name}=`));
|
|
538
|
+
}
|
|
539
|
+
|
|
474
540
|
function main() {
|
|
475
541
|
const cmd = process.argv[2];
|
|
476
542
|
const args = process.argv.slice(3);
|
|
477
543
|
try {
|
|
478
544
|
if (cmd === "metadata") {
|
|
479
545
|
const storeId = flagValue(args, "--store-id");
|
|
546
|
+
if (flagPresent(args, "--store-id") && !(storeId && storeId.trim())) {
|
|
547
|
+
console.error(
|
|
548
|
+
"Error: --store-id was given but empty. Omit it for the pre-publish placeholder pass, " +
|
|
549
|
+
"or pass the real store id from `digstore deploy` (e.g. --store-id <id>).",
|
|
550
|
+
);
|
|
551
|
+
process.exit(2);
|
|
552
|
+
}
|
|
480
553
|
const r = generateMetadata(ROOT, storeId ? { storeId } : {});
|
|
481
554
|
console.log(`Generated ${r.count} CHIP-0007 metadata file(s) → metadata/ and the items.json manifest.`);
|
|
482
555
|
if (storeId) {
|
|
483
|
-
console.log(
|
|
556
|
+
console.log(
|
|
557
|
+
`Baked store id ${storeId} into every URI. Next: \`npm run validate\`, then \`digstore collection mint --collection collection.json --manifest items.json\`.`,
|
|
558
|
+
);
|
|
484
559
|
} else {
|
|
485
|
-
console.log(
|
|
560
|
+
console.log(
|
|
561
|
+
"Used the placeholder store id. After `digstore deploy` gives you the real store id, re-run `npm run generate:metadata -- --store-id <id>` to bake it in — otherwise the minted NFT keeps the placeholder. Then `npm run validate` and `digstore collection mint`.",
|
|
562
|
+
);
|
|
486
563
|
}
|
|
487
564
|
} else if (cmd === "license") {
|
|
488
565
|
const r = generateLicense(ROOT);
|
|
@@ -490,11 +567,17 @@ function main() {
|
|
|
490
567
|
console.log("Re-run `npm run generate:metadata` to wire the license URI + hash into items.json.");
|
|
491
568
|
} else if (cmd === "validate") {
|
|
492
569
|
const r = validateProject(ROOT);
|
|
493
|
-
console.log(
|
|
570
|
+
console.log(
|
|
571
|
+
`OK — ${r.checked} item(s) valid: CHIP-0007 schema + data/metadata/license hashes agree with the real bytes.`,
|
|
572
|
+
);
|
|
494
573
|
} else {
|
|
495
574
|
console.error("Usage: node scripts/dig-nft.mjs <metadata|license|validate> [--store-id <id>]");
|
|
496
|
-
console.error(
|
|
497
|
-
|
|
575
|
+
console.error(
|
|
576
|
+
" metadata generate CHIP-0007 metadata/*.json + items.json from images/ + collection.json",
|
|
577
|
+
);
|
|
578
|
+
console.error(
|
|
579
|
+
" --store-id <id> bake the REAL published store id into URIs (after `digstore deploy`)",
|
|
580
|
+
);
|
|
498
581
|
console.error(" license write the license chosen in collection.json into licenses/");
|
|
499
582
|
console.error(" validate re-verify schema + URI/hash agreement before minting");
|
|
500
583
|
process.exit(2);
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
--error: #dc2626;
|
|
7
7
|
|
|
8
8
|
/* DIG brand accent — violet -> magenta (SYSTEM.md visual theme; matches dig.net/hub/status). */
|
|
9
|
-
--dig-violet: #
|
|
10
|
-
--dig-magenta: #
|
|
9
|
+
--dig-violet: #5800d6;
|
|
10
|
+
--dig-magenta: #ff00de;
|
|
11
11
|
--accent: var(--dig-violet);
|
|
12
|
-
--grad-brand: linear-gradient(115deg, #
|
|
12
|
+
--grad-brand: linear-gradient(115deg, #5800d6 0%, #ff00de 100%);
|
|
13
13
|
|
|
14
14
|
/* DIG brand fonts — named first, system stack as the offline/no-fetch fallback so a published
|
|
15
15
|
capsule never depends on an external font host (keeps the collection page self-contained). */
|
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
--dig-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
18
18
|
}
|
|
19
19
|
@media (prefers-color-scheme: dark) {
|
|
20
|
-
:root {
|
|
20
|
+
:root {
|
|
21
|
+
--fg: #e2e8f0;
|
|
22
|
+
--muted: #94a3b8;
|
|
23
|
+
--bg: #0b1120;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
21
28
|
}
|
|
22
|
-
* { box-sizing: border-box; }
|
|
23
29
|
body {
|
|
24
30
|
margin: 0;
|
|
25
31
|
min-height: 100vh;
|
|
@@ -27,8 +33,17 @@ body {
|
|
|
27
33
|
color: var(--fg);
|
|
28
34
|
background: var(--bg);
|
|
29
35
|
}
|
|
30
|
-
.banner {
|
|
31
|
-
|
|
36
|
+
.banner {
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-height: 320px;
|
|
39
|
+
object-fit: cover;
|
|
40
|
+
display: block;
|
|
41
|
+
}
|
|
42
|
+
.wrap {
|
|
43
|
+
max-width: 64rem;
|
|
44
|
+
margin: 0 auto;
|
|
45
|
+
padding: 2rem;
|
|
46
|
+
}
|
|
32
47
|
h1 {
|
|
33
48
|
font-size: 2.25rem;
|
|
34
49
|
margin: 0 0 0.5rem;
|
|
@@ -38,8 +53,12 @@ h1 {
|
|
|
38
53
|
background-clip: text;
|
|
39
54
|
color: transparent;
|
|
40
55
|
}
|
|
41
|
-
.muted {
|
|
42
|
-
|
|
56
|
+
.muted {
|
|
57
|
+
color: var(--muted);
|
|
58
|
+
}
|
|
59
|
+
.error {
|
|
60
|
+
color: var(--error);
|
|
61
|
+
}
|
|
43
62
|
button {
|
|
44
63
|
font: inherit;
|
|
45
64
|
padding: 0.6rem 1.1rem;
|
|
@@ -49,11 +68,31 @@ button {
|
|
|
49
68
|
color: white;
|
|
50
69
|
cursor: pointer;
|
|
51
70
|
}
|
|
52
|
-
.grid {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
.grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
margin-top: 1.5rem;
|
|
76
|
+
}
|
|
77
|
+
.item {
|
|
78
|
+
border-radius: 0.75rem;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
background: rgba(127, 127, 127, 0.12);
|
|
81
|
+
}
|
|
82
|
+
.item img {
|
|
83
|
+
width: 100%;
|
|
84
|
+
aspect-ratio: 1;
|
|
85
|
+
object-fit: cover;
|
|
86
|
+
display: block;
|
|
87
|
+
}
|
|
88
|
+
.item .label {
|
|
89
|
+
padding: 0.5rem 0.75rem;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
}
|
|
92
|
+
code {
|
|
93
|
+
font-family: var(--dig-mono);
|
|
94
|
+
word-break: break-all;
|
|
95
|
+
}
|
|
57
96
|
pre {
|
|
58
97
|
font-family: var(--dig-mono);
|
|
59
98
|
margin-top: 1.5rem;
|
|
@@ -44,13 +44,13 @@ export default function App() {
|
|
|
44
44
|
<main className="wrap">
|
|
45
45
|
<h1>__DISPLAY_NAME__</h1>
|
|
46
46
|
<p>
|
|
47
|
-
An NFT drop on the <strong>DIG Network</strong> — mint NFTs whose media can live in a DIG
|
|
48
|
-
|
|
47
|
+
An NFT drop on the <strong>DIG Network</strong> — mint NFTs whose media can live in a DIG capsule,
|
|
48
|
+
truly permanent on Chia.
|
|
49
49
|
</p>
|
|
50
50
|
<p className="muted">
|
|
51
|
-
Building and previewing this mint page are <strong>free</strong> — no mint, no chain, no spend
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
Building and previewing this mint page are <strong>free</strong> — no mint, no chain, no spend when
|
|
52
|
+
you scaffold or run it. Publishing the page is one <code>digstore deploy</code> ($DIG). Minting the
|
|
53
|
+
NFTs is a separate, wallet-signed on-chain action a visitor triggers.
|
|
54
54
|
</p>
|
|
55
55
|
|
|
56
56
|
{address ? (
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
--error: #dc2626;
|
|
7
7
|
|
|
8
8
|
/* DIG brand accent — violet -> magenta (SYSTEM.md visual theme; matches dig.net/hub/status). */
|
|
9
|
-
--dig-violet: #
|
|
10
|
-
--dig-magenta: #
|
|
9
|
+
--dig-violet: #5800d6;
|
|
10
|
+
--dig-magenta: #ff00de;
|
|
11
11
|
--accent: var(--dig-violet);
|
|
12
|
-
--grad-brand: linear-gradient(115deg, #
|
|
12
|
+
--grad-brand: linear-gradient(115deg, #5800d6 0%, #ff00de 100%);
|
|
13
13
|
|
|
14
14
|
/* DIG brand fonts — named first, system stack as the offline/no-fetch fallback so a scaffolded
|
|
15
15
|
capsule never depends on an external font host (keeps the site self-contained + offline-safe). */
|
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
--dig-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
18
18
|
}
|
|
19
19
|
@media (prefers-color-scheme: dark) {
|
|
20
|
-
:root {
|
|
20
|
+
:root {
|
|
21
|
+
--fg: #e2e8f0;
|
|
22
|
+
--muted: #94a3b8;
|
|
23
|
+
--bg: #0b1120;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
21
28
|
}
|
|
22
|
-
* { box-sizing: border-box; }
|
|
23
29
|
body {
|
|
24
30
|
margin: 0;
|
|
25
31
|
min-height: 100vh;
|
|
@@ -29,7 +35,10 @@ body {
|
|
|
29
35
|
color: var(--fg);
|
|
30
36
|
background: var(--bg);
|
|
31
37
|
}
|
|
32
|
-
.wrap {
|
|
38
|
+
.wrap {
|
|
39
|
+
max-width: 42rem;
|
|
40
|
+
padding: 2rem;
|
|
41
|
+
}
|
|
33
42
|
h1 {
|
|
34
43
|
font-size: 2.25rem;
|
|
35
44
|
margin: 0 0 0.5rem;
|
|
@@ -39,8 +48,12 @@ h1 {
|
|
|
39
48
|
background-clip: text;
|
|
40
49
|
color: transparent;
|
|
41
50
|
}
|
|
42
|
-
.muted {
|
|
43
|
-
|
|
51
|
+
.muted {
|
|
52
|
+
color: var(--muted);
|
|
53
|
+
}
|
|
54
|
+
.error {
|
|
55
|
+
color: var(--error);
|
|
56
|
+
}
|
|
44
57
|
button {
|
|
45
58
|
font: inherit;
|
|
46
59
|
padding: 0.6rem 1.1rem;
|
|
@@ -58,8 +71,16 @@ button {
|
|
|
58
71
|
display: grid;
|
|
59
72
|
gap: 0.5rem;
|
|
60
73
|
}
|
|
61
|
-
.row {
|
|
62
|
-
|
|
74
|
+
.row {
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: 0.75rem;
|
|
77
|
+
align-items: baseline;
|
|
78
|
+
flex-wrap: wrap;
|
|
79
|
+
}
|
|
80
|
+
code {
|
|
81
|
+
font-family: var(--dig-mono);
|
|
82
|
+
word-break: break-all;
|
|
83
|
+
}
|
|
63
84
|
pre {
|
|
64
85
|
font-family: var(--dig-mono);
|
|
65
86
|
margin-top: 1.5rem;
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<main>
|
|
11
11
|
<h1>__DISPLAY_NAME__</h1>
|
|
12
|
-
<p>
|
|
13
|
-
|
|
14
|
-
Build and preview for <strong>free</strong>. You only spend $DIG when you publish.
|
|
12
|
+
<p>
|
|
13
|
+
A static site on the <strong>DIG Network</strong> — no host can read it, change it, or take it down.
|
|
15
14
|
</p>
|
|
15
|
+
<p class="muted">Build and preview for <strong>free</strong>. You only spend $DIG when you publish.</p>
|
|
16
16
|
<pre><code>digstore dev # preview, free
|
|
17
17
|
digstore deploy # publish a capsule ($DIG)</code></pre>
|
|
18
18
|
</main>
|