@artblocks/abx-cli 0.1.0-alpha.21 → 0.1.0-alpha.22

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 (69) hide show
  1. package/CHANGELOG.md +852 -0
  2. package/assets/renderer-scaffold/README.md +2 -2
  3. package/assets/renderer-scaffold/src/MyRenderer.sol +2 -2
  4. package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +1 -1
  5. package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +2 -2
  6. package/assets/renderer-scaffold/test/MyRenderer.t.sol +1 -1
  7. package/dist/commands/deploy.d.ts +30 -17
  8. package/dist/commands/deploy.d.ts.map +1 -1
  9. package/dist/commands/deploy.js +234 -97
  10. package/dist/commands/deploy.js.map +1 -1
  11. package/dist/commands/project.d.ts +16 -0
  12. package/dist/commands/project.d.ts.map +1 -1
  13. package/dist/commands/project.js +150 -10
  14. package/dist/commands/project.js.map +1 -1
  15. package/dist/commands/reads.js +2 -2
  16. package/dist/commands/reads.js.map +1 -1
  17. package/dist/commands/scaffold.d.ts +3 -1
  18. package/dist/commands/scaffold.d.ts.map +1 -1
  19. package/dist/commands/scaffold.js +64 -20
  20. package/dist/commands/scaffold.js.map +1 -1
  21. package/dist/commands/service.js +1 -1
  22. package/dist/commands/service.js.map +1 -1
  23. package/dist/commands/submit-app.d.ts +58 -0
  24. package/dist/commands/submit-app.d.ts.map +1 -0
  25. package/dist/commands/submit-app.js +512 -0
  26. package/dist/commands/submit-app.js.map +1 -0
  27. package/dist/config.d.ts +1 -15
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +18 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/flag-allowlists.d.ts.map +1 -1
  32. package/dist/flag-allowlists.js +5 -1
  33. package/dist/flag-allowlists.js.map +1 -1
  34. package/dist/flags.d.ts +4 -0
  35. package/dist/flags.d.ts.map +1 -1
  36. package/dist/flags.js +23 -0
  37. package/dist/flags.js.map +1 -1
  38. package/dist/main.js +125 -37
  39. package/dist/main.js.map +1 -1
  40. package/dist/mintpage.d.ts.map +1 -1
  41. package/dist/mintpage.js +29 -4
  42. package/dist/mintpage.js.map +1 -1
  43. package/dist/output.d.ts +32 -1
  44. package/dist/output.d.ts.map +1 -1
  45. package/dist/output.js +74 -9
  46. package/dist/output.js.map +1 -1
  47. package/dist/ownerops.d.ts +101 -13
  48. package/dist/ownerops.d.ts.map +1 -1
  49. package/dist/ownerops.js +357 -89
  50. package/dist/ownerops.js.map +1 -1
  51. package/dist/preview.d.ts +1 -1
  52. package/dist/preview.js +1 -1
  53. package/dist/schema.d.ts +18 -0
  54. package/dist/schema.d.ts.map +1 -1
  55. package/dist/schema.js +37 -2
  56. package/dist/schema.js.map +1 -1
  57. package/dist/served.js +1 -1
  58. package/dist/update-check.d.ts.map +1 -1
  59. package/dist/update-check.js +9 -3
  60. package/dist/update-check.js.map +1 -1
  61. package/package.json +6 -6
  62. package/skill/SKILL.md +62 -32
  63. package/skill/reference/code-projects.md +78 -24
  64. package/skill/reference/creator-token.md +16 -3
  65. package/skill/reference/decisions.md +59 -13
  66. package/skill/reference/hosting.md +1 -1
  67. package/skill/reference/operating.md +33 -13
  68. package/skill/reference/setup.md +1 -1
  69. package/skill/reference/troubleshooting.md +16 -6
@@ -15,20 +15,20 @@
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, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, prepareDeployOneOfOneEdition, prepareDeployEditionImage, prepareDeployEditionCode, redactRpcUrl, resolveChain, resolveDepRegistryPointer, resolveGenerator, resolveRecommendedTransferValidator, resolveRpcUrl, resolveSeedSource, resolveSeriesCodeFactory, saltFor, seriesCodeAbi, seriesCodeFactoryAbi, tryReadContract, } from '@artblocks/abx-sdk';
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';
19
19
  import { DIRECT_URL_BACKENDS, contentTypeFromPath, decideImageContentLane, hashContent, isTurboArweave, resolveBackend, validateRenderStorageCombo, } from '@artblocks/abx-storage';
20
- import { DEFAULT_PORT, artContentHash, generateArt, resolveBaseUrl, startTokenApiServer } from '@artblocks/abx-token-api';
20
+ import { DEFAULT_PORT, contentHash, generateContent, resolveBaseUrl, startTokenApiServer } from '@artblocks/abx-token-api';
21
21
  import { encodeFunctionData, toHex, zeroAddress } from 'viem';
22
22
  import { CHAIN, explorerBase, assertTurboFundsForUpload, backendResolution, collectContentLocators, ensureArweaveIdentityForUpload, factoryAddress, faucetHint, localIndexer, loopbackBaseUrl, noteArweavePlan, noteStorageReadiness, rendererAddress, seriesFactoryAddress, oneOfOneEditionFactoryAddress, editionFactoryAddress, editionCodeFactoryAddress, storageOptions, storageOverrides, storageSignerChoice, } from '../config.js';
23
23
  import { parseDepFlag } from '../deps.js';
24
- import { isDryRun, parseSaltFlag, refuseStrayFlags, warnSignWithoutFor } from '../flags.js';
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
27
  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, authorshipContractFields, computeContentPlan, envStagingSender, parseCompress, parseTransferValidatorValue, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, } from '../ownerops.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
29
  import { canonicalLabel } from '../remote.js';
30
30
  import { confirmSend, gatedSend, laneFromFlags } from '../riskgate.js';
31
- import { describeSchema, parseSchemaSpecs } from '../schema.js';
31
+ import { describeSchema, editionSchemaAdvisory, parseSchemaSpecs } from '../schema.js';
32
32
  import { parseSeriesTraits } from '../series-traits.js';
33
33
  import { decodeOnChainJson } from '../served.js';
34
34
  import { openWalletSession, signHotSequence, signTx } from '../signer.js';
@@ -261,6 +261,7 @@ export function deployCodeCommandLine(flags, salt) {
261
261
  str('max', flags.max);
262
262
  str('schema', flags.schema);
263
263
  bool('no-seed');
264
+ str('seed-source', flags['seed-source']);
264
265
  bool('onchain-uri');
265
266
  str('generator', flags.generator);
266
267
  str('renderer', flags.renderer);
@@ -289,7 +290,7 @@ export function deployCodeCommandLine(flags, salt) {
289
290
  // Build the token's `image` field as an on-chain keccak256 commitment over the bytes,
290
291
  // and (with --image) store those bytes in custody keyed by that hash so the resolver
291
292
  // can serve + verify them. Without --image, fall back to the demo's generative-from-
292
- // address art (recomputable, nothing to store). Returns on-chain field inputs.
293
+ // address content (recomputable, nothing to store). Returns on-chain field inputs.
293
294
  export const imageKeccakField = (value) => ({
294
295
  field: encodeTag(F.image),
295
296
  representation: encodeTag(R.keccak256),
@@ -364,12 +365,12 @@ walletAddr) {
364
365
  // off-chain mode commits its keccak and regenerates/serves it.
365
366
  if (onChain) {
366
367
  return {
367
- tokenFields: [imageInlineField(generateArt(clone))],
368
+ tokenFields: [imageInlineField(generateContent(clone))],
368
369
  contentNote: 'content: generative SVG, INLINE on-chain (self-resolving, no custody)',
369
370
  };
370
371
  }
371
372
  return {
372
- tokenFields: [imageKeccakField(artContentHash(clone))],
373
+ tokenFields: [imageKeccakField(contentHash(clone))],
373
374
  contentNote: 'content: generative SVG from the contract address (demo default)',
374
375
  };
375
376
  }
@@ -509,7 +510,7 @@ export function describeTraits(traits, onchain) {
509
510
  // ── deploy (+ optionally serve) ──────────────────────────────────────────────
510
511
  export async function cmdDeploy(flags, serveAfter) {
511
512
  // `--copies <N|open>` routes to the ERC-1155 edition twin (OneOfOneEdition) — the flagship
512
- // edition product: a priced open/limited edition of ONE artwork. Checked before anything else
513
+ // edition product: a priced open/limited edition of ONE work. Checked before anything else
513
514
  // touches `flags`, so the 721 lane (`cmdDeployBody`) below is reached ONLY when `--copies` is
514
515
  // absent, and is otherwise untouched by this feature — byte-identical to before editions shipped.
515
516
  if (flags.copies !== undefined) {
@@ -543,7 +544,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
543
544
  // lockable) — mirrors the description model. On-chain wins if both ever set the same trait.
544
545
  const traits = parseDeployTraits(flags);
545
546
  console.log(serveAfter
546
- ? bold(`\n ABX · your first token\n ${dim("we'll put art on a blockchain — then delete our copy and get all of it back")}`)
547
+ ? bold(`\n ABX · your first token\n ${dim("we'll put your work on a blockchain — then delete our copy and get all of it back")}`)
547
548
  : bold(`\n ABX Self-Host Toolkit — deploy\n ${dim('a project, served from chain alone')}`));
548
549
  // Signing lane. `demo` used to hard-force the hot lane, which SILENTLY dropped `--sign`: with no
549
550
  // env key it died confusingly, and WITH one it signed from that key while the operator had asked
@@ -639,17 +640,17 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
639
640
  // otherwise leave it EMPTY rather than baking a misleading localhost into the contract.
640
641
  const hasPublicUrl = !!(flags['public-base-url'] || process.env.ABX_PUBLIC_BASE_URL);
641
642
  const onchainImage = !!flags['onchain-image'];
642
- // `abx demo` defaults to FULLY ON-CHAIN. Its default art is a generative SVG, which the renderer
643
+ // `abx demo` defaults to FULLY ON-CHAIN. Its default content is a generative SVG, which the renderer
643
644
  // can inline — so the token self-resolves and, crucially, NO localhost gets baked into the
644
645
  // contract as the tokenURI base. The old default shipped a first-ever token that resolved for
645
646
  // nobody but its author (broken on every marketplace, dead the moment `abx serve` stops) and
646
- // taught that as the normal shape of an NFT. It also undercut the demo's own claim: with the art
647
+ // taught that as the normal shape of an NFT. It also undercut the demo's own claim: with the content
647
648
  // on-chain, "rebuilt from the chain alone" now covers the IMAGE, not just the metadata.
648
649
  //
649
650
  // Opting back into off-chain custody is anything that says "I have somewhere to host": a real base
650
651
  // URL, an explicit --backend, or a RASTER --image. An SVG --image still goes on-chain — inlining is
651
652
  // exactly what the renderer supports (v1 is SVG-only), so there's no reason to send someone's own
652
- // vector art down the localhost path. Raster stays off-chain because forcing it on-chain would
653
+ // vector content down the localhost path. Raster stays off-chain because forcing it on-chain would
653
654
  // silently inline a placeholder instead of their image.
654
655
  const demoImageInlineable = !flags.image || contentTypeFromPath(flags.image) === 'image/svg+xml';
655
656
  const demoDefaultsOnChain = serveAfter && demoImageInlineable && !flags.backend && !hasPublicUrl;
@@ -680,12 +681,12 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
680
681
  // and it's invisible unless someone says it out loud. Two honest versions, because the answer is
681
682
  // genuinely different per lane. Demo only — `deploy` prints the same facts per-field.
682
683
  if (serveAfter && onChainUri) {
683
- console.log(` Your art goes ${bold('INTO')} the contract. Not a link to it — the image itself.`);
684
- console.log(` ${g('✦')} the chain will hold ${dim('your art · your name on it · your royalty · you as owner')}`);
684
+ console.log(` Your work goes ${bold('INTO')} the contract. Not a link to it — the image itself.`);
685
+ console.log(` ${g('✦')} the chain will hold ${dim('your work · your name on it · your royalty · you as owner')}`);
685
686
  console.log(` ${g('✦')} you will need ${dim('nothing else. no server, no IPFS pin, no monthly bill to forget')}`);
686
687
  }
687
688
  else if (serveAfter) {
688
- console.log(` ${g('✦')} the chain will hold ${dim('your name · your royalty · you as owner · a fingerprint of the art')}`);
689
+ console.log(` ${g('✦')} the chain will hold ${dim('your name · your royalty · you as owner · a fingerprint of the work')}`);
689
690
  console.log(` ${g('✦')} this computer holds ${dim('the image bytes themselves')}`);
690
691
  info(dim('the chain proves the bytes are unaltered; it does not store them (that fingerprint is a keccak256 hash)'));
691
692
  }
@@ -702,7 +703,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
702
703
  const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
703
704
  const msg = `Off-chain metadata needs a PUBLIC resolver URL baked on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one ` +
704
705
  `(not marketplaces, not wallets, not even your own browser unless \`abx serve\` is running). Pick a real path:\n` +
705
- ` • Fully on-chain, no host: --onchain-image --compress fastlz (best for small static art)\n` +
706
+ ` • Fully on-chain, no host: --onchain-image --compress fastlz (best for small static content)\n` +
706
707
  ` • Hosted resolver: --public-base-url https://your.domain (or set ABX_PUBLIC_BASE_URL)`;
707
708
  if (!devAllow) {
708
709
  if (dryRun)
@@ -769,12 +770,12 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
769
770
  }
770
771
  step('Stage on-chain image');
771
772
  if (dryRun) {
772
- info(previewImageStaging(flags.image, parseCompress(flags.compress)));
773
+ info(previewImageStaging(flags.image, parseCompress(flags.compress), allowUnreadableOnchain(flags)));
773
774
  }
774
775
  else if (lane === 'send') {
775
776
  // hot lane: the env key stages now (deployer-independent — the manifest doesn't depend
776
777
  // on the clone address), then the deploy below bakes in the reader field.
777
- const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), envStagingSender());
778
+ const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), envStagingSender(), allowUnreadableOnchain(flags));
778
779
  bakedImage = field;
779
780
  info(note);
780
781
  }
@@ -851,7 +852,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
851
852
  royaltyBps,
852
853
  transferValidator,
853
854
  tokenFields,
854
- // authorship + rights (artist / license / …) are collection-scope on-chain fields.
855
+ // authorship + rights (creator / license / …) are collection-scope on-chain fields.
855
856
  contractFields: authorshipContractFields(flags),
856
857
  };
857
858
  return { clone, params, salt, contentNote };
@@ -917,7 +918,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
917
918
  return;
918
919
  }
919
920
  // opt-in --confirm: a final y/N before the real send (no-op without --confirm; never blocks scripts)
920
- await confirmSend(`About to deploy 1/1 "${name}" (${symbol})${flags.image ? ' with your image' : ' (generative demo art)'}; mint: ${noMint ? 'deferred' : 'token #0 at deploy'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
921
+ await confirmSend(`About to deploy 1/1 "${name}" (${symbol})${flags.image ? ' with your image' : ' (generative demo content)'}; mint: ${noMint ? 'deferred' : 'token #0 at deploy'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
921
922
  (transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
922
923
  if (lane === 'send') {
923
924
  // hot lane: the env key is the deployer, known up front.
@@ -960,7 +961,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
960
961
  try {
961
962
  const signer = await session.connect();
962
963
  // Stage on-chain through the session (sets bakedImage, which buildForDeployer reads below).
963
- const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sessionStagingSender(session));
964
+ const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sessionStagingSender(session), allowUnreadableOnchain(flags));
964
965
  bakedImage = field;
965
966
  info(note);
966
967
  const { clone: predicted, params, salt, contentNote } = await buildForDeployer(signer);
@@ -1148,7 +1149,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
1148
1149
  keepAlive();
1149
1150
  }
1150
1151
  // ── deploy --copies: OneOfOneEdition (the flagship ERC-1155 product) ─────────────────────────────
1151
- // Copies of ONE artwork — a priced open/limited edition. The edition twin of `cmdDeployBody`,
1152
+ // Copies of ONE work — a priced open/limited edition. The edition twin of `cmdDeployBody`,
1152
1153
  // reusing every standard-agnostic helper it does (content custody, traits, authorship fields, the
1153
1154
  // `--721c`/`--1155c`-shared validator resolution, salt prediction, signing lanes) but written as its
1154
1155
  // own function rather than threaded into `cmdDeployBody` with `if (copies)` branches throughout —
@@ -1168,10 +1169,10 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1168
1169
  // `--type` is the 721 lane's OWN dimension seam (cmdDeployBody redirects `--type series` to
1169
1170
  // `deploy-series`) — it never even reaches this function for that redirect, since --copies routes
1170
1171
  // here before --type is ever inspected. Refuse the confusing combination explicitly rather than
1171
- // silently ignore it (a creator combining them almost certainly means the multi-artwork edition —
1172
+ // silently ignore it (a creator combining them almost certainly means the multi-work edition —
1172
1173
  // `deploy-series --copies`, not this 1/1-edition lane).
1173
1174
  if (flags.type !== undefined && resolveDimension(flags.type) === 'series') {
1174
- throw new Error("--type series + --copies: this is the 1/1-edition lane (one artwork, copies of it) — for N artworks each with copies, " +
1175
+ throw new Error("--type series + --copies: this is the 1/1-edition lane (one work, copies of it) — for N works each with copies, " +
1175
1176
  'use `abx deploy-series --copies <n|open> --dir <folder>` instead. Drop --type here.');
1176
1177
  }
1177
1178
  const editionSize = parseCopies(flags.copies);
@@ -1182,8 +1183,8 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1182
1183
  const baseUrl = flags['public-base-url'] ?? resolveBaseUrl(port);
1183
1184
  const traits = parseDeployTraits(flags);
1184
1185
  console.log(bold(`\n ABX Self-Host Toolkit — deploy edition\n ${dim(editionSize === 0n
1185
- ? 'OneOfOneEdition (ERC-1155) — an OPEN edition of one artwork, served from chain alone'
1186
- : `OneOfOneEdition (ERC-1155) — ${editionSize} cop${editionSize === 1n ? 'y' : 'ies'} of one artwork, served from chain alone`)}`));
1186
+ ? 'OneOfOneEdition (ERC-1155) — an OPEN edition of one work, served from chain alone'
1187
+ : `OneOfOneEdition (ERC-1155) — ${editionSize} cop${editionSize === 1n ? 'y' : 'ies'} of one work, served from chain alone`)}`));
1187
1188
  if (editionSize === 1n)
1188
1189
  info(copiesOneNote('deploy'));
1189
1190
  const lane = laneFromFlags(flags);
@@ -1257,7 +1258,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1257
1258
  if (!hasPublicUrl || loopback) {
1258
1259
  const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
1259
1260
  const msg = `Off-chain metadata needs a PUBLIC resolver URL baked on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one. ` +
1260
- `Pick a real path:\n • Fully on-chain (SVG art): --onchain-uri\n • Fully on-chain (any media): --onchain-image --compress fastlz\n • Hosted resolver: --public-base-url https://your.domain`;
1261
+ `Pick a real path:\n • Fully on-chain (SVG content): --onchain-uri\n • Fully on-chain (any media): --onchain-image --compress fastlz\n • Hosted resolver: --public-base-url https://your.domain`;
1261
1262
  if (!devAllow) {
1262
1263
  if (dryRun)
1263
1264
  warn(`would REFUSE to deploy — ${msg}`);
@@ -1279,7 +1280,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1279
1280
  // connect, so the connecting wallet pays for and owns every chunk write. `stageNow` is the hot-lane
1280
1281
  // (and dry-run) path; the session branch below calls the same helper with a session-backed sender.
1281
1282
  const stageOnChainImage = async (sender) => {
1282
- const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sender);
1283
+ const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sender, allowUnreadableOnchain(flags));
1283
1284
  bakedImage = field;
1284
1285
  info(note);
1285
1286
  };
@@ -1288,7 +1289,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1288
1289
  throw new Error('--onchain-image needs --image <path> (the bytes to put on-chain)');
1289
1290
  step('Stage on-chain image');
1290
1291
  if (dryRun)
1291
- info(previewImageStaging(flags.image, parseCompress(flags.compress)));
1292
+ info(previewImageStaging(flags.image, parseCompress(flags.compress), allowUnreadableOnchain(flags)));
1292
1293
  else if (lane === 'send')
1293
1294
  await stageOnChainImage(envStagingSender());
1294
1295
  // lane === 'sign' → staged inside the wallet session further down.
@@ -1399,7 +1400,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1399
1400
  emit(jsonSafe({ command: 'deploy', kind: '1of1-edition', copies: editionSize.toString(), dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol }));
1400
1401
  return;
1401
1402
  }
1402
- await confirmSend(`About to deploy an edition of "${name}" (${symbol}) — ${editionSize === 0n ? 'open' : editionSize.toString()} copies${flags.image ? ' with your image' : ' (generative demo art)'}; mint: ${mintAmount > 0n ? `${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
1403
+ await confirmSend(`About to deploy an edition of "${name}" (${symbol}) — ${editionSize === 0n ? 'open' : editionSize.toString()} copies${flags.image ? ' with your image' : ' (generative demo content)'}; mint: ${mintAmount > 0n ? `${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
1403
1404
  (transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
1404
1405
  if (lane === 'send') {
1405
1406
  const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
@@ -1509,7 +1510,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1509
1510
  // token (`0..N-1`, natural-sorted) — a token's metadata is its token id, and tokens mint
1510
1511
  // strictly in order. Content is hosted exactly like a 1/1, per token — three placements:
1511
1512
  // • off-chain custody (default): keccak-committed, bytes served by the resolver.
1512
- // • `--onchain-uri`: inline each token's SVG on-chain (tiny art; SVG only).
1513
+ // • `--onchain-uri`: inline each token's SVG on-chain (tiny content; SVG only).
1513
1514
  // • `--onchain-image [--compress fastlz]`: stage each token's bytes (raster or SVG) into the
1514
1515
  // shared chunk store and bake a per-token `reader` field — multi-chunk on-chain content,
1515
1516
  // self-resolving via the renderer. Implies on-chain URI resolution.
@@ -1587,13 +1588,16 @@ export async function cmdDeploySeriesBody(flags, emit) {
1587
1588
  throw new Error('Staging on-chain images (--onchain-image) needs interactive signing — each chunk tx feeds ' +
1588
1589
  "the next, so it can't run on the cold lane (--unsigned). Use the hot lane (a funded key) or --sign (browser wallet).");
1589
1590
  }
1590
- // Whole-collection cost guard: per-file warnings fire during staging/preview, but a set of many
1591
- // small files can still sum to an expensive on-chain deploy flag the total once, up front.
1591
+ // Whole-collection WRITE-cost guard: a set of many small files can still sum to an expensive
1592
+ // on-chain deploy, so flag the total once, up front. This is a cost signal only — the READ gate is
1593
+ // per token (`guardOnChainSize`, fired per file during staging/preview), because each token's
1594
+ // `tokenURI` assembles only its own content, so a 300-file collection of 5KB pieces reads fine.
1592
1595
  if (onchainImage) {
1593
1596
  const totalBytes = slots.reduce((n, s) => n + statSync(joinPath(dirPath, s)).size, 0);
1594
1597
  if (totalBytes > ONCHAIN_PROJECT_SOFT_LIMIT) {
1595
- warn(`on-chain collection total is ${Math.round(totalBytes / 1024)}KB across ${slots.length} file(s) — on-chain costs ~200 gas/byte, so this is expensive, not cheaper. ` +
1596
- `Past ~${Math.round(ONCHAIN_PROJECT_SOFT_LIMIT / 1024)}KB total, prefer off-chain: --backend arweave (pay-once, permanent) or a hosted resolver + IPFS.`);
1598
+ warn(`on-chain collection total is ${Math.round(totalBytes / 1024)}KB across ${slots.length} file(s) — on-chain costs ~200 gas/byte to WRITE, so this is expensive, not cheaper. ` +
1599
+ `Past ~${Math.round(ONCHAIN_PROJECT_SOFT_LIMIT / 1024)}KB total, prefer off-chain: --backend arweave (pay-once, permanent) or a hosted resolver + IPFS. ` +
1600
+ `(Per-token readability is judged per file, not on this total.)`);
1597
1601
  }
1598
1602
  }
1599
1603
  const minter = flags.minter ?? zeroAddress;
@@ -1636,7 +1640,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1636
1640
  if (!hasPublicUrl || loopback) {
1637
1641
  const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
1638
1642
  const msg = `Off-chain metadata needs a PUBLIC resolver URL baked on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one. ` +
1639
- `Pick a real path:\n • Fully on-chain (SVG art): --onchain-uri\n • Fully on-chain (any media): --onchain-image --compress fastlz\n • Hosted resolver: --public-base-url https://your.domain`;
1643
+ `Pick a real path:\n • Fully on-chain (SVG content): --onchain-uri\n • Fully on-chain (any media): --onchain-image --compress fastlz\n • Hosted resolver: --public-base-url https://your.domain`;
1640
1644
  if (!devAllow) {
1641
1645
  if (dryRun)
1642
1646
  warn(`would REFUSE to deploy — ${msg}`);
@@ -1719,7 +1723,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1719
1723
  const buildImageFields = async (stage) => {
1720
1724
  // --onchain-image: stage every token's bytes into ONE shared chunk store, each a `reader` field.
1721
1725
  if (onchainImage) {
1722
- const { fields } = await stageImageFieldsBatch(tokenPaths.map((s) => s.path), compress, stage, flags['chunk-store']);
1726
+ const { fields } = await stageImageFieldsBatch(tokenPaths.map((s) => s.path), compress, stage, flags['chunk-store'], allowUnreadableOnchain(flags));
1723
1727
  tokenFields = tokenPaths.map(({ tokenId }, i) => tokenFieldOf(tokenId, fields[i]));
1724
1728
  return;
1725
1729
  }
@@ -1835,7 +1839,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1835
1839
  mintTo: mintCount > 0 ? deployer : zeroAddress,
1836
1840
  mintCount,
1837
1841
  tokenFields,
1838
- // image contract fields (the O(1) url-template) + authorship/rights (artist / license / …).
1842
+ // image contract fields (the O(1) url-template) + authorship/rights (creator / license / …).
1839
1843
  contractFields: [...contractFields, ...authorshipContractFields(flags)],
1840
1844
  };
1841
1845
  return { clone, params, salt };
@@ -1887,7 +1891,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
1887
1891
  }
1888
1892
  if (onchainImage) {
1889
1893
  for (const { tokenId, name: fname, path } of tokenPaths) {
1890
- info(`token ${tokenId} ← ${fname}: ${previewImageStaging(path, compress)}`);
1894
+ info(`token ${tokenId} ← ${fname}: ${previewImageStaging(path, compress, allowUnreadableOnchain(flags))}`);
1891
1895
  }
1892
1896
  }
1893
1897
  info(`approvals ${approvals} wallet approval(s)`); // B2 — TX signatures only
@@ -2062,7 +2066,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
2062
2066
  console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces once metadata is live')}\n`);
2063
2067
  }
2064
2068
  // ── deploy-series --copies: EditionImage ──────────────────────────────────────────────────────────
2065
- // N distinct artworks (one per file, like a Series) × copies each. The edition twin of
2069
+ // N distinct works (one per file, like a Series) × copies each. The edition twin of
2066
2070
  // `cmdDeploySeriesBody`, kept as its OWN function for the same reason `cmdDeployOneOfOneEditionBody`
2067
2071
  // is (byte-identical 721 lane — see that function's module note).
2068
2072
  //
@@ -2176,7 +2180,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2176
2180
  if (!hasPublicUrl || loopback) {
2177
2181
  const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
2178
2182
  const msg = `Off-chain metadata needs a PUBLIC resolver URL baked on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one. ` +
2179
- `Pick a real path:\n • Fully on-chain (SVG art): --onchain-uri\n • Hosted resolver: --public-base-url https://your.domain`;
2183
+ `Pick a real path:\n • Fully on-chain (SVG content): --onchain-uri\n • Hosted resolver: --public-base-url https://your.domain`;
2180
2184
  if (!devAllow) {
2181
2185
  if (dryRun)
2182
2186
  warn(`would REFUSE to deploy — ${msg}`);
@@ -2194,8 +2198,8 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2194
2198
  const backendId = overrides.backend ?? process.env.ABX_STORAGE_BACKEND ?? 'fs';
2195
2199
  // Pattern 2 — image off-chain on a durable backend, JSON rendered on-chain, NOTHING to run. Ported
2196
2200
  // from the 721 Series twin, which has had it all along; `EditionImage` shipped without it, so a
2197
- // multi-artwork RASTER edition had no no-server option at all and `--onchain-uri` just refused the
2198
- // art. That pushed a cold agent into deploying one contract PER artwork to get permanence (B28).
2201
+ // multi-work RASTER edition had no no-server option at all and `--onchain-uri` just refused the
2202
+ // content. That pushed a cold agent into deploying one contract PER work to get permanence (B28).
2199
2203
  // Nothing on-chain needed changing: `EditionImageInitParams` already takes `contractFields`, and the
2200
2204
  // renderer's `url-template` substitutes `{id}` from whatever id it is called with — `uri(id)` on a
2201
2205
  // 1155 reaches it exactly as `tokenURI(id)` does on a 721.
@@ -2203,6 +2207,14 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2203
2207
  const offchainImageOnchainJson = onChainUri && !onchainImage && directUrlBackend;
2204
2208
  const backend = (offchainImageOnchainJson || (!onChainUri && !onchainImage)) && !dryRun ? resolveBackend(opts) : undefined;
2205
2209
  const tokenPaths = slots.map((s, i) => ({ tokenId: i, name: s, path: joinPath(dirPath, s) }));
2210
+ // The render-gas gate on the dry run. Unlike the 721 Series twin this lane has no per-id
2211
+ // `previewImageStaging` line to hang it off, so without this an oversized edition would only be
2212
+ // refused once `buildFields` reached the first real staging call — i.e. after the chunk store was
2213
+ // already deployed. Same per-id predicate either way (`guardOnChainSize` runs again during staging).
2214
+ if (onchainImage && dryRun) {
2215
+ for (const { name, path } of tokenPaths)
2216
+ guardOnChainSize(statSync(path).size, name, allowUnreadableOnchain(flags));
2217
+ }
2206
2218
  // Say WHICH backend holds the bytes, exactly as the 1/1 lane does. Both Series lanes resolved a
2207
2219
  // backend and never named it, so a creator asking for "images on IPFS" had no way to confirm from
2208
2220
  // the preview that their collection would actually pin there — the flag was accepted in silence.
@@ -2230,14 +2242,14 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2230
2242
  const tokenFields = [];
2231
2243
  // Collection-scope fields the image lane may add (the O(1) `url-template`). Kept separate from the
2232
2244
  // authorship fields and CONCATENATED at the params site — never assigned over them, or an
2233
- // `--artist`/`--license` value would silently vanish whenever pattern 2 is in play.
2245
+ // `--creator`/`--license` value would silently vanish whenever pattern 2 is in play.
2234
2246
  let imageContractFields = [];
2235
2247
  const buildFields = async (stage) => {
2236
2248
  // Pattern 1: the bytes themselves on-chain, staged into the shared chunk store per id, each id's
2237
2249
  // field pointing at its manifest. Ported from the 721 Series twin — the edition lane refused this
2238
2250
  // outright before (B28), though nothing on-chain prevented it.
2239
2251
  if (onchainImage) {
2240
- const { fields } = await stageImageFieldsBatch(tokenPaths.map((s) => s.path), compress, stage, flags['chunk-store']);
2252
+ const { fields } = await stageImageFieldsBatch(tokenPaths.map((s) => s.path), compress, stage, flags['chunk-store'], allowUnreadableOnchain(flags));
2241
2253
  tokenPaths.forEach(({ tokenId }, i) => tokenFields.push(tokenFieldOf(tokenId, fields[i])));
2242
2254
  if (seriesTraitsOnchain)
2243
2255
  for (const [tokenId, attrs] of seriesTraits)
@@ -2296,13 +2308,13 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2296
2308
  const text = Buffer.from(bytes).toString('utf8');
2297
2309
  if (!looksLikeSvg(text)) {
2298
2310
  // Name the real alternatives, the way the 721 twin's version of this refusal does — a bare
2299
- // "drop --onchain-uri" once sent a cold agent fanning a 3-artwork edition out into THREE
2311
+ // "drop --onchain-uri" once sent a cold agent fanning a 3-work edition out into THREE
2300
2312
  // separate contracts to find permanence. The `--backend` route is now the first suggestion
2301
2313
  // because it IS available on this lane (see `offchainImageOnchainJson` above).
2302
- throw new Error(`--onchain-uri inlines SVG only; id ${tokenId} "${fname}" is ${contentType}. For raster art in ONE edition contract:\n` +
2314
+ throw new Error(`--onchain-uri inlines SVG only; id ${tokenId} "${fname}" is ${contentType}. For raster content in ONE edition contract:\n` +
2303
2315
  ` • No server, images off-chain: add --backend arweave (or ipfs/cloud) — the image URL is baked into the on-chain JSON\n` +
2304
2316
  ` • Host the metadata yourself: drop --onchain-uri and pass --public-base-url https://your.domain\n` +
2305
- ` • Keep it SVG: supply SVG art and --onchain-uri inlines every id on-chain\n` +
2317
+ ` • Keep it SVG: supply SVG content and --onchain-uri inlines every id on-chain\n` +
2306
2318
  ` • Bytes fully on-chain: --onchain-image --compress fastlz (hot or --sign lane; --unsigned is refused)`);
2307
2319
  }
2308
2320
  tokenFields.push(tokenFieldOf(tokenId, imageInlineField(text)));
@@ -2319,7 +2331,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2319
2331
  for (const [tokenId, attrs] of seriesTraits)
2320
2332
  tokenFields.push(tokenFieldOf(tokenId, attributesInlineField(attrs)));
2321
2333
  // AFTER the per-id content actually validated — stating "resolves ON-CHAIN, no server" and then
2322
- // refusing the very art that was passed reads as the tool contradicting itself.
2334
+ // refusing the very content that was passed reads as the tool contradicting itself.
2323
2335
  if (onChainUri) {
2324
2336
  info(`uri()/contractURI() resolve ON-CHAIN via the renderer — no resolver, no server, no localhost.`);
2325
2337
  if (!hasPublicUrl)
@@ -2360,7 +2372,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2360
2372
  mintAmount,
2361
2373
  tokenFields,
2362
2374
  // Concatenate — the image lane's collection-scope url-template rides ALONGSIDE the authorship
2363
- // fields. Assigning either over the other would silently drop the creator's --artist/--license.
2375
+ // fields. Assigning either over the other would silently drop the creator's --creator/--license.
2364
2376
  contractFields: [...authorshipContractFields(flags), ...imageContractFields],
2365
2377
  };
2366
2378
  return { clone, params, salt };
@@ -2519,8 +2531,40 @@ export async function cmdDeployEditionImageBody(flags, emit) {
2519
2531
  * (`putDirectory`; ipfs/arweave), its root as the `code` field
2520
2532
  *
2521
2533
  * Schemas: --schema key:Type:Auth[,key:Type:Auth…] (e.g. palette:HexColor:TokenOwner).
2522
- * Seeds: canonical randomizer by default; --no-seed opts out.
2534
+ * Seeds: the canonical pseudorandom seed source by default; --no-seed opts out.
2535
+ */
2536
+ /**
2537
+ * The one honest sentence about the canonical seed, printed beside the `seed source` line in the
2538
+ * deploy plan — the moment a creator is choosing it, not an appendix. `AbxSeedSource` is
2539
+ * pseudorandom from on-chain values: replayable after the mint (what makes generative output
2540
+ * verifiable) and computable *during* it, so a buyer can decline an outcome for the price of gas.
2541
+ * That rules out anything lottery-like; the escape hatch is a per-project address, so we name it —
2542
+ * and now name it as the FLAG it is (`--seed-source 0x…`), not as an env var, because the flag is
2543
+ * probed and the whole point of naming an escape hatch is that a creator can actually reach it.
2544
+ */
2545
+ const seedHonestyNote = ' pseudorandom — replayable after the mint, computable DURING it (a buyer can decline an outcome).\n' +
2546
+ ' Right for diversifying output; NOT strong enough for a raffle or prize draw — for that, pass\n' +
2547
+ ' --seed-source 0x… pointing at your own IAbxSeedSource over commit-reveal or a VRF oracle\n' +
2548
+ ' (a per-project swap, not a fork). Change it later with `abx set-seed-source`.';
2549
+ /**
2550
+ * The `seed source` line of a deploy plan, for both code lanes. Three states, three different
2551
+ * sentences — the honesty note describes `AbxSeedSource`'s OWN properties, so printing it beside a
2552
+ * stranger's contract would be us vouching for randomness we have never seen.
2523
2553
  */
2554
+ function printSeedSourcePlan(seedSource, chainId) {
2555
+ if (seedSource === zeroAddress) {
2556
+ info('seed source: none (--no-seed / not configured)');
2557
+ return;
2558
+ }
2559
+ if (seedSource.toLowerCase() === canonicalSeedSource(chainId).toLowerCase()) {
2560
+ info(`seed source: ${seedSource} ${dim('(canonical AbxSeedSource)')}`);
2561
+ info(seedHonestyNote);
2562
+ return;
2563
+ }
2564
+ info(`seed source: ${seedSource} ${dim('— CUSTOM (yours, not the canonical AbxSeedSource)')}`);
2565
+ info(' probed: it answers seed(uint256,address) with 32 bytes. Its randomness properties are YOURS to\n' +
2566
+ ' state to buyers — ABX makes no claim about them, and the honesty note above does not apply.');
2567
+ }
2524
2568
  // ── the demo walkthrough: teaching sections, demo-only ────────────────────────
2525
2569
  //
2526
2570
  // `abx demo` is a TEACHING command, not a shortcut — the docs point a first-time reader here to
@@ -2608,7 +2652,7 @@ export async function walkthroughRebuild(indexer, address, before) {
2608
2652
  * Which is genuinely a different act per lane, so it reads from the real source in each case rather
2609
2653
  * than always going through the local server:
2610
2654
  * • fully on-chain → call `tokenURI(0)` on the contract. That IS what a marketplace does, and on
2611
- * this lane the whole answer (art included) comes back from the chain with nothing else running.
2655
+ * this lane the whole answer (content included) comes back from the chain with nothing else running.
2612
2656
  * • off-chain custody → fetch the resolver, because that's what the baked URI points at.
2613
2657
  * Reading the on-chain lane over HTTP would have quietly implied the local server was load-bearing
2614
2658
  * when it isn't — the opposite of the lesson.
@@ -2635,9 +2679,9 @@ export async function walkthroughReadBack(state, baseUrl, onChainUri) {
2635
2679
  const img = typeof parsed.image === 'string' ? parsed.image : '';
2636
2680
  console.log(` ${g('✓')} came back with: ${bold(String(parsed.name ?? '(no name)'))}`);
2637
2681
  console.log(` ${dim(`image: ${img.slice(0, 48)}${img.length > 48 ? '…' : ''}`)}`);
2638
- // The punchline of the whole lane: a data: URI means the art travelled IN the answer.
2682
+ // The punchline of the whole lane: a data: URI means the work travelled IN the answer.
2639
2683
  if (img.startsWith('data:'))
2640
- info(`${g('the art itself came back in that answer')} ${dim('— no link to follow, nothing to go missing')}`);
2684
+ info(`${g('the work itself came back in that answer')} ${dim('— no link to follow, nothing to go missing')}`);
2641
2685
  }
2642
2686
  else {
2643
2687
  console.log(` ${dim(uri.slice(0, 160))}${uri.length > 160 ? dim('…') : ''}`);
@@ -2674,12 +2718,12 @@ export async function walkthroughReadBack(state, baseUrl, onChainUri) {
2674
2718
  export const DEPLOY_CODE_FLAGS = new Set([
2675
2719
  // content + collection identity
2676
2720
  'script', 'code-dir', 'name', 'symbol', 'description', 'external-url', 'description-onchain',
2677
- ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag), // artist · display-notes · artist-links · license
2721
+ ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag), // creator · display-notes · creator-links · license
2678
2722
  'image-base', 'attributes-renderer', 'image-renderer',
2679
2723
  // supply + mint + economics
2680
2724
  'max', 'mint-count', 'mint-all', 'no-mint', 'unpaused', 'minter', 'primary-payee', 'royalty-bps', '721c',
2681
2725
  // params + dependencies
2682
- 'schema', 'no-seed', 'no-delegation', 'dep', 'dep-registry',
2726
+ 'schema', 'no-seed', 'seed-source', 'no-delegation', 'dep', 'dep-registry',
2683
2727
  // serving lane
2684
2728
  'public-base-url', 'onchain-uri', 'generator', 'renderer', 'port',
2685
2729
  // storage (directory mode) — mirrors storageOverrides()
@@ -2701,7 +2745,7 @@ export const SHARED_DEPLOY_FLAGS = [
2701
2745
  // A 1/1 `abx deploy` accepts these (see `abx help deploy`). Anything else warns (typo'd/unsupported).
2702
2746
  export const DEPLOY_FLAGS = new Set([
2703
2747
  'image', 'name', 'symbol', 'type', 'description', 'description-onchain', 'external-url', 'traits', 'traits-onchain', 'attributes',
2704
- 'onchain-uri', 'onchain-image', 'compress', 'royalty-bps', 'no-mint',
2748
+ 'onchain-uri', 'onchain-image', 'compress', 'allow-unreadable-onchain', 'royalty-bps', 'no-mint',
2705
2749
  ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag),
2706
2750
  ...SHARED_DEPLOY_FLAGS,
2707
2751
  ]);
@@ -2709,7 +2753,7 @@ export const DEPLOY_FLAGS = new Set([
2709
2753
  export const DEPLOY_SERIES_FLAGS = new Set([
2710
2754
  'dir', 'count', 'name', 'symbol', 'description', 'external-url', 'royalty-bps', 'attributes', 'traits-onchain',
2711
2755
  'no-mint', 'mint-all', 'mint-count', 'unpaused', 'minter', 'primary-payee',
2712
- 'onchain-uri', 'onchain-image', 'compress', 'chunk-store',
2756
+ 'onchain-uri', 'onchain-image', 'compress', 'allow-unreadable-onchain', 'chunk-store',
2713
2757
  ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag),
2714
2758
  ...SHARED_DEPLOY_FLAGS,
2715
2759
  ]);
@@ -2743,7 +2787,7 @@ export async function cmdDeployCodeBody(flags, emit) {
2743
2787
  const usage = 'abx deploy-code (--script <file> | --code-dir <dir>) --name "Title" --symbol SYM ' +
2744
2788
  '(--public-base-url https://your.resolver.domain | --onchain-uri) ' +
2745
2789
  '[--description "<s>"] [--external-url <url>] [--image-base <url> | --image-renderer 0x..] [--attributes-renderer 0x..] ' +
2746
- '[--max N] [--mint-count N | --mint-all] [--schema key:Type:Auth,…] [--no-seed] ' +
2790
+ '[--max N] [--mint-count N | --mint-all] [--schema key:Type:Auth,…] [--no-seed | --seed-source 0x..] ' +
2747
2791
  '[--dep <name@version|0x..>[,…]] [--dep-registry 0x..] ' +
2748
2792
  '[--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..] [--backend ipfs|arweave] [--dry-run] [--confirm] [--bootstrap-factory]';
2749
2793
  const scriptPath = flags.script;
@@ -2835,11 +2879,7 @@ export async function cmdDeployCodeBody(flags, emit) {
2835
2879
  else {
2836
2880
  factory = await ensureSeriesCodeFactory(publicClient, flags.factory, !!flags['bootstrap-factory']);
2837
2881
  }
2838
- const seedSource = flags['no-seed'] !== undefined
2839
- ? zeroAddress
2840
- : dryRun
2841
- ? (resolveSeedSource(chainId) ?? zeroAddress)
2842
- : await ensureSeedSource(publicClient);
2882
+ const seedSource = await resolveSeedSourceFlag(flags, publicClient, dryRun, chainId);
2843
2883
  // --onchain-uri: resolve the two canonical singletons the lane points at. The generator is
2844
2884
  // per-chain infrastructure (constructor-wired to the dependency registry, the abx.js/gunzip
2845
2885
  // runtime pointers, and default gateways), so it is NEVER auto-deployed — a chain without one
@@ -2890,6 +2930,13 @@ export async function cmdDeployCodeBody(flags, emit) {
2890
2930
  // schemas: key:Type:Auth (comma-separated), Type carrying optional Select options / Range bounds
2891
2931
  // (Select[A|B|C], Uint256Range[0..100], …) → key, enum indices, bytes32 min/max, selectOptions.
2892
2932
  const schemas = parseSchemaSpecs(flags.schema);
2933
+ // Say it where the decision is made, not in a doc the creator may never open: on an edition, a
2934
+ // holder-writable param is SHARED by every holder of that id. See `editionSchemaAdvisory`.
2935
+ if (flags.copies !== undefined) {
2936
+ const advisory = editionSchemaAdvisory(schemas);
2937
+ if (advisory)
2938
+ warn(advisory);
2939
+ }
2893
2940
  // Dependencies: ordered `--dep` refs (repeatable / comma-separable) — index 0 = the runtime,
2894
2941
  // by convention. `name@version` ⇒ Resolution.Registry (resolved through the soft registry
2895
2942
  // pointer); `0x…` ⇒ Resolution.OnChain (a raw data contract, no registry involved). The legs
@@ -2969,7 +3016,7 @@ export async function cmdDeployCodeBody(flags, emit) {
2969
3016
  }
2970
3017
  else {
2971
3018
  // Renderer-only (in-chain SVG): no program to chunk — the image/attributes are Solidity field
2972
- // renderers. Nothing rides SSTORE2; the whole artwork is computed on-chain per view.
3019
+ // renderers. Nothing rides SSTORE2; the whole work is computed on-chain per view.
2973
3020
  contentSummary = 'no program — image + traits computed on-chain by Solidity field renderers';
2974
3021
  ok(contentSummary);
2975
3022
  }
@@ -2994,14 +3041,14 @@ export async function cmdDeployCodeBody(flags, emit) {
2994
3041
  contractFields.push({ field: encodeTag(F.externalUrl), representation: encodeTag(R.inline), value: toHex(String(flags['external-url'])) });
2995
3042
  identityFields.push('external_url');
2996
3043
  }
2997
- // authorship + rights — same on-chain inline collection-field slot (artist / license / …).
3044
+ // authorship + rights — same on-chain inline collection-field slot (creator / license / …).
2998
3045
  contractFields.push(...authorshipContractFields(flags));
2999
3046
  for (const [flag, field] of AUTHORSHIP_DEPLOY_FIELDS)
3000
3047
  if (flags[flag])
3001
3048
  identityFields.push(field);
3002
3049
  // --image-renderer: bake the on-chain `image` field to a `renderer` representation — the SVG is
3003
3050
  // COMPUTED on-chain by a Solidity IAbxFieldRenderer (e.g. a SeedSvgRenderer fork) from the token's
3004
- // seed + params. THE in-chain-art lane: the marketplace still lives in tokenURI itself — no bucket,
3051
+ // seed + params. THE in-chain-renderer lane: the marketplace still lives in tokenURI itself — no bucket,
3005
3052
  // no effect runner, no resolver. Mutually exclusive with --image-base (both set `image`). Verified
3006
3053
  // like --attributes-renderer: a real deploy REFUSES a codeless address; dry-run probes best-effort.
3007
3054
  if (flags['image-renderer']) {
@@ -3210,7 +3257,15 @@ export async function cmdDeployCodeBody(flags, emit) {
3210
3257
  // TOKENURI PUBLIC-READ GAS — a large --onchain-uri document assembles on-chain per call; an
3211
3258
  // unauthenticated public read (Etherscan "Read Contract" with no wallet connected) can hit a client
3212
3259
  // gas cap and appear to REVERT. Expected, not a bug — a real session mis-diagnosed it as indexing lag.
3213
- const tokenUriGasRisk = onChainUri && !codeDir && (scriptAnalysis?.doc.estBytes ?? 0) > 30_000;
3260
+ //
3261
+ // Same threshold as the staged-bytes gate (`ONCHAIN_READ_WARN_BYTES`), since it is the same measured
3262
+ // cost: `tokenURI` string-building runs ~360-405k gas per KB of assembled document (superlinear — the
3263
+ // rate climbs with size). This one only WARNS
3264
+ // where the staged-bytes path refuses, and deliberately so: `estBytes` is an ESTIMATE (a dependency
3265
+ // of unknown size is undercounted — see `unknownDepSizes`), and a registry-hosted library like p5 is
3266
+ // legitimately ~200KB on its own, so the honest move is to name the cost and point at the piecewise
3267
+ // getters rather than to block a shipped lane on a heuristic.
3268
+ const tokenUriGasRisk = onChainUri && !codeDir && (scriptAnalysis?.doc.estBytes ?? 0) >= ONCHAIN_READ_WARN_BYTES;
3214
3269
  // --onchain-uri legs (ride the setup multicall, before the mints): the animation_url field
3215
3270
  // pointing at the generator, and the two URI renderers. The param surface needs NO leg — the
3216
3271
  // generator enumerates it from the token on-chain (`contractParamKeys`/`tokenParamKeys`, kept in
@@ -3298,7 +3353,7 @@ export async function cmdDeployCodeBody(flags, emit) {
3298
3353
  if (!flags.description)
3299
3354
  warn(`no --description — the on-chain metadata ships with no description (the one creators most often forget). Add --description "…" or deploy bare.`);
3300
3355
  info(`content: ${contentSummary}`);
3301
- info(`seed source: ${seedSource === zeroAddress ? 'none (--no-seed / not configured)' : seedSource}`);
3356
+ printSeedSourcePlan(seedSource, chainId);
3302
3357
  info(`PostParam schema(s): ${schemaSummary}`);
3303
3358
  info(`dependencies: ${deps.length ? deps.map((d, i) => `[${i}] ${d.display}`).join(' · ') + ` — ${deps.length} setDependency leg(s)` : 'none'}` +
3304
3359
  (depRegistry ? ` + setDependencyRegistry → ${depRegistry}` : hasRegistryDeps ? ' (no registry pointer — see the warning above)' : ''));
@@ -3335,7 +3390,9 @@ export async function cmdDeployCodeBody(flags, emit) {
3335
3390
  else
3336
3391
  info(` ${dim('nothing to run — image + traits are on-chain; a resolver is neither needed nor a benefit here.')}`);
3337
3392
  if (tokenUriGasRisk) {
3338
- info(` ${dim('public-read note:')} this ${Math.round((scriptAnalysis?.doc.estBytes ?? 0) / 1000)}KB doc assembles on-chain per call, so an ${bold('unauthenticated read on Etherscan')} ("Read Contract", no wallet connected) may hit its gas cap and appear to ${bold('REVERT')} — ${dim('EXPECTED, not a broken token and NOT an indexing issue. Verify with `abx tokenuri`, or connect a wallet / use a high-gas RPC. Do not run `abx index --full` to "fix" it.')}`);
3393
+ const docKb = Math.round((scriptAnalysis?.doc.estBytes ?? 0) / 1024);
3394
+ info(` ${dim('public-read note:')} this ~${docKb}KB doc assembles on-chain per call — ${bold(`~${Math.round(tokenUriGasEstimate(scriptAnalysis?.doc.estBytes ?? 0) / 1_000_000)}M gas per tokenURI`)} at the measured ~360-405k gas/KB (climbing with size). An ${bold('unauthenticated read on Etherscan')} ("Read Contract", no wallet connected) may hit its gas cap and appear to ${bold('REVERT')} — ${dim('EXPECTED, not a broken token and NOT an indexing issue. Verify with `abx tokenuri`, or connect a wallet / use a high-gas RPC. Do not run `abx index --full` to "fix" it.')}`);
3395
+ info(` ${dim('these are OFF-CHAIN view reads by design:')} past a node\'s cap, a client assembles from the generator\'s piecewise getters instead of one call — ${dim('`document`, `tokenDataJson`, `dependencyTag`, `abxJs`, `gunzipScript`, `registryScriptChunk`. No contract can call tokenURI inside a transaction at this size; nothing on-chain depends on it.')}`);
3339
3396
  }
3340
3397
  }
3341
3398
  else {
@@ -3849,7 +3906,7 @@ export async function cmdDeployCodeBody(flags, emit) {
3849
3906
  // • `--code-dir` (directory-build code drops) — not ported; use `--script`.
3850
3907
  // • `--dep` / `--dep-registry` (declared code dependencies) — not ported; ship a self-contained
3851
3908
  // script (the skill already recommends zero-dependency vanilla JS as the cleanest option).
3852
- // • `--image-renderer` / `--attributes-renderer` / `--image-base` (the in-chain-Solidity-art and
3909
+ // • `--image-renderer` / `--attributes-renderer` / `--image-base` (the in-chain-Solidity-renderer and
3853
3910
  // deterministic-off-chain-thumbnail lanes) — not ported.
3854
3911
  // • `--no-delegation` — refused outright: EditionCode has no TokenOwner-leg delegation to opt out
3855
3912
  // of (its auth leg generalizes to "any holder", per the parity plan's ConfigurableParams note).
@@ -3861,7 +3918,7 @@ export const DEPLOY_CODE_EDITION_FLAGS = new Set([
3861
3918
  'script', 'name', 'symbol', 'description', 'external-url', 'description-onchain',
3862
3919
  ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag),
3863
3920
  'max', 'mint-count', 'mint-all', 'no-mint', 'mint-amount', 'unpaused', 'minter', 'primary-payee', 'royalty-bps', '721c',
3864
- 'schema', 'no-seed',
3921
+ 'schema', 'no-seed', 'seed-source',
3865
3922
  'public-base-url', 'onchain-uri', 'generator', 'renderer', 'port',
3866
3923
  'sign', 'unsigned', 'for', 'salt', 'factory', 'bootstrap-factory', 'sign-url-file',
3867
3924
  'dry-run', 'confirm', 'yes', 'json', 'copies',
@@ -3875,7 +3932,7 @@ export const DEPLOY_CODE_EDITION_FLAGS = new Set([
3875
3932
  export async function cmdDeployEditionCodeBody(flags, emit) {
3876
3933
  const usage = 'abx deploy-code --copies <n|open> --script <file> --name "Title" --symbol SYM (--public-base-url https://your.resolver.domain | --onchain-uri) ' +
3877
3934
  '[--description "<s>"] [--external-url <url>] [--max N] [--mint-count N | --mint-all] [--mint-amount N] ' +
3878
- '[--schema key:Type:Auth,…] [--no-seed] [--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..]';
3935
+ '[--schema key:Type:Auth,…] [--no-seed | --seed-source 0x..] [--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..]';
3879
3936
  refuseStrayFlags(flags, DEPLOY_CODE_EDITION_FLAGS, 'deploy-code');
3880
3937
  warnSignWithoutFor(flags);
3881
3938
  const editionSize = parseCopies(flags.copies);
@@ -3957,11 +4014,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
3957
4014
  else {
3958
4015
  factory = await ensureEditionCodeFactory(publicClient, flags.factory, !!flags['bootstrap-factory']);
3959
4016
  }
3960
- const seedSource = flags['no-seed'] !== undefined
3961
- ? zeroAddress
3962
- : dryRun
3963
- ? (resolveSeedSource(chainId) ?? zeroAddress)
3964
- : await ensureSeedSource(publicClient);
4017
+ const seedSource = await resolveSeedSourceFlag(flags, publicClient, dryRun, chainId);
3965
4018
  let generator = zeroAddress;
3966
4019
  let metadataRenderer = zeroAddress;
3967
4020
  if (onChainUri) {
@@ -3986,6 +4039,13 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
3986
4039
  info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
3987
4040
  }
3988
4041
  const schemas = parseSchemaSpecs(flags.schema);
4042
+ // Say it where the decision is made, not in a doc the creator may never open: on an edition, a
4043
+ // holder-writable param is SHARED by every holder of that id. See `editionSchemaAdvisory`.
4044
+ if (flags.copies !== undefined) {
4045
+ const advisory = editionSchemaAdvisory(schemas);
4046
+ if (advisory)
4047
+ warn(advisory);
4048
+ }
3989
4049
  // Dependencies — same shape as the 721 SeriesCode twin (see its block for the full reasoning).
3990
4050
  // `EditionCode` already inherits the `Dependencies` extension and calls `_initDependencies()`, and
3991
4051
  // the legs are the same `setDependency`/`setDependencyRegistry` calls on the same ABI, so this was
@@ -4149,7 +4209,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
4149
4209
  info(`deterministic address: ${shownAddr}`);
4150
4210
  info(`name "${name}" · symbol ${symbol} · max ${max} id(s) · royalty ${royaltyBps / 100}%`);
4151
4211
  info(`content: script ${bytes.length} bytes → ${scriptChunks.length} on-chain chunk(s)`);
4152
- info(`seed source: ${seedSource === zeroAddress ? 'none (--no-seed / not configured)' : seedSource}`);
4212
+ printSeedSourcePlan(seedSource, chainId);
4153
4213
  info(`PostParam schema(s): ${schemas.length ? schemas.map((s) => describeSchema(s)).join(', ') : 'none'}`);
4154
4214
  info(`mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${deployer ?? 'your wallet'} at deploy` : 'deferred (mint later / external minter)'}`);
4155
4215
  info(`transactions: 2 — deploy + setup multicall (script chunks${schemas.length ? ' + schemas' : ''}${onChainUri ? ' + on-chain URI wiring' : ''}${mintCount > 0 ? ' + reserve mints' : ''})`);
@@ -4243,26 +4303,28 @@ export async function resolveTransferValidatorFlag(flags, publicClient, dryRun,
4243
4303
  return zeroAddress;
4244
4304
  const chainId = resolveChain(CHAIN).id;
4245
4305
  const validator = parseTransferValidatorValue(raw, { chainId, chainLabel: CHAIN });
4246
- // Has-code precheck — for the recommended constant too (presence on THIS chain is the fact that
4247
- // matters; a sandbox/private chain won't have it). A dry run tolerates an unreachable RPC (it
4248
- // sends nothing); a real deploy refuses to enroll blind.
4249
- let code;
4250
- try {
4251
- code = await publicClient.getCode({ address: validator });
4252
- }
4253
- catch (err) {
4306
+ // Usability precheck — for the recommended constant too (presence on THIS chain is the fact that
4307
+ // matters; a sandbox/private chain won't have it). This asks the same question the factory asks:
4308
+ // has code AND is not a permissive fallback that would enforce nothing (a Safe, an uninitialised
4309
+ // proxy, a 7702-delegated EOA all pass a bare has-code check and then wave every transfer
4310
+ // through). A dry run tolerates an unreachable RPC (it sends nothing); a real deploy refuses to
4311
+ // enroll blind.
4312
+ const probe = await probeTransferValidator(publicClient, validator);
4313
+ if (probe.verdict === 'unreachable') {
4254
4314
  if (!dryRun)
4255
- throw new Error(`--721c: couldn't verify the validator has code at ${validator} (${err.message}) — refusing to enroll blind; retry when the RPC answers.`);
4256
- code = null; // offline previewnote it below
4315
+ throw new Error(`--721c: couldn't verify the validator at ${validator} (${probe.error ?? 'RPC did not answer'}) — refusing to enroll blind; retry when the RPC answers.`);
4316
+ info(dim(`--721c: RPC unreachable, so the validator check is deferred a real deploy verifies ${validator} is a usable validator first.`));
4257
4317
  }
4258
- if (code === null) {
4259
- info(dim(`--721c: RPC unreachable, so the validator code check is deferred — a real deploy verifies ${validator} has code first.`));
4260
- }
4261
- else if (!code || code === '0x') {
4318
+ else if (probe.verdict !== 'ok') {
4262
4319
  const rec = resolveRecommendedTransferValidator(chainId);
4263
- throw new Error(`--721c: no contract code at ${validator} on ${CHAIN} the deploy would revert InvalidTransferValidator(). ` +
4264
- `A transfer validator must be a DEPLOYED contract on this chain` +
4265
- (rec && rec !== validator ? ` (the known-good one: --721c recommended → ${rec})` : '') + `.`);
4320
+ const recHint = rec && rec !== validator ? ` (the known-good one: --721c recommended → ${rec})` : '';
4321
+ throw new Error(probe.verdict === 'no-code'
4322
+ ? `--721c: no contract code at ${validator} on ${CHAIN} the deploy would revert InvalidTransferValidator(). ` +
4323
+ `A transfer validator must be a DEPLOYED contract on this chain${recHint}.`
4324
+ : `--721c: ${validator} has code on ${CHAIN}, but it is not a transfer validator — it answers ANY function call ` +
4325
+ `successfully (a Safe, an uninitialised proxy, or a 7702-delegated EOA does this), so every transfer would ` +
4326
+ `silently pass validation while ERC-165 and getTransferValidator() reported enforcement as ON. The factory ` +
4327
+ `refuses it too (InvalidTransferValidator()). Pass a real validator contract${recHint}, or drop --721c.`);
4266
4328
  }
4267
4329
  // The one plain statement — printed once, only when enrolling. Never appears un-enrolled.
4268
4330
  info(validator === resolveRecommendedTransferValidator(chainId)
@@ -4270,4 +4332,79 @@ export async function resolveTransferValidatorFlag(flags, publicClient, dryRun,
4270
4332
  : `ERC-${standard}: every non-mint transfer is checked by validator ${validator} — its policy decides which operators may transfer. Manage with \`abx set-transfer-validator\`.`);
4271
4333
  return validator;
4272
4334
  }
4335
+ /**
4336
+ * Resolve the three-way seed decision into the InitParams `seedSource`, for both code lanes:
4337
+ *
4338
+ * - `--no-seed` → `zeroAddress`. No mint-time seed at all.
4339
+ * - nothing, or `canonical` → the canonical `AbxSeedSource` via `ensureSeedSource` (manifest →
4340
+ * self-heal to the deterministic address → CREATE2-deploy it). This
4341
+ * is today's behaviour, byte-for-byte, and stays the default.
4342
+ * - `--seed-source 0x…` → the creator's own `IAbxSeedSource`, **probed first**.
4343
+ *
4344
+ * The flag exists because we tell creators, in the docs and in this command's own help, that the fix
4345
+ * for "the canonical seed isn't strong enough for my raffle" is to point `seedSource` at a
4346
+ * commit-reveal / VRF-backed source of their own. Until now the only route was an `ABX_SEED_SOURCE`
4347
+ * env var checked for nothing but code length, which made a first-class promise out of a
4348
+ * side entrance.
4349
+ *
4350
+ * **Everything that isn't the canonical singleton gets probed**, including an address that arrived
4351
+ * via `ABX_SEED_SOURCE` — so the env lane keeps working and stops being the unchecked one. The
4352
+ * comparison is against the deterministic canonical address rather than "did a flag say so", because
4353
+ * that is the actual question (is this our audited bytecode, or someone's contract we know nothing
4354
+ * about?). A dry run tolerates an unreachable RPC and says the check is deferred; a real deploy
4355
+ * refuses to bake a seed source it couldn't verify.
4356
+ *
4357
+ * The probe here runs with **no `from`**, unlike the owner op's (which calls as the token). That is
4358
+ * not an oversight: the clone's address is a function of the CREATE2 salt, which isn't reserved yet
4359
+ * at this point, so a source that gates on `msg.sender` could not have been told about this token in
4360
+ * the first place. A gated/commit-reveal source is a deploy-then-`abx set-seed-source` flow by
4361
+ * construction, and the `reverted` refusal says exactly that.
4362
+ */
4363
+ export async function resolveSeedSourceFlag(flags, publicClient, dryRun, chainId) {
4364
+ const raw = flags['seed-source'];
4365
+ // `--resume` finishes an EXISTING contract, and the seed source is an InitParams field — fixed at
4366
+ // creation, never in the setup multicall this replays. So there is nothing here to resolve, probe,
4367
+ // or (worse) lazily DEPLOY. Not a refusal: `--resume`'s own contract is "pass the same content
4368
+ // flags", so the creator's original command legitimately still carries `--no-seed`/`--seed-source`.
4369
+ // Say plainly that it's read from chain, and name the verb that can actually change it.
4370
+ if (flags.resume !== undefined) {
4371
+ if (raw !== undefined || flags['no-seed'] !== undefined) {
4372
+ info(dim(`--resume: the seed source was fixed when ${flags.resume} was created, so ${raw !== undefined ? '--seed-source' : '--no-seed'} has nothing to write here ` +
4373
+ `(it isn't part of the setup transaction being replayed). Read it with \`abx state\`; change it with \`abx set-seed-source\`.`));
4374
+ }
4375
+ return zeroAddress; // unused by the resume legs
4376
+ }
4377
+ if (flags['no-seed'] !== undefined) {
4378
+ // Two flags, one field. Refuse rather than pick a winner — a creator who passed both has a
4379
+ // belief about the outcome, and either answer betrays half of them.
4380
+ if (raw !== undefined) {
4381
+ throw new Error('--no-seed and --seed-source contradict each other: one says "no mint-time seed at all", the other names where the seed comes from. Drop one.');
4382
+ }
4383
+ return zeroAddress;
4384
+ }
4385
+ const canonical = canonicalSeedSource(chainId);
4386
+ const requested = raw === undefined ? canonical : parseSeedSourceValue(raw, { chainId });
4387
+ // The canonical singleton keeps its existing bootstrap path (deploy it if this chain lacks one).
4388
+ // Anything else must already exist — we never deploy a stranger's contract.
4389
+ const same = (a, b) => a.toLowerCase() === b.toLowerCase();
4390
+ let seedSource;
4391
+ if (same(requested, canonical)) {
4392
+ seedSource = dryRun ? (resolveSeedSource(chainId) ?? zeroAddress) : await ensureSeedSource(publicClient);
4393
+ }
4394
+ else {
4395
+ seedSource = requested;
4396
+ }
4397
+ if (seedSource === zeroAddress || same(seedSource, canonical))
4398
+ return seedSource;
4399
+ // Custom source (flag or ABX_SEED_SOURCE) — ask it the question the mint will ask.
4400
+ const probe = await probeSeedSource(publicClient, seedSource);
4401
+ if (probe.verdict === 'unreachable' && dryRun) {
4402
+ info(dim(`--seed-source: RPC unreachable, so the seed-source probe is deferred — a real deploy verifies ${seedSource} answers seed(uint256,address) with 32 bytes first.`));
4403
+ return seedSource;
4404
+ }
4405
+ if (probe.verdict !== 'ok')
4406
+ refuseUnusableSeedSource(probe, { flag: raw === undefined ? 'ABX_SEED_SOURCE' : '--seed-source', chainLabel: CHAIN });
4407
+ info(`seed source: CUSTOM ${seedSource} ${dim('— answers seed(uint256,address) with 32 bytes (probed). Its randomness properties are yours to state to buyers; ABX makes no claim about them.')}`);
4408
+ return seedSource;
4409
+ }
4273
4410
  //# sourceMappingURL=deploy.js.map