@artblocks/abx-cli 0.1.0-alpha.20 → 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 (82) hide show
  1. package/CHANGELOG.md +1081 -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 +523 -117
  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 +171 -15
  14. package/dist/commands/project.js.map +1 -1
  15. package/dist/commands/reads.d.ts.map +1 -1
  16. package/dist/commands/reads.js +35 -5
  17. package/dist/commands/reads.js.map +1 -1
  18. package/dist/commands/scaffold.d.ts +3 -1
  19. package/dist/commands/scaffold.d.ts.map +1 -1
  20. package/dist/commands/scaffold.js +81 -29
  21. package/dist/commands/scaffold.js.map +1 -1
  22. package/dist/commands/service.js +1 -1
  23. package/dist/commands/service.js.map +1 -1
  24. package/dist/commands/storage.d.ts.map +1 -1
  25. package/dist/commands/storage.js +11 -3
  26. package/dist/commands/storage.js.map +1 -1
  27. package/dist/commands/submit-app.d.ts +58 -0
  28. package/dist/commands/submit-app.d.ts.map +1 -0
  29. package/dist/commands/submit-app.js +512 -0
  30. package/dist/commands/submit-app.js.map +1 -0
  31. package/dist/config.d.ts +1 -15
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +18 -1
  34. package/dist/config.js.map +1 -1
  35. package/dist/flag-allowlists.d.ts +53 -0
  36. package/dist/flag-allowlists.d.ts.map +1 -0
  37. package/dist/flag-allowlists.js +152 -0
  38. package/dist/flag-allowlists.js.map +1 -0
  39. package/dist/flags.d.ts +6 -2
  40. package/dist/flags.d.ts.map +1 -1
  41. package/dist/flags.js +23 -0
  42. package/dist/flags.js.map +1 -1
  43. package/dist/kind.d.ts +8 -3
  44. package/dist/kind.d.ts.map +1 -1
  45. package/dist/kind.js +25 -0
  46. package/dist/kind.js.map +1 -1
  47. package/dist/main.js +166 -53
  48. package/dist/main.js.map +1 -1
  49. package/dist/mintpage.d.ts.map +1 -1
  50. package/dist/mintpage.js +29 -4
  51. package/dist/mintpage.js.map +1 -1
  52. package/dist/output.d.ts +32 -1
  53. package/dist/output.d.ts.map +1 -1
  54. package/dist/output.js +74 -9
  55. package/dist/output.js.map +1 -1
  56. package/dist/ownerops.d.ts +101 -13
  57. package/dist/ownerops.d.ts.map +1 -1
  58. package/dist/ownerops.js +421 -71
  59. package/dist/ownerops.js.map +1 -1
  60. package/dist/preview.d.ts +1 -1
  61. package/dist/preview.js +1 -1
  62. package/dist/remote.d.ts.map +1 -1
  63. package/dist/remote.js +8 -2
  64. package/dist/remote.js.map +1 -1
  65. package/dist/schema.d.ts +18 -0
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +37 -2
  68. package/dist/schema.js.map +1 -1
  69. package/dist/served.js +1 -1
  70. package/dist/update-check.d.ts +25 -0
  71. package/dist/update-check.d.ts.map +1 -1
  72. package/dist/update-check.js +49 -9
  73. package/dist/update-check.js.map +1 -1
  74. package/package.json +6 -6
  75. package/skill/SKILL.md +64 -32
  76. package/skill/reference/code-projects.md +98 -22
  77. package/skill/reference/creator-token.md +16 -3
  78. package/skill/reference/decisions.md +83 -11
  79. package/skill/reference/hosting.md +1 -1
  80. package/skill/reference/operating.md +35 -12
  81. package/skill/reference/setup.md +1 -1
  82. package/skill/reference/troubleshooting.md +16 -6
@@ -115,6 +115,14 @@ export declare const STATE_ABI: readonly [{
115
115
  readonly outputs: readonly [{
116
116
  readonly type: "address";
117
117
  }];
118
+ }, {
119
+ readonly type: "function";
120
+ readonly name: "seedSource";
121
+ readonly stateMutability: "view";
122
+ readonly inputs: readonly [];
123
+ readonly outputs: readonly [{
124
+ readonly type: "address";
125
+ }];
118
126
  }];
119
127
  /** The edition twin of {@link STATE_ABI} — `owner`/`maxInvocations`/`paused`/`minter`/
120
128
  * `primaryPayee`/`tokenURIRenderer`/`royaltyInfo` are shared, unchanged, function names (see
@@ -222,6 +230,14 @@ export declare const EDITION_STATE_ABI: readonly [{
222
230
  readonly outputs: readonly [{
223
231
  readonly type: "address";
224
232
  }];
233
+ }, {
234
+ readonly type: "function";
235
+ readonly name: "seedSource";
236
+ readonly stateMutability: "view";
237
+ readonly inputs: readonly [];
238
+ readonly outputs: readonly [{
239
+ readonly type: "address";
240
+ }];
225
241
  }];
226
242
  export declare function cmdState(address: Address | undefined, flags: Flags): Promise<void>;
227
243
  export declare function cmdStateBody(address: Address, flags: Flags, emit: (p: Record<string, unknown>) => void): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,OAAO,EA0Bb,MAAM,oBAAoB,CAAC;AAgB5B,OAAO,EAAC,KAAK,KAAK,EAA0B,MAAM,aAAa,CAAC;AAKhE,OAAO,EACL,KAAK,YAAY,EAalB,MAAM,cAAc,CAAC;AAQtB,wBAAsB,UAAU,CAAC,KAAK,EAAE,KAAK,iBA8E5C;AAMD,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBA2FtE;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBAoCxE;AAMD,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBAazE;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GACzC,OAAO,CAAC,IAAI,CAAC,CA0Mf;AAMD,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,iBA+F3E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CnH;AAKD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaZ,CAAC;AAEX;;;;;oGAKoG;AACpG,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAapB,CAAC;AAEX,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBASxE;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GACzC,OAAO,CAAC,IAAI,CAAC,CA+Hf;AAgFD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBAqDzE;AAED;0EAC0E;AAC1E,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,iBAgErG;AAKD,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBA2BzE"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,OAAO,EA4Bb,MAAM,oBAAoB,CAAC;AAgB5B,OAAO,EAAC,KAAK,KAAK,EAA0B,MAAM,aAAa,CAAC;AAMhE,OAAO,EACL,KAAK,YAAY,EAalB,MAAM,cAAc,CAAC;AAQtB,wBAAsB,UAAU,CAAC,KAAK,EAAE,KAAK,iBA+E5C;AAMD,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBA2FtE;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBAoCxE;AAMD,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBAazE;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GACzC,OAAO,CAAC,IAAI,CAAC,CAmQf;AAMD,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,iBA+F3E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CnH;AAqBD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiBZ,CAAC;AAEX;;;;;oGAKoG;AACpG,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAepB,CAAC;AAEX,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBASxE;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GACzC,OAAO,CAAC,IAAI,CAAC,CAqKf;AAgHD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBAqDzE;AAED;0EAC0E;AAC1E,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,iBAgErG;AAKD,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,iBA2BzE"}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { resolve as resolvePath } from 'node:path';
9
- import { AUTH_OPTIONS, CREATOR_TOKEN_INTERFACE_ID, PARAM_TYPES, hasOnChainUriLane, isCodeProject, isCurrentFactory, isCurrentOneOfOneEditionFactory, makePublicClient, makeWalletClient, normalizeAttributes, onChainUriReport, parseTraitPairs, predictClone, readParamSchema, readParamSchemaKeys, readSetParamKeys, resolveChain, resolveSeriesCodeFactory, saltFor, saltGuard, sleep, tryReadContract, } from '@artblocks/abx-sdk';
9
+ import { AUTH_OPTIONS, CREATOR_TOKEN_INTERFACE_ID, PARAM_TYPES, hasOnChainUriLane, isCodeProject, isCurrentFactory, isCurrentOneOfOneEditionFactory, makePublicClient, makeWalletClient, normalizeAttributes, onChainUriReport, parseTraitPairs, predictClone, readParamHooks, readParamHooksLocked, readParamSchema, readParamSchemaKeys, readSetParamKeys, resolveChain, resolveSeriesCodeFactory, saltFor, saltGuard, sleep, tryReadContract, } from '@artblocks/abx-sdk';
10
10
  import { resolveBackend } from '@artblocks/abx-storage';
11
11
  import { currentRenderArtifact, verifyProject } from '@artblocks/abx-token-api';
12
12
  import { zeroAddress } from 'viem';
@@ -15,6 +15,7 @@ import { isDryRun, parseSaltFlag } from '../flags.js';
15
15
  import { jsonSafe, withJson } from '../jsonout.js';
16
16
  import { allowLargeScan, bold, c, detectCanonicalFactory, dim, g, info, ok, registerAndIndexLocally, resolveScanFloor, warn } from '../output.js';
17
17
  import { parseCopies } from './deploy.js';
18
+ import { canonicalSeedSource } from '../ownerops.js';
18
19
  import { detectTokenKind, describeKind } from '../kind.js';
19
20
  import { canonicalLabel, describeRemoteError, indexErrorAction, remoteFlag, reportRemoteIndexing, requireRemoteToken, rollUp, serviceClient, statusLabel, statusLine, statusRow, tokenSourceLabel, } from '../remote.js';
20
21
  import { describeSchema } from '../schema.js';
@@ -66,9 +67,10 @@ export async function cmdPredict(flags) {
66
67
  }
67
68
  // The current-factory guard is the 1/1 impl-version check (721 or its edition twin); the Series/
68
69
  // Series-edition/code/code-edition lanes verify their own factory at deploy time instead —
69
- // EditionCodeFactory mirrors SeriesCodeFactory's stance here (no version probe: a link-time-linked
70
- // factory has no CREATE2-deterministic address to self-heal to, so there's nothing this check would
71
- // catch that the real deploy doesn't already). predictDeterministicAddress works on any factory ABI.
70
+ // EditionCodeFactory mirrors SeriesCodeFactory's stance here (no version probe: a configured code
71
+ // factory with code is always accepted, and one found at its CREATE2 address is bytecode-bound to
72
+ // this build anyway, so there's nothing this check would catch that the real deploy doesn't
73
+ // already). predictDeterministicAddress works on any factory ABI.
72
74
  if (lane.cmd === 'deploy') {
73
75
  const current = isEdition ? await isCurrentOneOfOneEditionFactory(publicClient, factory) : await isCurrentFactory(publicClient, factory);
74
76
  if (!current) {
@@ -338,6 +340,27 @@ export async function cmdVerifyBody(address, flags, emit) {
338
340
  info(state.paramHooks?.augmentHook
339
341
  ? `live data: augment hook ${state.paramHooks.augmentHook} — live view reads chain per view; stills snapshot settled state`
340
342
  : 'live data: none (settled params only — the live view makes zero extra chain reads)');
343
+ // Immutability of the WORK itself — the on-chain program (script chunks) and its library set.
344
+ // This is the gap `lock-field`/`lock-uri` don't cover: those freeze metadata, but the owner can
345
+ // still call `setScriptChunk`/`removeLastScriptChunk` until `lock-script` is sent. Report it so a
346
+ // "locked" drop isn't quietly still-mutable in the one place that carries the work.
347
+ verifyReport.script = state.script ? { locked: !!state.script.locked } : null;
348
+ verifyReport.dependencies = state.dependencies
349
+ ? { count: state.dependencies.list.length, locked: !!state.dependencies.locked }
350
+ : null;
351
+ emit(verifyReport);
352
+ if (state.script) {
353
+ if (state.script.locked)
354
+ ok('script: locked — the program bytes are frozen permanently (setScriptChunk reverts)');
355
+ else
356
+ console.log(` ${c.orange}⚠${c.reset} script: UNLOCKED — the owner can still change the program. Freeze the work with ${bold(`abx lock-script ${address}`)} ${dim("(lock-field/lock-uri don't cover the script)")}`);
357
+ }
358
+ if (state.dependencies && state.dependencies.list.length > 0) {
359
+ if (state.dependencies.locked)
360
+ ok('dependencies: locked — the library set is frozen');
361
+ else
362
+ info(`dependencies: unlocked — freeze the list + registry pointer with \`abx lock-dependencies ${address}\` (that pins WHICH library each ref means; a Registry ref's bytes still live in the registry)`);
363
+ }
341
364
  // The other two param hooks (if wired) — surfaced here since this is where hooks show. Quiet when
342
365
  // unset (the common case). Manage all three with `abx set-param-hooks`.
343
366
  if (state.paramHooks?.configureHook || state.paramHooks?.transferHook) {
@@ -346,7 +369,26 @@ export async function cmdVerifyBody(address, flags, emit) {
346
369
  parts.push(`configure ${state.paramHooks.configureHook}`);
347
370
  if (state.paramHooks.transferHook)
348
371
  parts.push(`transfer ${state.paramHooks.transferHook}`);
349
- info(`param hooks: ${parts.join(' · ')} ${dim('(configure = write-time veto · transfer = ownership-change lifecycle; manage with `abx set-param-hooks`)')}`);
372
+ info(`param hooks: ${parts.join(' · ')} ${dim('(configure = write-time veto · transfer = a VETO over transfers AND mints; manage with `abx set-param-hooks`)')}`);
373
+ }
374
+ // The hook lock is reported for every project with the surface, set hooks or not: a frozen empty
375
+ // trio is a real guarantee ("this project can never add a transfer veto"), and an unfrozen one is
376
+ // a live power even when no hook is set today. Folded from `ParamHooksFrozen`.
377
+ if (state.paramHooks) {
378
+ verifyReport.paramHooks = {
379
+ configureHook: state.paramHooks.configureHook,
380
+ augmentHook: state.paramHooks.augmentHook,
381
+ transferHook: state.paramHooks.transferHook,
382
+ locked: state.paramHooks.locked,
383
+ };
384
+ if (state.paramHooks.locked)
385
+ ok('param hooks: frozen — no hook address can change again (no transfer veto can ever be armed)');
386
+ else if (state.paramHooks.transferHook) {
387
+ console.log(` ${c.orange}⚠${c.reset} param hooks: UNLOCKED, and a transfer hook is armed — it can block transfers and mints, and the owner can re-point it. Freeze the set with ${bold(`abx lock-param-hooks ${address}`)}`);
388
+ }
389
+ else {
390
+ info(`param hooks: unlocked — no hook is set, but the owner can still add one (a transfer hook can block transfers/mints). Freeze with \`abx lock-param-hooks ${address}\``);
391
+ }
350
392
  }
351
393
  // This checks THIS machine's store. A render PUBLISHED to a hosted resolver (locator bridge) lives
352
394
  // on that resolver, NOT here — so a "not found" below can be a false negative for a hosted drop.
@@ -391,6 +433,14 @@ export async function cmdVerifyBody(address, flags, emit) {
391
433
  // what the animation_url actually is. All eth_calls; no server, no local store involved.
392
434
  if (hasOnChainUriLane(state)) {
393
435
  const client = makePublicClient({ chainKey: CHAIN });
436
+ // Is this actually a PROGRAM, or a static image that merely resolves on-chain? `hasOnChainUriLane`
437
+ // is true whenever a tokenURI renderer is set — which it is for EVERY fully-on-chain project,
438
+ // image or code. So an image drop ran the code-project checks and ended in two ⚠ ("generator
439
+ // reports NO code", "no animation_url") that are the DESIGN for a static image, not a defect.
440
+ // Two cold agents in one sweep flagged those warnings as the worst thing about `verify`: the
441
+ // command the skill points at to answer "did my on-chain drop work?" was ending in warnings on a
442
+ // drop that is completely fine, which teaches people to discount its warnings generally.
443
+ const isCodeProject = state.contractType === 'code' || state.contractType === 'edition-code';
394
444
  try {
395
445
  const report = await onChainUriReport(client, state, flags.generator);
396
446
  const { status, probe } = report;
@@ -409,8 +459,15 @@ export async function cmdVerifyBody(address, flags, emit) {
409
459
  info(`on-chain URI lane: generator ${report.generator}${state.tokenURIRenderer ? ` · tokenURI renderer ${state.tokenURIRenderer}` : ' · tokenURIRenderer NOT set (animation field only — tokenURI still resolves off-chain)'}`);
410
460
  if (status.branchName === 'template') {
411
461
  ok(`generator branch: template — the document assembles from the on-chain script chunks`);
412
- if (status.chainComplete)
413
- ok('chain-complete — every dependency resolves to proven on-chain bytes; no server, gateway, or CDN in the graph');
462
+ if (status.chainComplete) {
463
+ ok('chain-complete — every dependency resolves to on-chain bytes; no server, gateway, or CDN in the graph');
464
+ // chainComplete is a SERVING claim, not an immutability one. A `Registry` dependency is
465
+ // fetched live from the registry contract on every read, so its bytes can change after
466
+ // `lock-dependencies` froze the pointer — and this flag reported `true` on both sides of
467
+ // exactly that swap during the audit. Say what it means rather than letting "complete" read
468
+ // as "finished".
469
+ info(dim(' chain-complete describes WHERE the bytes come from, not that they are frozen: a Registry dependency is re-fetched from the registry on every read, so its bytes can change even with lock-dependencies set. Locks freeze this contract; they cannot freeze another one.'));
470
+ }
414
471
  else
415
472
  info('not chain-complete — CDN-served or unresolved dependencies (CDN entries serve fine; on-chain bytes are the durability floor)');
416
473
  for (const ref of status.unresolvedRefs) {
@@ -424,17 +481,21 @@ export async function cmdVerifyBody(address, flags, emit) {
424
481
  else
425
482
  info('URL within the 8KB budget');
426
483
  }
427
- else {
484
+ else if (isCodeProject) {
428
485
  console.log(` ${c.orange}⚠${c.reset} generator reports NO code (neither script chunks nor a code field) — animation_url degrades to an <!-- abx:no-code --> marker`);
429
486
  }
487
+ else {
488
+ // A static image drop has no program by design — stating that is fine, warning about it is not.
489
+ info('no program on this project — a static image drop, so the metadata carries no animation_url (expected, not a gap)');
490
+ }
430
491
  if (!probe) {
431
492
  info('tokenURI probe skipped — no token minted yet (mint token #0, then re-run)');
432
493
  }
433
494
  else if (!probe.onChainJson) {
434
- console.log(` ${c.red}✗${c.reset} tokenURI(${probe.tokenId}) is NOT an on-chain data:application/json;base64 URI — got "${probe.uriPrefix}…"`);
495
+ console.log(` ${c.red}✗${c.reset} ${probe.accessor}(${probe.tokenId}) is NOT an on-chain data:application/json;base64 URI — got "${probe.uriPrefix}…"`);
435
496
  }
436
497
  else {
437
- ok(`tokenURI(${probe.tokenId}) resolves ON-CHAIN: data:application/json;base64 — decoded from the contract, no server`);
498
+ ok(`${probe.accessor}(${probe.tokenId}) resolves ON-CHAIN: data:application/json;base64 — decoded from the contract, no server`);
438
499
  const a = probe.animation;
439
500
  if (a.form === 'data-html') {
440
501
  if (a.marker)
@@ -448,9 +509,10 @@ export async function cmdVerifyBody(address, flags, emit) {
448
509
  else if (a.form === 'data-other') {
449
510
  console.log(` ${c.orange}⚠${c.reset} animation_url is a data: URI but not text/html — got "${a.prefix}…"`);
450
511
  }
451
- else {
512
+ else if (isCodeProject) {
452
513
  console.log(` ${c.orange}⚠${c.reset} no animation_url in the on-chain JSON — the generator field may be missing or unrenderable`);
453
514
  }
515
+ // else: a static image has no animation_url and is not supposed to — say nothing.
454
516
  }
455
517
  // The READ-side envelope. Params enumerate on-chain, so the write side is unbounded — but
456
518
  // tokenURI and tokenData assemble EVERY enumerated param per call, and that is what grows.
@@ -466,7 +528,10 @@ export async function cmdVerifyBody(address, flags, emit) {
466
528
  }
467
529
  }
468
530
  catch (e) {
469
- console.log(` ${c.orange}⚠${c.reset} on-chain URI check unavailable: ${e.message}`);
531
+ // First line only. A viem read error carries a multi-line dump (Contract Call / args / Docs /
532
+ // Version) that is meaningless to a creator and buried the actual sentence when this fired.
533
+ const first = String(e.message ?? e).split('\n')[0].trim();
534
+ console.log(` ${c.orange}⚠${c.reset} on-chain URI check unavailable: ${first}`);
470
535
  }
471
536
  }
472
537
  if (renderGap)
@@ -641,6 +706,22 @@ export async function reportRemoteByteIntegrity(address, remote, base) {
641
706
  // ── state: a one-glance operational snapshot of a deployed contract, read straight from chain ──
642
707
  // Read-only (no tx, no local index). Series-only getters revert on a 1/1, so each read is defensive
643
708
  // → the same command works for both. The agent-friendly "what's the state before/after an op" call.
709
+ /**
710
+ * The `seed source` readout — three states, and the distinction between them is the whole reason the
711
+ * line exists. `0x0` is a code project that deliberately draws no mint seed; the canonical address is
712
+ * the shared pseudorandom `AbxSeedSource` (whose properties are documented and NOT lottery-grade);
713
+ * anything else is the creator's own contract, about which ABX knows and claims nothing. A buyer or an
714
+ * agent reading this needs to be able to tell "the standard one" from "someone's custom randomness"
715
+ * at a glance — flattening them to a bare address hides exactly the fact worth surfacing.
716
+ */
717
+ function describeSeedSource(source) {
718
+ if (source === zeroAddress)
719
+ return dim('none (0x0 — no mint-time seed drawn at mint)');
720
+ if (source.toLowerCase() === canonicalSeedSource(resolveChain(CHAIN).id).toLowerCase()) {
721
+ return `${source} ${dim('(canonical AbxSeedSource — pseudorandom, not lottery-grade)')}`;
722
+ }
723
+ return `${source} ${dim('— CUSTOM IAbxSeedSource (not the canonical one; its randomness properties are the project’s to state)')}`;
724
+ }
644
725
  export const STATE_ABI = [
645
726
  { type: 'function', name: 'owner', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
646
727
  { type: 'function', name: 'totalSupply', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
@@ -654,6 +735,10 @@ export const STATE_ABI = [
654
735
  // ERC-721C (creator token) — ERC-165 advertises ICreatorToken ONLY when enrolled at deploy.
655
736
  { type: 'function', name: 'supportsInterface', stateMutability: 'view', inputs: [{ type: 'bytes4' }], outputs: [{ type: 'bool' }] },
656
737
  { type: 'function', name: 'getTransferValidator', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
738
+ // Seed Source extension — composed by CODE projects only, so absent (undefined) on an image token.
739
+ // Shown because it is the one setting an owner can re-point mid-sale that changes what a LATER
740
+ // buyer receives; `SeedSourceSet` puts it on the spine, and this makes it readable in one command.
741
+ { type: 'function', name: 'seedSource', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
657
742
  ];
658
743
  /** The edition twin of {@link STATE_ABI} — `owner`/`maxInvocations`/`paused`/`minter`/
659
744
  * `primaryPayee`/`tokenURIRenderer`/`royaltyInfo` are shared, unchanged, function names (see
@@ -674,6 +759,8 @@ export const EDITION_STATE_ABI = [
674
759
  // ERC-1155C (creator token) — identical ERC-165 id + getter as 721C; advertised only when enrolled.
675
760
  { type: 'function', name: 'supportsInterface', stateMutability: 'view', inputs: [{ type: 'bytes4' }], outputs: [{ type: 'bool' }] },
676
761
  { type: 'function', name: 'getTransferValidator', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
762
+ // Seed Source extension — EditionCode composes it; the image editions don't. See STATE_ABI's note.
763
+ { type: 'function', name: 'seedSource', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
677
764
  ];
678
765
  export async function cmdState(address, flags) {
679
766
  if (!address || address.startsWith('--')) {
@@ -694,7 +781,7 @@ export async function cmdStateBody(address, flags, emit) {
694
781
  return cmdStateEditionBody(address, publicClient, kind, emit);
695
782
  // getter absent (extension not composed) or contract has no code → undefined, never a throw.
696
783
  const read = (functionName, args = []) => tryReadContract(publicClient, { address, abi: STATE_ABI, functionName, args });
697
- const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator] = await Promise.all([
784
+ const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator, seedSource] = await Promise.all([
698
785
  read('owner'),
699
786
  read('totalSupply'),
700
787
  read('nextTokenId'),
@@ -706,6 +793,7 @@ export async function cmdStateBody(address, flags, emit) {
706
793
  read('royaltyInfo', [0n, 10000n]),
707
794
  read('supportsInterface', [CREATOR_TOKEN_INTERFACE_ID]),
708
795
  read('getTransferValidator'),
796
+ read('seedSource'),
709
797
  ]);
710
798
  if (owner === undefined && totalSupply === undefined) {
711
799
  throw new Error(`no ABX contract state at ${address} on ${CHAIN} (no code, or not an ABX token).`);
@@ -732,6 +820,10 @@ export async function cmdStateBody(address, flags, emit) {
732
820
  royalty: royalty ? { receiver: royalty[0], bps: Number(royalty[1]) } : null,
733
821
  creatorToken: !!creatorToken,
734
822
  transferValidator: creatorToken ? zeroToNull(transferValidator) : null,
823
+ // `undefined` (no extension — an image token) and `0x0` (a code project that opted out) are
824
+ // different facts; the JSON keeps them apart the same way every other getter here does.
825
+ seedSource: zeroToNull(seedSource),
826
+ hasSeedSource: seedSource !== undefined,
735
827
  params: { governed: [], contractScopeSet: [] },
736
828
  });
737
829
  emit(payload);
@@ -758,6 +850,8 @@ export async function cmdStateBody(address, flags, emit) {
758
850
  if (creatorToken) {
759
851
  info(`721C validator ${transferValidator && transferValidator !== zeroAddress ? transferValidator : `${c.orange}suspended${c.reset} ${dim('(zero — enforcement off; re-enable with `abx set-transfer-validator`)')}`}`);
760
852
  }
853
+ if (seedSource !== undefined)
854
+ info(`seed source ${describeSeedSource(seedSource)}`);
761
855
  // The PostParam surface, read straight from chain: `paramSchemaKeys()` is the DECLARED (governed)
762
856
  // set — append-only, and it includes keys declared but never yet written, which nothing off-chain
763
857
  // could otherwise discover — and `contractParamKeys()` is the collection-scope set values. Both
@@ -786,7 +880,7 @@ export async function cmdStateBody(address, flags, emit) {
786
880
  })));
787
881
  emit(payload);
788
882
  if (governed.length) {
789
- console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/artist set these; the generator injects them`)}`);
883
+ console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/creator set these; the generator injects them`)}`);
790
884
  for (const { key, s } of governed) {
791
885
  const retired = s.lockAfter !== 0 && s.lockAfter < now;
792
886
  const locks = s.lockAfter !== 0 && !retired ? dim(` locks ${new Date(s.lockAfter * 1000).toISOString().slice(0, 10)}`) : '';
@@ -802,6 +896,36 @@ export async function cmdStateBody(address, flags, emit) {
802
896
  emit(payload);
803
897
  info(`${dim('contract-scope params set:')} ${[...set.contract].sort().join(', ')} ${dim('(apply to every token)')}`);
804
898
  }
899
+ // The hooks, and whether they can still change. This is a BUYER's read, which is why it lives in
900
+ // `state` next to the validator rather than only in `verify`: the transfer hook is a veto over
901
+ // transfers and mints, so "which hooks, and are they frozen" is exactly the pair someone needs
902
+ // before buying. Printed whenever the surface exists — including with all three unset, because
903
+ // "none, and frozen" is the strongest thing this line can say and it must be visible.
904
+ const hooks = await readParamHooks(publicClient, address);
905
+ if (hooks) {
906
+ const locked = await readParamHooksLocked(publicClient, address);
907
+ const shown = [['configure', hooks.configureHook], ['augment', hooks.augmentHook], ['transfer', hooks.transferHook]]
908
+ .filter(([, a]) => a !== zeroAddress)
909
+ .map(([role, a]) => `${role} ${a}`);
910
+ payload.params.hooks = jsonSafe({
911
+ configureHook: hooks.configureHook === zeroAddress ? null : hooks.configureHook,
912
+ augmentHook: hooks.augmentHook === zeroAddress ? null : hooks.augmentHook,
913
+ transferHook: hooks.transferHook === zeroAddress ? null : hooks.transferHook,
914
+ // `null` = could not be established (see readParamHooksLocked). Never collapse it to false:
915
+ // "unknown" and "the owner can still arm a transfer veto" are different answers.
916
+ locked: locked ?? null,
917
+ });
918
+ emit(payload);
919
+ const lockNote = locked === true
920
+ ? `${c.green}frozen${c.reset} ${dim('— no hook address can ever change again')}`
921
+ : locked === false
922
+ ? `${c.orange}not frozen${c.reset} ${dim('— the owner can re-point these (`abx lock-param-hooks` freezes them forever)')}`
923
+ : dim('freeze state unknown (the node refused the check)');
924
+ info(`param hooks ${shown.length ? shown.join(' · ') : dim('none set')} ${lockNote}`);
925
+ if (hooks.transferHook !== zeroAddress) {
926
+ info(dim(` the transfer hook is a VETO: if it reverts, the transfer fails — and mints too (mint = transfer from 0x0).`));
927
+ }
928
+ }
805
929
  }
806
930
  catch {
807
931
  /* best-effort — a non-code project has no params surface to read */
@@ -817,7 +941,7 @@ export async function cmdStateBody(address, flags, emit) {
817
941
  */
818
942
  async function cmdStateEditionBody(address, publicClient, kind, emit) {
819
943
  const read = (functionName, args = []) => tryReadContract(publicClient, { address, abi: EDITION_STATE_ABI, functionName, args });
820
- const [owner, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator] = await Promise.all([
944
+ const [owner, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator, seedSource] = await Promise.all([
821
945
  read('owner'),
822
946
  read('maxInvocations'), // absent on a 1/1-edition — its id space is fixed to {0}
823
947
  read('paused'),
@@ -827,6 +951,7 @@ async function cmdStateEditionBody(address, publicClient, kind, emit) {
827
951
  read('royaltyInfo', [0n, 10000n]),
828
952
  read('supportsInterface', [CREATOR_TOKEN_INTERFACE_ID]),
829
953
  read('getTransferValidator'),
954
+ read('seedSource'),
830
955
  ]);
831
956
  if (owner === undefined)
832
957
  throw new Error(`no ABX contract state at ${address} on ${CHAIN} (no code, or not an ABX token).`);
@@ -853,6 +978,8 @@ async function cmdStateEditionBody(address, publicClient, kind, emit) {
853
978
  royalty: royalty ? { receiver: royalty[0], bps: Number(royalty[1]) } : null,
854
979
  creatorToken: !!creatorToken,
855
980
  transferValidator: creatorToken ? zeroToNull(transferValidator) : null,
981
+ seedSource: zeroToNull(seedSource),
982
+ hasSeedSource: seedSource !== undefined,
856
983
  });
857
984
  emit(payload);
858
985
  console.log(bold(`\n ${describeKind(kind)} state`) + dim(` ${address} · ${CHAIN}`));
@@ -875,6 +1002,35 @@ async function cmdStateEditionBody(address, publicClient, kind, emit) {
875
1002
  if (creatorToken) {
876
1003
  info(`1155C validator ${transferValidator && transferValidator !== zeroAddress ? transferValidator : `${c.orange}suspended${c.reset} ${dim('(zero — enforcement off; re-enable with `abx set-transfer-validator`)')}`}`);
877
1004
  }
1005
+ if (seedSource !== undefined)
1006
+ info(`seed source ${describeSeedSource(seedSource)}`);
1007
+ // EditionCode composes ConfigurableParams exactly like SeriesCode, so an edition has hooks too —
1008
+ // and the transfer hook's veto reaches a mint, which on this lane is a buyer picking an id. Report
1009
+ // the same pair as the 721 body (which hooks, and whether they can still change); silent for the
1010
+ // image/1-of-1 edition twins, which have no params surface at all.
1011
+ const hooks = await readParamHooks(publicClient, address);
1012
+ if (hooks) {
1013
+ const locked = await readParamHooksLocked(publicClient, address);
1014
+ const shown = [['configure', hooks.configureHook], ['augment', hooks.augmentHook], ['transfer', hooks.transferHook]]
1015
+ .filter(([, a]) => a !== zeroAddress)
1016
+ .map(([role, a]) => `${role} ${a}`);
1017
+ payload.paramHooks = jsonSafe({
1018
+ configureHook: hooks.configureHook === zeroAddress ? null : hooks.configureHook,
1019
+ augmentHook: hooks.augmentHook === zeroAddress ? null : hooks.augmentHook,
1020
+ transferHook: hooks.transferHook === zeroAddress ? null : hooks.transferHook,
1021
+ locked: locked ?? null, // null = unknown; never collapse it to false
1022
+ });
1023
+ emit(payload);
1024
+ const lockNote = locked === true
1025
+ ? `${c.green}frozen${c.reset} ${dim('— no hook address can ever change again')}`
1026
+ : locked === false
1027
+ ? `${c.orange}not frozen${c.reset} ${dim('— the owner can re-point these (`abx lock-param-hooks` freezes them forever)')}`
1028
+ : dim('freeze state unknown (the node refused the check)');
1029
+ info(`param hooks ${shown.length ? shown.join(' · ') : dim('none set')} ${lockNote}`);
1030
+ if (hooks.transferHook !== zeroAddress) {
1031
+ info(dim(` the transfer hook is a VETO: if it reverts, the transfer fails — and mints too (mint = transfer from 0x0).`));
1032
+ }
1033
+ }
878
1034
  console.log('');
879
1035
  }
880
1036
  /**