@artblocks/abx-cli 0.1.0-alpha.23 → 0.1.0-alpha.26

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 (65) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/dist/commands/deploy.d.ts +27 -4
  3. package/dist/commands/deploy.d.ts.map +1 -1
  4. package/dist/commands/deploy.js +220 -70
  5. package/dist/commands/deploy.js.map +1 -1
  6. package/dist/commands/project.d.ts.map +1 -1
  7. package/dist/commands/project.js +15 -0
  8. package/dist/commands/project.js.map +1 -1
  9. package/dist/commands/scaffold.d.ts.map +1 -1
  10. package/dist/commands/scaffold.js +27 -4
  11. package/dist/commands/scaffold.js.map +1 -1
  12. package/dist/commands/storage.d.ts +14 -0
  13. package/dist/commands/storage.d.ts.map +1 -1
  14. package/dist/commands/storage.js +31 -4
  15. package/dist/commands/storage.js.map +1 -1
  16. package/dist/config.d.ts +13 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +25 -1
  19. package/dist/config.js.map +1 -1
  20. package/dist/conformance.d.ts.map +1 -1
  21. package/dist/conformance.js +20 -1
  22. package/dist/conformance.js.map +1 -1
  23. package/dist/flag-allowlists.d.ts.map +1 -1
  24. package/dist/flag-allowlists.js +4 -0
  25. package/dist/flag-allowlists.js.map +1 -1
  26. package/dist/kind.d.ts +8 -0
  27. package/dist/kind.d.ts.map +1 -1
  28. package/dist/kind.js +17 -0
  29. package/dist/kind.js.map +1 -1
  30. package/dist/main.js +38 -8
  31. package/dist/main.js.map +1 -1
  32. package/dist/output.d.ts.map +1 -1
  33. package/dist/output.js +59 -2
  34. package/dist/output.js.map +1 -1
  35. package/dist/ownerops.d.ts +51 -0
  36. package/dist/ownerops.d.ts.map +1 -1
  37. package/dist/ownerops.js +152 -5
  38. package/dist/ownerops.js.map +1 -1
  39. package/dist/provision.d.ts.map +1 -1
  40. package/dist/provision.js +6 -1
  41. package/dist/provision.js.map +1 -1
  42. package/dist/remote.d.ts +20 -0
  43. package/dist/remote.d.ts.map +1 -1
  44. package/dist/remote.js +38 -0
  45. package/dist/remote.js.map +1 -1
  46. package/dist/riskgate.d.ts +19 -0
  47. package/dist/riskgate.d.ts.map +1 -1
  48. package/dist/riskgate.js +31 -1
  49. package/dist/riskgate.js.map +1 -1
  50. package/dist/schema.js +4 -4
  51. package/dist/schema.js.map +1 -1
  52. package/dist/script-chunks.d.ts +8 -0
  53. package/dist/script-chunks.d.ts.map +1 -0
  54. package/dist/script-chunks.js +35 -0
  55. package/dist/script-chunks.js.map +1 -0
  56. package/dist/signer.d.ts.map +1 -1
  57. package/dist/signer.js +4 -1
  58. package/dist/signer.js.map +1 -1
  59. package/package.json +6 -6
  60. package/skill/SKILL.md +18 -8
  61. package/skill/reference/code-projects.md +3 -3
  62. package/skill/reference/decisions.md +3 -3
  63. package/skill/reference/hosting.md +15 -4
  64. package/skill/reference/operating.md +27 -1
  65. package/skill/reference/setup.md +2 -1
@@ -15,7 +15,7 @@
15
15
  import { createHash } from 'node:crypto';
16
16
  import { readFileSync, readdirSync, statSync } from 'node:fs';
17
17
  import { basename, extname, join as joinPath, resolve as resolvePath } from 'node:path';
18
- import { DEFAULT_CHAIN_KEY, DEP_RESOLUTION, METADATA_FIELD as F, METADATA_REPRESENTATION as R, analyzeScript, assertChainId, checkRegistryDeps, dependencySetupCalls, deployOneOfOne, deploySeries, deployOneOfOneEdition, deployEditionImage, discoverDeployBlock, encodeFieldRenderer, encodeTag, expectedChainComplete, makeHotSender, makePublicClient, makeWalletClient, normalizeAttributes, onchainUriSetupCalls, oneOfOneImageAbi, oneOfOneEditionAbi, parseTraitPairs, planResume, predictClone, probeSeedSource, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, prepareDeployOneOfOneEdition, prepareDeployEditionImage, prepareDeployEditionCode, redactRpcUrl, resolveChain, resolveDepRegistryPointer, resolveGenerator, probeTransferValidator, resolveRecommendedTransferValidator, resolveRpcUrl, resolveSeedSource, resolveSeriesCodeFactory, saltFor, seriesCodeAbi, seriesCodeFactoryAbi, tryReadContract, } from '@artblocks/abx-sdk';
18
+ import { DEFAULT_CHAIN_KEY, DEP_RESOLUTION, METADATA_FIELD as F, contentIdFromLocator, METADATA_REPRESENTATION as R, analyzeScript, assertChainId, checkRegistryDeps, dependencySetupCalls, deployOneOfOne, deploySeries, deployOneOfOneEdition, deployEditionImage, discoverDeployBlock, encodeFieldRenderer, encodeTag, expectedChainComplete, makeHotSender, makePublicClient, makeWalletClient, normalizeAttributes, onchainUriSetupCalls, oneOfOneImageAbi, oneOfOneEditionAbi, parseTraitPairs, planResume, predictClone, probeSeedSource, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, prepareDeployOneOfOneEdition, prepareDeployEditionImage, prepareDeployEditionCode, redactRpcUrl, resolveChain, resolveDepRegistryPointer, resolveGenerator, probeTransferValidator, resolveRecommendedTransferValidator, resolveRpcUrl, resolveSeedSource, resolveSeriesCodeFactory, saltFor, seriesCodeAbi, seriesCodeFactoryAbi, tryReadContract, AbxServiceClient, } from '@artblocks/abx-sdk';
19
19
  import { DIRECT_URL_BACKENDS, contentTypeFromPath, decideImageContentLane, hashContent, isTurboArweave, resolveBackend, validateRenderStorageCombo, } from '@artblocks/abx-storage';
20
20
  import { DEFAULT_PORT, contentHash, generateContent, resolveBaseUrl, startTokenApiServer } from '@artblocks/abx-token-api';
21
21
  import { encodeFunctionData, toHex, zeroAddress } from 'viem';
@@ -24,10 +24,11 @@ import { parseDepFlag } from '../deps.js';
24
24
  import { allowUnreadableOnchain, isDryRun, parseSaltFlag, refuseStrayFlags, warnSignWithoutFor } from '../flags.js';
25
25
  import { isEditionContract } from '../kind.js';
26
26
  import { jsonSafe, withJson } from '../jsonout.js';
27
+ import { planOnChainScript } from '../script-chunks.js';
27
28
  import { bold, c, dim, ensureFactory, ensureRenderer, ensureSeedSource, ensureSeriesCodeFactory, ensureSeriesFactory, ensureOneOfOneEditionFactory, ensureEditionFactory, ensureEditionCodeFactory, g, info, keepAlive, ok, p, portInUse, printServing, registerAndIndexLocally, reindexAfterDeploy, step, warn, } from '../output.js';
28
- import { AUTHORSHIP_DEPLOY_FIELDS, ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, tokenUriGasEstimate, authorshipContractFields, computeContentPlan, envStagingSender, parseCompress, parseSeedSourceValue, canonicalSeedSource, refuseUnusableSeedSource, parseTransferValidatorValue, guardOnChainSize, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, } from '../ownerops.js';
29
+ import { AUTHORSHIP_DEPLOY_FIELDS, ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, tokenUriGasEstimate, authorshipContractFields, gatewayContractFields, servingGateway, computeContentPlan, envStagingSender, parseCompress, parseSeedSourceValue, canonicalSeedSource, refuseUnusableSeedSource, parseTransferValidatorValue, guardOnChainSize, previewImageStaging, refusePrewrappedImage, sessionStagingSender, stageImageField, stageImageFieldsBatch, } from '../ownerops.js';
29
30
  import { canonicalLabel } from '../remote.js';
30
- import { confirmSend, gatedSend, laneFromFlags } from '../riskgate.js';
31
+ import { assertLaneCanSign, confirmSend, gatedSend, laneFromFlags } from '../riskgate.js';
31
32
  import { describeSchema, editionSchemaAdvisory, parseSchemaSpecs } from '../schema.js';
32
33
  import { parseSeriesTraits } from '../series-traits.js';
33
34
  import { decodeOnChainJson } from '../served.js';
@@ -192,6 +193,8 @@ export function deployCommandLine(flags, salt) {
192
193
  str('backend', flags.backend);
193
194
  str('gateway', flags.gateway);
194
195
  str('bucket', flags.bucket);
196
+ str('ipfs-gateway', flags['ipfs-gateway']);
197
+ str('arweave-gateway', flags['arweave-gateway']);
195
198
  str('public-base-url', flags['public-base-url']);
196
199
  if (flags.sign !== undefined)
197
200
  parts.push('--sign');
@@ -223,6 +226,8 @@ export function deploySeriesCommandLine(flags, salt) {
223
226
  str('backend', flags.backend);
224
227
  str('gateway', flags.gateway);
225
228
  str('bucket', flags.bucket);
229
+ str('ipfs-gateway', flags['ipfs-gateway']);
230
+ str('arweave-gateway', flags['arweave-gateway']);
226
231
  str('public-base', flags['public-base']);
227
232
  str('public-base-url', flags['public-base-url']);
228
233
  bool('mint-all');
@@ -278,6 +283,8 @@ export function deployCodeCommandLine(flags, salt) {
278
283
  str('backend', flags.backend);
279
284
  str('gateway', flags.gateway);
280
285
  str('public-base-url', flags['public-base-url']);
286
+ str('ipfs-gateway', flags['ipfs-gateway']);
287
+ str('arweave-gateway', flags['arweave-gateway']);
281
288
  if (flags.sign !== undefined)
282
289
  parts.push('--sign');
283
290
  if (flags.unsigned !== undefined)
@@ -296,11 +303,14 @@ export const imageKeccakField = (value) => ({
296
303
  representation: encodeTag(R.keccak256),
297
304
  value,
298
305
  });
299
- export const imageInlineField = (svg) => ({
300
- field: encodeTag(F.image),
301
- representation: encodeTag(R.inline),
302
- value: toHex(svg),
303
- });
306
+ export const imageInlineField = (svg) => {
307
+ refusePrewrappedImage(svg, 'inline image');
308
+ return {
309
+ field: encodeTag(F.image),
310
+ representation: encodeTag(R.inline),
311
+ value: toHex(svg),
312
+ };
313
+ };
304
314
  /** An `image` field pointing at an off-chain URL (the renderer emits it verbatim). */
305
315
  export const imageUrlField = (url) => ({
306
316
  field: encodeTag(F.image),
@@ -314,6 +324,82 @@ export const imageUrlTemplateField = (template) => ({
314
324
  representation: encodeTag(R.urlTemplate),
315
325
  value: toHex(template),
316
326
  });
327
+ /**
328
+ * An `image` field carrying a CONTENT-ADDRESSED identity — the bare CID / txid, with the serving
329
+ * gateway supplied at read time from the collection's `abx_gateway_*` preference (renderer spec v11).
330
+ *
331
+ * This replaces what `--onchain-uri --backend ipfs|arweave` used to write. That path stored the
332
+ * backend's gateway HTTPS URL as a `url` field, which looked identical in a marketplace and was
333
+ * wrong in two ways that only showed up later: the creator's chosen gateway host was welded into a
334
+ * value they might have locked, so a dead gateway became a dead token rather than a repoint; and
335
+ * `abx_provenance` reported `source: url` for bytes that live on IPFS, so the chain stopped saying
336
+ * where the work actually was. The CID is identity and the gateway is a preference — one field each.
337
+ *
338
+ * `{id}` in the value still substitutes at read (the O(1) directory pattern), so a collection-scope
339
+ * `ipfs` field addresses a whole pinned folder exactly as `url-template` does for a plain CDN.
340
+ */
341
+ export const imageContentAddressedField = (network, contentId) => ({
342
+ field: encodeTag(F.image),
343
+ representation: encodeTag(network === 'ipfs' ? R.ipfs : R.arweave),
344
+ value: toHex(contentId),
345
+ });
346
+ /**
347
+ * The `image` field for a locator a durable backend just produced — ONE decision point, so every
348
+ * lane (1/1, series, both edition twins) writes the same shape for the same backend.
349
+ *
350
+ * `ipfs` / `arweave` store IDENTITY: recover the bare CID/txid from the backend's gateway URL and
351
+ * commit that, leaving the serving prefix to the collection's `abx_gateway_*` preference. Any path
352
+ * suffix rides along, so a directory upload's `<cid>/{id}.png` is still one collection-scope field
353
+ * covering every token — the renderer substitutes `{id}` for `ipfs`/`arweave` exactly as it does for
354
+ * `url-template`.
355
+ *
356
+ * `cloud` and friends keep the plain `url` / `url-template` they always had: an https CDN locator IS
357
+ * the address, with no identity underneath to separate out.
358
+ */
359
+ /**
360
+ * The URL a creator's token will actually carry, for a locator a durable backend just produced.
361
+ *
362
+ * For `ipfs`/`arweave` the backend's locator is an UPLOAD-gateway URL, and the on-chain field holds
363
+ * only the bare CID/txid — so narrating the locator verbatim told the creator a host their token
364
+ * does not use (a live deploy printed `gateway.pinata.cloud/…` while committing a CID served from
365
+ * `ipfs.io`). Rebuild it under the serving preference instead, and say which one that is, since
366
+ * "the floor is filling a silence" and "you chose this" have different futures.
367
+ *
368
+ * Every other backend is returned verbatim: an https CDN locator IS the address.
369
+ */
370
+ function servedImageUrl(backendId, locator, flags) {
371
+ if (backendId !== 'ipfs' && backendId !== 'arweave')
372
+ return { url: locator, note: '' };
373
+ const contentId = contentIdFromLocator(backendId, locator);
374
+ if (!contentId)
375
+ return { url: locator, note: '' };
376
+ const { prefix, chosen } = servingGateway(backendId, flags);
377
+ return {
378
+ url: `${prefix}${contentId}`,
379
+ note: chosen ? `gateway: yours, written on-chain` : `gateway: public default — repoint any time with ${bold('abx set-gateway')}`,
380
+ };
381
+ }
382
+ /** How a lane should describe what it just committed, so the narration matches the field written. */
383
+ function describeLocatorField(backendId) {
384
+ return backendId === 'ipfs' || backendId === 'arweave'
385
+ ? `ON-CHAIN image as a bare ${backendId} id — the gateway is the collection's preference, repointable with ${bold('abx set-gateway')}`
386
+ : 'ON-CHAIN image url (the locator IS the address)';
387
+ }
388
+ function imageLocatorField(backendId, locator, isTemplate = false) {
389
+ const network = backendId === 'ipfs' || backendId === 'arweave' ? backendId : null;
390
+ if (network) {
391
+ const contentId = contentIdFromLocator(network, locator);
392
+ if (contentId)
393
+ return imageContentAddressedField(network, contentId);
394
+ // A subdomain-style gateway (`https://<cid>.ipfs.dweb.link`), which ABX's own backends never
395
+ // produce. Degrade to the old shape rather than guess at the CID — and say so, because the
396
+ // consequence (a welded host) is exactly what the caller thought they were avoiding.
397
+ warn(`could not recover the ${network} content id from ${locator} — writing the gateway URL as a plain ` +
398
+ `'url' field instead. That URL's host is then part of the on-chain value: a gateway change ` +
399
+ `needs ${bold('abx set-field')}, not ${bold('abx set-gateway')}.`);
400
+ }
401
+ return isTemplate ? imageUrlTemplateField(locator) : imageUrlField(locator);
402
+ }
317
403
  export const looksLikeSvg = (s) => /^\s*<(\?xml|svg)/i.test(s);
318
404
  /**
319
405
  * Does the IMAGE itself end up on-chain for this deploy? Two ways it can:
@@ -359,7 +445,8 @@ export function imageEndsUpOnChain(flags, onchainImage) {
359
445
  export async function prepareContent(imagePath, clone, overrides, store = true, // false for --dry-run: compute the hash but don't custody bytes
360
446
  onChain = false, // true (--onchain-uri): put the image ON-CHAIN (inline SVG) so it self-resolves
361
447
  remoteEth, // wallet-lane Turbo uploads: sign+pay with the connected browser wallet
362
- walletAddr) {
448
+ walletAddr, // the deployer wallet — checked for existing Turbo credits if the managed key is short
449
+ gatewayFlags = {}) {
363
450
  if (!imagePath) {
364
451
  // demo default = generative SVG. On-chain mode inlines it (self-resolving, no custody);
365
452
  // off-chain mode commits its keccak and regenerates/serves it.
@@ -413,8 +500,8 @@ walletAddr) {
413
500
  const backendId = backendResolution(overrides).backend;
414
501
  if (onchainUrlLane) {
415
502
  return {
416
- tokenFields: [imageUrlField(`<${backendId}-url>`)],
417
- contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) → would upload to ${backendId}; its public URL becomes the ON-CHAIN image field (no server)`,
503
+ tokenFields: [imageLocatorField(backendId, `<${backendId}-url>`)],
504
+ contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) → would upload to ${backendId}; ${describeLocatorField(backendId)}`,
418
505
  };
419
506
  }
420
507
  return {
@@ -440,10 +527,13 @@ walletAddr) {
440
527
  // Keep the bytes under their hash too: `abx verify` can then still prove the served image
441
528
  // matches what was uploaded, even though the on-chain field addresses it by URL.
442
529
  await backend.put(hash, { bytes, contentType });
443
- ok(`image ${url} ${dim(`(${backend.id}; baked on-chain, no server)`)}`);
530
+ const served = servedImageUrl(backend.id, url, gatewayFlags);
531
+ ok(`image → ${served.url} ${dim(`(${backend.id}; baked on-chain, no server)`)}`);
532
+ if (served.note)
533
+ info(dim(` ${served.note}`));
444
534
  return {
445
- tokenFields: [imageUrlField(url)],
446
- contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) on ${backend.id} · ON-CHAIN image url ${url}`,
535
+ tokenFields: [imageLocatorField(backend.id, url)],
536
+ contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) on ${backend.id} · ON-CHAIN image ${describeLocatorField(backend.id)}`,
447
537
  };
448
538
  }
449
539
  warn(`'${backend.id}' cannot upload a directory, so no public image URL could be baked — falling back to custody (the on-chain image will be a placeholder).`);
@@ -475,16 +565,24 @@ export const attributesInlineField = (attrs) => ({
475
565
  representation: encodeTag(R.inline),
476
566
  value: toHex(JSON.stringify(attrs)),
477
567
  });
478
- /** Warn when IPFS/Arweave custody is pointed at a LOCAL gateway: the served image URL
479
- * (`<gateway>/ipfs/<cid>`) would resolve only on this machine, so marketplaces/wallets can't
480
- * load it — a pinning service + a PUBLIC gateway is part of operating off-chain custody. */
568
+ /**
569
+ * Warn when IPFS/Arweave custody is pointed at a LOCAL gateway.
570
+ *
571
+ * Two distinct problems ride on one flag, and the warning names the one that actually applies. The
572
+ * bytes are pinned only to a node on this machine, so nothing off it can retrieve them — that is the
573
+ * real damage and it survives any later `set-gateway`. It ALSO used to weld that localhost host into
574
+ * the on-chain value; since the field holds a bare CID it no longer does (the serving prefix is the
575
+ * collection's preference), so promising "the served URL resolves only on THIS machine" would now be
576
+ * false and would point the creator at the wrong repair.
577
+ */
481
578
  export function localGatewayWarning(flags) {
482
579
  const opts = storageOptions(storageOverrides(flags));
483
580
  const gateway = opts.backend === 'ipfs' ? opts.ipfs?.gateway : opts.backend === 'arweave' ? opts.arweave?.gateway : undefined;
484
581
  if (gateway && /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)\b/i.test(gateway)) {
485
- return (`${opts.backend} gateway is ${gateway} — a LOCAL gateway. The served image URL resolves only on THIS machine; ` +
486
- `marketplaces and wallets won't load it. Pin to a service with a PUBLIC gateway (e.g. a Pinata dedicated gateway) ` +
487
- `via --gateway https://… or ABX_IPFS_GATEWAY.`);
582
+ return (`${opts.backend} gateway is ${gateway} — a LOCAL gateway, so the bytes would be pinned only to a node on THIS ` +
583
+ `machine and nobody else could retrieve them. The on-chain field holds the bare content id (a gateway change is ` +
584
+ `later just ${bold('abx set-gateway')}), but no gateway can serve content that was never pinned publicly. Pin to a ` +
585
+ `service with a PUBLIC gateway (e.g. a Pinata dedicated gateway) via --gateway https://… or ABX_IPFS_GATEWAY.`);
488
586
  }
489
587
  return null;
490
588
  }
@@ -815,6 +913,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
815
913
  // explicit --salt is given. With the wallet lane the signer is unknown until they
816
914
  // connect, so this runs per-signer.
817
915
  const buildForDeployer = async (deployer) => {
916
+ assertLaneCanSign(flags); // no upload before we know this run can be signed — see `assertLaneCanSign`
818
917
  const salt = explicitSalt ?? saltFor(deployer);
819
918
  const clone = await predictClone(publicClient, { factory, salt });
820
919
  // A baked on-chain image (--onchain-image) replaces prepareContent's image field —
@@ -825,7 +924,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
825
924
  ? { tokenFields: [bakedImage], contentNote: 'content: image staged ON-CHAIN via reader (self-resolving, no custody)' }
826
925
  : onchainImage
827
926
  ? { tokenFields: [], contentNote: 'content: image would be staged ON-CHAIN via reader (chunk store) — dry run skips the staging write' }
828
- : await prepareContent(flags.image, clone, storageOverrides(flags), !dryRun, onChainUri, uploadRemoteEth, deployer);
927
+ : await prepareContent(flags.image, clone, storageOverrides(flags), !dryRun, onChainUri, uploadRemoteEth, deployer, flags);
829
928
  // Put the description ON-CHAIN (inline) when asked, or always under --onchain-uri
830
929
  // (the on-chain renderer can only emit on-chain fields, so an off-chain-only
831
930
  // description would simply be omitted). On-chain wins at resolve either way.
@@ -853,7 +952,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
853
952
  transferValidator,
854
953
  tokenFields,
855
954
  // authorship + rights (creator / license / …) are collection-scope on-chain fields.
856
- contractFields: authorshipContractFields(flags),
955
+ contractFields: [...authorshipContractFields(flags), ...gatewayContractFields(flags)],
857
956
  };
858
957
  return { clone, params, salt, contentNote };
859
958
  };
@@ -1319,13 +1418,14 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1319
1418
  info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
1320
1419
  }
1321
1420
  const buildForDeployer = async (deployer) => {
1421
+ assertLaneCanSign(flags); // no upload before we know this run can be signed — see `assertLaneCanSign`
1322
1422
  const salt = explicitSalt ?? saltFor(deployer);
1323
1423
  const clone = await predictClone(publicClient, { factory, salt });
1324
1424
  const { tokenFields, contentNote } = bakedImage
1325
1425
  ? { tokenFields: [bakedImage], contentNote: 'content: image staged ON-CHAIN via reader (self-resolving, no custody)' }
1326
1426
  : onchainImage
1327
1427
  ? { tokenFields: [], contentNote: 'content: image would be staged ON-CHAIN via reader (chunk store) — dry run skips the staging write' }
1328
- : await prepareContent(flags.image, clone, storageOverrides(flags), !dryRun, onChainUri);
1428
+ : await prepareContent(flags.image, clone, storageOverrides(flags), !dryRun, onChainUri, undefined, undefined, flags);
1329
1429
  if (flags.description && (onChainUri || flags['description-onchain'])) {
1330
1430
  tokenFields.push({ field: encodeTag(F.description), representation: encodeTag(R.inline), value: toHex(flags.description) });
1331
1431
  }
@@ -1349,7 +1449,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1349
1449
  minter,
1350
1450
  paused,
1351
1451
  tokenFields,
1352
- contractFields: authorshipContractFields(flags),
1452
+ contractFields: [...authorshipContractFields(flags), ...gatewayContractFields(flags)],
1353
1453
  };
1354
1454
  return { clone, params, salt, contentNote };
1355
1455
  };
@@ -1659,7 +1759,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1659
1759
  // Image custody × URI resolution (see reference/hosting.md):
1660
1760
  // - --onchain-image → chunk store (on-chain bytes), JSON on-chain
1661
1761
  // - --onchain-uri + a durable backend → image OFF-CHAIN (ipfs/arweave), JSON on-chain via the
1662
- // (ipfs/arweave) renderer: ONE collection `url-template` when files share
1762
+ // (ipfs/arweave) renderer: ONE collection-scope image field when files share
1663
1763
  // an extension (O(1) directory), else per-token `url` (O(N))
1664
1764
  // - --onchain-uri alone → inline SVG on-chain
1665
1765
  // - neither → off-chain custody (keccak), served by a hosted resolver
@@ -1694,6 +1794,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1694
1794
  // early managed-key creation + funds guard and the up-front buildFields; the session branch runs them.
1695
1795
  const remoteEthUpload = lane === 'sign' && !onchainImage && !!backend && storageSignerChoice(overrides) === 'eth' && isTurboArweave(opts);
1696
1796
  if (backend && !remoteEthUpload) {
1797
+ assertLaneCanSign(flags); // no upload before we know this run can be signed — see `assertLaneCanSign`
1697
1798
  ensureArweaveIdentityForUpload(opts); // Turbo identity, created on first upload
1698
1799
  // The deployer wallet (--for, else the .env key's address) — checked for existing Turbo credits
1699
1800
  // if the managed key is short, so we recommend `--storage-signer eth` before any top-up.
@@ -1733,23 +1834,35 @@ export async function cmdDeploySeriesBody(flags, emit) {
1733
1834
  const uniform = exts[0] !== '' && exts.every((e) => e === exts[0]);
1734
1835
  if (dryRun || !backend) {
1735
1836
  if (uniform) {
1736
- contractFields = [imageUrlTemplateField(`<${backendId}-dir>/{id}${exts[0]}`)];
1737
- info(`would upload ${tokenPaths.length} file(s) as ONE ${backendId} directory → collection image url-template <${backendId}-dir>/{id}${exts[0]} (O(1))`);
1837
+ contractFields = [imageLocatorField(backendId, `<${backendId}-dir>/{id}${exts[0]}`, true)];
1838
+ info(`would upload ${tokenPaths.length} file(s) as ONE ${backendId} directory → collection image <${backendId}-dir>/{id}${exts[0]} (O(1); ${describeLocatorField(backendId)})`);
1839
+ // Name the SERVING prefix in the plan too. Without it a preview says "the gateway is the
1840
+ // collection's preference" and never says what that preference resolves to, so a reader
1841
+ // fills the blank with the upload gateway they just typed — the same confusion the live
1842
+ // narration used to cause outright.
1843
+ if (backendId === 'ipfs' || backendId === 'arweave') {
1844
+ const { prefix, chosen } = servingGateway(backendId, flags);
1845
+ info(`served from ${bold(prefix)} ${dim(chosen ? '(your --' + backendId + '-gateway, written on-chain)' : `(public default — nothing written; repoint any time with ${bold('abx set-gateway')})`)}`);
1846
+ }
1738
1847
  }
1739
1848
  else {
1740
- tokenFields = tokenPaths.map(({ tokenId }) => tokenFieldOf(tokenId, imageUrlField(`<${backendId}-url-${tokenId}>`)));
1849
+ tokenFields = tokenPaths.map(({ tokenId }) => tokenFieldOf(tokenId, imageLocatorField(backendId, `<${backendId}-url-${tokenId}>`)));
1741
1850
  info(`would upload ${tokenPaths.length} file(s) to ${backendId} → per-token url image fields (mixed extensions, O(N))`);
1742
1851
  }
1743
1852
  return;
1744
1853
  }
1745
1854
  if (uniform && backend.putDirectory) {
1746
- // O(1) directory-base: upload the folder renamed to {id}{ext}; ONE collection url-template.
1855
+ // O(1) directory-base: upload the folder renamed to {id}{ext}; ONE collection-scope image
1856
+ // field. `{id}` substitutes at read on every locator representation, `ipfs`/`arweave` included.
1747
1857
  const entries = tokenPaths.map(({ tokenId, path }) => ({ name: `${tokenId}${exts[0]}`, bytes: new Uint8Array(readFileSync(path)), contentType: contentTypeFromPath(path) }));
1748
1858
  const { base } = await backend.putDirectory(entries);
1749
1859
  const template = `${base}/{id}${exts[0]}`;
1750
- contractFields = [imageUrlTemplateField(template)];
1860
+ contractFields = [imageLocatorField(backendId, template, true)];
1751
1861
  info(`uploaded ${entries.length} file(s) as one ${backendId} directory`);
1752
- info(`collection image (url-template): ${template} ${dim('— one field covers every token (O(1))')}`);
1862
+ const servedTemplate = servedImageUrl(backendId, template, flags);
1863
+ info(`collection image: ${servedTemplate.url} ${dim(`— one field covers every token (O(1); ${describeLocatorField(backendId)})`)}`);
1864
+ if (servedTemplate.note)
1865
+ info(dim(` ${servedTemplate.note}`));
1753
1866
  }
1754
1867
  else {
1755
1868
  // mixed extensions (or no directory support): per-token url fields (O(N)).
@@ -1760,10 +1873,10 @@ export async function cmdDeploySeriesBody(flags, emit) {
1760
1873
  const locator = (await backend.locator?.(hash)) ?? '';
1761
1874
  if (!locator)
1762
1875
  throw new Error(`backend ${backendId} returned no public locator for token ${tokenId} — a durable backend (ipfs/arweave) is required for --onchain-uri image hosting`);
1763
- tokenFields.push(tokenFieldOf(tokenId, imageUrlField(locator)));
1764
- info(`token ${tokenId} ← ${fname} → ${locator}`);
1876
+ tokenFields.push(tokenFieldOf(tokenId, imageLocatorField(backendId, locator)));
1877
+ info(`token ${tokenId} ← ${fname} → ${servedImageUrl(backendId, locator, flags).url}`);
1765
1878
  }
1766
- info(`mixed file extensions → per-token url fields (O(N)); a uniform extension enables the O(1) directory template`);
1879
+ info(`mixed file extensions → per-token image fields (O(N)); a uniform extension enables the O(1) directory template`);
1767
1880
  }
1768
1881
  return;
1769
1882
  }
@@ -1819,6 +1932,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1819
1932
  // Assemble the init params for a given deployer (owner/royalty/mintTo = deployer). The clone
1820
1933
  // address is deterministic in (factory, salt) — so URIs are baked before the tx is signed.
1821
1934
  const buildForDeployer = async (deployer) => {
1935
+ assertLaneCanSign(flags); // no upload before we know this run can be signed — see `assertLaneCanSign`
1822
1936
  const salt = explicitSalt ?? saltFor(deployer);
1823
1937
  const clone = await predictClone(publicClient, { factory, salt });
1824
1938
  const params = {
@@ -1839,8 +1953,8 @@ export async function cmdDeploySeriesBody(flags, emit) {
1839
1953
  mintTo: mintCount > 0 ? deployer : zeroAddress,
1840
1954
  mintCount,
1841
1955
  tokenFields,
1842
- // image contract fields (the O(1) url-template) + authorship/rights (creator / license / …).
1843
- contractFields: [...contractFields, ...authorshipContractFields(flags)],
1956
+ // image contract fields (the O(1) collection-scope image) + authorship/rights (creator / license / …).
1957
+ contractFields: [...contractFields, ...authorshipContractFields(flags), ...gatewayContractFields(flags)],
1844
1958
  };
1845
1959
  return { clone, params, salt };
1846
1960
  };
@@ -1877,7 +1991,9 @@ export async function cmdDeploySeriesBody(flags, emit) {
1877
1991
  info(onchainImage
1878
1992
  ? 'resolution: ON-CHAIN via the renderer (chunked reader per token — image bytes on-chain)'
1879
1993
  : offchainImageOnchainJson
1880
- ? `resolution: ON-CHAIN via the renderer; image OFF-CHAIN on ${backendId} (${contractFields.length ? 'one collection url-template, O(1)' : 'per-token url, O(N)'})`
1994
+ // Say which SHAPE landed, not a fixed representation name: since v11 a content-addressed
1995
+ // backend writes an `ipfs`/`arweave` field, not `url`/`url-template`.
1996
+ ? `resolution: ON-CHAIN via the renderer; image OFF-CHAIN on ${backendId} (${contractFields.length ? 'one collection-scope image field, O(1)' : 'per-token image fields, O(N)'})`
1881
1997
  : onChainUri
1882
1998
  ? 'resolution: ON-CHAIN via the renderer (inline SVG per token)'
1883
1999
  : `resolution: off-chain, base ${params.tokenURIBase}`);
@@ -2240,7 +2356,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2240
2356
  }, 0) + 1 // + the deploy tx
2241
2357
  : 1;
2242
2358
  const tokenFields = [];
2243
- // Collection-scope fields the image lane may add (the O(1) `url-template`). Kept separate from the
2359
+ // Collection-scope fields the image lane may add (the O(1) directory image). Kept separate from the
2244
2360
  // authorship fields and CONCATENATED at the params site — never assigned over them, or an
2245
2361
  // `--creator`/`--license` value would silently vanish whenever pattern 2 is in play.
2246
2362
  let imageContractFields = [];
@@ -2263,13 +2379,21 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2263
2379
  const uniform = exts[0] !== '' && exts.every((e) => e === exts[0]);
2264
2380
  if (dryRun || !backend) {
2265
2381
  if (uniform) {
2266
- imageContractFields = [imageUrlTemplateField(`<${backendId}-dir>/{id}${exts[0]}`)];
2267
- info(`would upload ${tokenPaths.length} file(s) as ONE ${backendId} directory → collection image url-template <${backendId}-dir>/{id}${exts[0]} (O(1))`);
2382
+ imageContractFields = [imageLocatorField(backendId, `<${backendId}-dir>/{id}${exts[0]}`, true)];
2383
+ info(`would upload ${tokenPaths.length} file(s) as ONE ${backendId} directory → collection image <${backendId}-dir>/{id}${exts[0]} (O(1); ${describeLocatorField(backendId)})`);
2384
+ // Name the SERVING prefix in the plan too. Without it a preview says "the gateway is the
2385
+ // collection's preference" and never says what that preference resolves to, so a reader
2386
+ // fills the blank with the upload gateway they just typed — the same confusion the live
2387
+ // narration used to cause outright.
2388
+ if (backendId === 'ipfs' || backendId === 'arweave') {
2389
+ const { prefix, chosen } = servingGateway(backendId, flags);
2390
+ info(`served from ${bold(prefix)} ${dim(chosen ? '(your --' + backendId + '-gateway, written on-chain)' : `(public default — nothing written; repoint any time with ${bold('abx set-gateway')})`)}`);
2391
+ }
2268
2392
  }
2269
2393
  else {
2270
2394
  for (const { tokenId } of tokenPaths)
2271
- tokenFields.push(tokenFieldOf(tokenId, imageUrlField(`<${backendId}-url-${tokenId}>`)));
2272
- info(`would upload ${tokenPaths.length} file(s) to ${backendId} → per-id url image fields (mixed extensions, O(N))`);
2395
+ tokenFields.push(tokenFieldOf(tokenId, imageLocatorField(backendId, `<${backendId}-url-${tokenId}>`)));
2396
+ info(`would upload ${tokenPaths.length} file(s) to ${backendId} → per-id image fields (mixed extensions, O(N); ${describeLocatorField(backendId)})`);
2273
2397
  }
2274
2398
  }
2275
2399
  else if (uniform && backend.putDirectory) {
@@ -2277,9 +2401,9 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2277
2401
  const entries = tokenPaths.map(({ tokenId, path }) => ({ name: `${tokenId}${exts[0]}`, bytes: new Uint8Array(readFileSync(path)), contentType: contentTypeFromPath(path) }));
2278
2402
  const { base } = await backend.putDirectory(entries);
2279
2403
  const template = `${base}/{id}${exts[0]}`;
2280
- imageContractFields = [imageUrlTemplateField(template)];
2404
+ imageContractFields = [imageLocatorField(backendId, template, true)];
2281
2405
  info(`uploaded ${entries.length} file(s) as one ${backendId} directory`);
2282
- info(`collection image (url-template): ${template} ${dim('— one field covers every id (O(1))')}`);
2406
+ info(`collection image: ${template} ${dim(`— one field covers every id (O(1); ${describeLocatorField(backendId)})`)}`);
2283
2407
  }
2284
2408
  else {
2285
2409
  // mixed extensions (or no directory support): per-id url fields (O(N)).
@@ -2290,10 +2414,10 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2290
2414
  const locator = (await backend.locator?.(hash)) ?? '';
2291
2415
  if (!locator)
2292
2416
  throw new Error(`backend ${backendId} returned no public locator for id ${tokenId} — a durable backend (ipfs/arweave/cloud) is required for --onchain-uri image hosting`);
2293
- tokenFields.push(tokenFieldOf(tokenId, imageUrlField(locator)));
2417
+ tokenFields.push(tokenFieldOf(tokenId, imageLocatorField(backendId, locator)));
2294
2418
  info(`id ${tokenId} ← ${fname} → ${locator}`);
2295
2419
  }
2296
- info(`mixed file extensions → per-id url fields (O(N)); a uniform extension enables the O(1) directory template`);
2420
+ info(`mixed file extensions → per-id image fields (O(N)); a uniform extension enables the O(1) directory template`);
2297
2421
  }
2298
2422
  if (seriesTraitsOnchain)
2299
2423
  for (const [tokenId, attrs] of seriesTraits)
@@ -2349,6 +2473,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2349
2473
  if (!onchainImage)
2350
2474
  await buildFields();
2351
2475
  const buildForDeployer = async (deployer) => {
2476
+ assertLaneCanSign(flags); // no upload before we know this run can be signed — see `assertLaneCanSign`
2352
2477
  const salt = explicitSalt ?? saltFor(deployer);
2353
2478
  const clone = await predictClone(publicClient, { factory, salt });
2354
2479
  const params = {
@@ -2373,7 +2498,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2373
2498
  tokenFields,
2374
2499
  // Concatenate — the image lane's collection-scope url-template rides ALONGSIDE the authorship
2375
2500
  // fields. Assigning either over the other would silently drop the creator's --creator/--license.
2376
- contractFields: [...authorshipContractFields(flags), ...imageContractFields],
2501
+ contractFields: [...authorshipContractFields(flags), ...gatewayContractFields(flags), ...imageContractFields],
2377
2502
  };
2378
2503
  return { clone, params, salt };
2379
2504
  };
@@ -2738,6 +2863,11 @@ export const DEPLOY_CODE_FLAGS = new Set([
2738
2863
  // Storage + signing + preview flags shared by every deploy path (mirrors storageOverrides() + the lanes).
2739
2864
  export const SHARED_DEPLOY_FLAGS = [
2740
2865
  'backend', 'endpoint', 'bucket', 'region', 'prefix', 'public-base', 'gateway', 'mode', 'api-url', 'upload-url', 'provider', 'storage-signer',
2866
+ // The collection's PREFERRED SERVING gateways, written on chain (`abx_gateway_ipfs` /
2867
+ // `abx_gateway_arweave`) and repointable forever with `abx set-gateway`. Distinct from `--gateway`,
2868
+ // which is where the CLI uploads and probes; `--gateway` seeds these for the backend in use, and
2869
+ // these override it when the two should differ. Every deploy path takes them.
2870
+ 'ipfs-gateway', 'arweave-gateway',
2741
2871
  'sign', 'unsigned', 'for', 'sign-url-file', 'dry-run', 'confirm', 'yes', 'salt', 'factory', 'bootstrap-factory', 'port', 'renderer', 'public-base-url',
2742
2872
  '721c', // opt-in ERC-721C enrollment (recommended | 0x…) — every deploy path takes it
2743
2873
  'json', // B19: the deployed ADDRESS as data, narration to stderr — every deploy path takes it
@@ -3006,11 +3136,9 @@ export async function cmdDeployCodeBody(flags, emit) {
3006
3136
  }
3007
3137
  else if (scriptPath) {
3008
3138
  const source = readFileSync(resolvePath(scriptPath), 'utf8');
3009
- const bytes = new TextEncoder().encode(source);
3010
- const CHUNK = 22_000; // SSTORE2 headroom
3011
- for (let off = 0; off < bytes.length; off += CHUNK)
3012
- scriptChunks.push(toHex(bytes.slice(off, off + CHUNK)));
3013
- contentSummary = `script ${bytes.length} bytes → ${scriptChunks.length} on-chain chunk(s)`;
3139
+ scriptChunks = planOnChainScript(source);
3140
+ const bytes = new TextEncoder().encode(source).length;
3141
+ contentSummary = `script ${bytes} bytes ${scriptChunks.length} on-chain chunk(s)`;
3014
3142
  ok(contentSummary);
3015
3143
  scriptAnalysis = analyzeScript(source, deps.map((d) => d.display)); // traits + PostParam reads + doc size → the Surfaces disposition below
3016
3144
  }
@@ -3042,7 +3170,7 @@ export async function cmdDeployCodeBody(flags, emit) {
3042
3170
  identityFields.push('external_url');
3043
3171
  }
3044
3172
  // authorship + rights — same on-chain inline collection-field slot (creator / license / …).
3045
- contractFields.push(...authorshipContractFields(flags));
3173
+ contractFields.push(...authorshipContractFields(flags), ...gatewayContractFields(flags));
3046
3174
  for (const [flag, field] of AUTHORSHIP_DEPLOY_FIELDS)
3047
3175
  if (flags[flag])
3048
3176
  identityFields.push(field);
@@ -3243,7 +3371,7 @@ export async function cmdDeployCodeBody(flags, emit) {
3243
3371
  const undeclaredParams = (scriptAnalysis?.paramHints ?? []).filter((k) => !declaredSchemaKeys.has(k));
3244
3372
  const paramsDisposition = scriptAnalysis
3245
3373
  ? undeclaredParams.length
3246
- ? `⚠ the script READS ${undeclaredParams.join(', ')} but ${undeclaredParams.length === 1 ? "it isn't" : "they aren't"} in --schema → dropped at render (default used). Add --schema ${undeclaredParams.map((k) => `${k}:<Type>:<Auth>`).join(',')} (a palette collectors set = HexColor:TokenOwner)`
3374
+ ? `⚠ the script READS ${undeclaredParams.join(', ')} but ${undeclaredParams.length === 1 ? "it isn't" : "they aren't"} in --schema → dropped at render (default used) UNLESS an augment hook supplies ${undeclaredParams.length === 1 ? 'it' : 'them'}. Add --schema ${undeclaredParams.map((k) => `${k}:<Type>:<Auth>`).join(',')} only for collector/creator inputs (a palette collectors set = HexColor:TokenOwner); ignore this for keys your augment hook derives`
3247
3375
  : schemas.length
3248
3376
  ? `${schemas.length} declared: ${schemaSummary}`
3249
3377
  : 'none (the script reads no collector params)'
@@ -3380,7 +3508,7 @@ export async function cmdDeployCodeBody(flags, emit) {
3380
3508
  info(` ${g('fully on-chain')} — the whole tokenURI (name + image SVG + traits) is computed on-chain; zero dependency outside the EVM ${dim('(abx verify confirms from chain)')}`);
3381
3509
  }
3382
3510
  else {
3383
- info(` directory branch: no-server, not chain-complete — liveness rides the gateway (generator default; the ${bold('display.gateway')} contract param repoints), and the params ride the URL (8KB budget — abx verify reports urlOverBudget).`);
3511
+ info(` directory branch: no-server, not chain-complete — liveness rides the gateway (public default; ${bold('abx set-gateway')} repoints it), and the params ride the URL (8KB budget — abx verify reports urlOverBudget).`);
3384
3512
  }
3385
3513
  // A resolver is a "kinder serving path" ONLY when there's a live view / off-chain still to
3386
3514
  // serve — i.e. a program. The in-chain renderer-only lane has neither, so don't imply you
@@ -3446,12 +3574,27 @@ export async function cmdDeployCodeBody(flags, emit) {
3446
3574
  if (!imageOrphaned && !hasImageRenderer) {
3447
3575
  if (!hasPublicUrl)
3448
3576
  info(` ${dim('render mode (a still is never on-chain):')} ${bold('service')} (auto-render every mint/param change — a live/for-sale drop) · ${bold('once')} (fixed supply) · ${bold('none')} (placeholder). ${dim('freshness: no resolver ⇒ no chain-watcher ⇒ MANUAL/backfill (`abx render` after mints + each param change; the on-chain animation updates live, the bucket still does NOT). Continuous/live stills ⇒ run a resolver.')}`);
3449
- // The fs render-home footgun bites the RESOLVER lane hardest (a hosted resolver can't read your
3450
- // laptop disk → placeholder on OpenSea), so warn on BOTH lanes and name the concrete fix.
3451
- if (renderHome === 'fs')
3577
+ // A managed provider whose descriptor says `render.attached` owns rendering local
3578
+ // ABX_STORAGE_BACKEND is irrelevant, and warning "placeholder forever" is a lie.
3579
+ let renderAttached = false;
3580
+ if (hasPublicUrl && !loopbackBaseUrl(baseUrl)) {
3581
+ try {
3582
+ const d = await new AbxServiceClient({ baseUrl, timeoutMs: 2_500 }).descriptor();
3583
+ renderAttached = !!d.render?.attached;
3584
+ }
3585
+ catch { /* advisory — a down/non-ABX URL must not fail the dry run */ }
3586
+ }
3587
+ if (renderAttached) {
3588
+ info(` ${dim('render storage home:')} ${bold('managed by the resolver')} ${dim(`(${baseUrl}) — local ABX_STORAGE_BACKEND is irrelevant; skip abx deploy-effects`)}`);
3589
+ }
3590
+ else if (renderHome === 'fs') {
3591
+ // The fs render-home footgun bites the RESOLVER lane hardest (a hosted resolver can't read your
3592
+ // laptop disk → placeholder on OpenSea), so warn on BOTH lanes and name the concrete fix.
3452
3593
  warn(` render storage home is ${bold('fs')} — LOCAL to this machine, so ${hasPublicUrl ? 'a hosted resolver' : 'a marketplace'} can't read the rendered stills → placeholder forever. FIX before you render: ${bold('set ABX_STORAGE_BACKEND=arweave|s3|ipfs')} in .env (a PUBLIC home the runner uploads to).`);
3453
- else
3594
+ }
3595
+ else {
3454
3596
  info(` ${dim('render storage home:')} ${bold(renderHome)} ${dim('— a public home the runner uploads to ✓')}`);
3597
+ }
3455
3598
  }
3456
3599
  (traitsBroken ? warn : info)(`traits: ${traitsDisposition}`);
3457
3600
  const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
@@ -3467,7 +3610,7 @@ export async function cmdDeployCodeBody(flags, emit) {
3467
3610
  // A dropped param is its own, milder problem: the piece renders, that input just takes its
3468
3611
  // default. Kept separate from the broken-surface alarm above (see the note there).
3469
3612
  if (undeclaredParams.length)
3470
- info(` ${dim('dropped params render with their defaults — declare them to make them real:')} ${bold('--schema <key>:<Type>:<Auth>')}`);
3613
+ info(` ${dim('dropped params render with their defaults — declare collector/creator inputs with')} ${bold('--schema <key>:<Type>:<Auth>')} ${dim('(skip this for keys an augment hook supplies)')}`);
3471
3614
  // The runner/verify line is for lanes that need an off-chain STILL. An on-chain image renderer has
3472
3615
  // no still to render, host, or refresh — whether or not there's also a program driving
3473
3616
  // animation_url — so pointing at a runner and a bucket backend there is simply wrong.
@@ -3927,7 +4070,11 @@ export const DEPLOY_CODE_EDITION_FLAGS = new Set([
3927
4070
  // give them otherwise — a real but less helpful refusal (it doesn't say WHY, or the way out).
3928
4071
  // (`resume` is NOT here: --resume + --copies is refused earlier, at cmdDeployCode's dispatch —
3929
4072
  // this function never runs with flags.resume set, so listing it here would just be dead.)
3930
- 'code-dir', 'dep', 'dep-registry', 'image-renderer', 'attributes-renderer', 'image-base', 'no-delegation',
4073
+ 'code-dir', 'image-renderer', 'attributes-renderer', 'image-base', 'no-delegation',
4074
+ // NOT scope cuts: --dep/--dep-registry are fully wired on EditionCode (it inherits the same
4075
+ // Dependencies extension, and the legs ride the same setup multicall — see the Dependencies step
4076
+ // below). They're plain supported flags; `abx help deploy-code` documents them as working.
4077
+ 'dep', 'dep-registry',
3931
4078
  ]);
3932
4079
  export async function cmdDeployEditionCodeBody(flags, emit) {
3933
4080
  const usage = 'abx deploy-code --copies <n|open> --script <file> --name "Title" --symbol SYM (--public-base-url https://your.resolver.domain | --onchain-uri) ' +
@@ -4086,18 +4233,14 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
4086
4233
  step('Content');
4087
4234
  const scriptPath = flags.script;
4088
4235
  const source = readFileSync(resolvePath(scriptPath), 'utf8');
4089
- const bytes = new TextEncoder().encode(source);
4090
- const CHUNK = 22_000; // SSTORE2 headroom
4091
- const scriptChunks = [];
4092
- for (let off = 0; off < bytes.length; off += CHUNK)
4093
- scriptChunks.push(toHex(bytes.slice(off, off + CHUNK)));
4094
- ok(`script ${bytes.length} bytes → ${scriptChunks.length} on-chain chunk(s)`);
4236
+ const scriptChunks = planOnChainScript(source);
4237
+ ok(`script ${new TextEncoder().encode(source).length} bytes ${scriptChunks.length} on-chain chunk(s)`);
4095
4238
  const contractFields = [];
4096
4239
  if (flags.description)
4097
4240
  contractFields.push({ field: encodeTag(F.description), representation: encodeTag(R.inline), value: toHex(String(flags.description)) });
4098
4241
  if (flags['external-url'])
4099
4242
  contractFields.push({ field: encodeTag(F.externalUrl), representation: encodeTag(R.inline), value: toHex(String(flags['external-url'])) });
4100
- contractFields.push(...authorshipContractFields(flags));
4243
+ contractFields.push(...authorshipContractFields(flags), ...gatewayContractFields(flags));
4101
4244
  const onchainUriLegs = onChainUri ? onchainUriSetupCalls({ generator, metadataRenderer }) : [];
4102
4245
  step('Deploy');
4103
4246
  const lane = laneFromFlags(flags);
@@ -4179,7 +4322,11 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
4179
4322
  const depLegCount = dependencySetupCalls(deps, depRegistry).length;
4180
4323
  const approvals = 1 + (scriptChunks.length + schemas.length + depLegCount + onchainUriLegs.length + mintCount > 0 ? 1 : 0);
4181
4324
  if (!dryRun) {
4182
- await confirmSend(`About to deploy an edition code project "${name}" (${symbol}) — script ${bytes.length} bytes → ${scriptChunks.length} chunk(s); ` +
4325
+ await confirmSend(
4326
+ // Second site of the same missing binding as the plan line above — and the worse one: this is
4327
+ // the REAL deploy path, so `deploy-code --copies` threw at the confirmation prompt, after the
4328
+ // wallet and factory work was already done.
4329
+ `About to deploy an edition code project "${name}" (${symbol}) — script ${Buffer.byteLength(source, 'utf8')} bytes → ${scriptChunks.length} chunk(s); ` +
4183
4330
  `up to ${max} id(s) × ${editionSize === 0n ? 'open' : editionSize.toString()} cop${editionSize === 1n ? 'y' : 'ies'} each; ` +
4184
4331
  `${onChainUri ? `tokenURI ON-CHAIN via the renderer (generator ${generator})` : `resolver base ${baseUrl}`}; ` +
4185
4332
  `mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
@@ -4208,7 +4355,10 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
4208
4355
  if (shownAddr)
4209
4356
  info(`deterministic address: ${shownAddr}`);
4210
4357
  info(`name "${name}" · symbol ${symbol} · max ${max} id(s) · royalty ${royaltyBps / 100}%`);
4211
- info(`content: script ${bytes.length} bytes ${scriptChunks.length} on-chain chunk(s)`);
4358
+ // `source` is what this lane read; there has never been a `bytes` binding here, so this line
4359
+ // threw a ReferenceError for every `deploy-code --copies … --dry-run` — the dry run crashed at the
4360
+ // last step of the plan it was printing. Byte length, not UTF-16 code units, matching the 721 twin.
4361
+ info(`content: script ${Buffer.byteLength(source, 'utf8')} bytes → ${scriptChunks.length} on-chain chunk(s)`);
4212
4362
  printSeedSourcePlan(seedSource, chainId);
4213
4363
  info(`PostParam schema(s): ${schemas.length ? schemas.map((s) => describeSchema(s)).join(', ') : 'none'}`);
4214
4364
  info(`mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${deployer ?? 'your wallet'} at deploy` : 'deferred (mint later / external minter)'}`);