@artblocks/abx-cli 0.1.0-alpha.19 → 0.1.0-alpha.20

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 (44) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/commands/deploy.d.ts +32 -7
  3. package/dist/commands/deploy.d.ts.map +1 -1
  4. package/dist/commands/deploy.js +1079 -49
  5. package/dist/commands/deploy.js.map +1 -1
  6. package/dist/commands/project.d.ts +107 -0
  7. package/dist/commands/project.d.ts.map +1 -1
  8. package/dist/commands/project.js +141 -16
  9. package/dist/commands/project.js.map +1 -1
  10. package/dist/commands/reads.d.ts.map +1 -1
  11. package/dist/commands/reads.js +52 -20
  12. package/dist/commands/reads.js.map +1 -1
  13. package/dist/commands/scaffold.d.ts +13 -11
  14. package/dist/commands/scaffold.d.ts.map +1 -1
  15. package/dist/commands/scaffold.js +73 -37
  16. package/dist/commands/scaffold.js.map +1 -1
  17. package/dist/config.d.ts +8 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +18 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/kind.d.ts +48 -0
  22. package/dist/kind.d.ts.map +1 -0
  23. package/dist/kind.js +69 -0
  24. package/dist/kind.js.map +1 -0
  25. package/dist/main.js +107 -23
  26. package/dist/main.js.map +1 -1
  27. package/dist/mintpage.d.ts +14 -1
  28. package/dist/mintpage.d.ts.map +1 -1
  29. package/dist/mintpage.js +166 -4
  30. package/dist/mintpage.js.map +1 -1
  31. package/dist/output.d.ts +11 -0
  32. package/dist/output.d.ts.map +1 -1
  33. package/dist/output.js +133 -3
  34. package/dist/output.js.map +1 -1
  35. package/dist/ownerops.d.ts +33 -2
  36. package/dist/ownerops.d.ts.map +1 -1
  37. package/dist/ownerops.js +356 -19
  38. package/dist/ownerops.js.map +1 -1
  39. package/package.json +6 -6
  40. package/skill/SKILL.md +5 -3
  41. package/skill/reference/creator-token.md +40 -25
  42. package/skill/reference/hosting.md +1 -1
  43. package/skill/reference/operating.md +20 -0
  44. package/skill/reference/troubleshooting.md +7 -0
@@ -15,15 +15,16 @@
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, discoverDeployBlock, encodeFieldRenderer, encodeTag, expectedChainComplete, makeHotSender, makePublicClient, makeWalletClient, normalizeAttributes, onchainUriSetupCalls, oneOfOneImageAbi, parseTraitPairs, planResume, predictClone, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, 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, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, prepareDeployOneOfOneEdition, prepareDeployEditionImage, prepareDeployEditionCode, redactRpcUrl, resolveChain, resolveDepRegistryPointer, resolveGenerator, 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
20
  import { DEFAULT_PORT, artContentHash, generateArt, resolveBaseUrl, startTokenApiServer } from '@artblocks/abx-token-api';
21
21
  import { encodeFunctionData, toHex, zeroAddress } from 'viem';
22
- import { CHAIN, explorerBase, assertTurboFundsForUpload, backendResolution, collectContentLocators, ensureArweaveIdentityForUpload, factoryAddress, faucetHint, localIndexer, loopbackBaseUrl, noteArweavePlan, noteStorageReadiness, rendererAddress, seriesFactoryAddress, storageOptions, storageOverrides, storageSignerChoice, } from '../config.js';
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
24
  import { isDryRun, parseSaltFlag, refuseStrayFlags, warnSignWithoutFor } from '../flags.js';
25
+ import { isEditionContract } from '../kind.js';
25
26
  import { jsonSafe, withJson } from '../jsonout.js';
26
- import { bold, c, dim, ensureFactory, ensureRenderer, ensureSeedSource, ensureSeriesCodeFactory, ensureSeriesFactory, g, info, keepAlive, ok, p, portInUse, printServing, registerAndIndexLocally, reindexAfterDeploy, step, warn, } from '../output.js';
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';
27
28
  import { AUTHORSHIP_DEPLOY_FIELDS, ONCHAIN_PROJECT_SOFT_LIMIT, authorshipContractFields, computeContentPlan, envStagingSender, parseCompress, parseTransferValidatorValue, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, } from '../ownerops.js';
28
29
  import { canonicalLabel } from '../remote.js';
29
30
  import { confirmSend, gatedSend, laneFromFlags } from '../riskgate.js';
@@ -31,9 +32,11 @@ import { describeSchema, parseSchemaSpecs } from '../schema.js';
31
32
  import { parseSeriesTraits } from '../series-traits.js';
32
33
  import { decodeOnChainJson } from '../served.js';
33
34
  import { openWalletSession, signHotSequence, signTx } from '../signer.js';
34
- // Product dimensions — what you can launch. One concrete contract exists today
35
- // (the 1/1 image); this table is the seam future implementations slot into, so
36
- // `abx deploy --type <dimension>` is stable while the contracts grow under it.
35
+ // Product dimensions — what you can launch. Two concrete contract FAMILIES exist today: the 721
36
+ // ladder (`--type`, below — 1/1 image or Series) and, orthogonally, ERC-1155 editions (`--copies`,
37
+ // on ANY of the three deploy commands see `parseCopies`). `--type` picks among unique-token
38
+ // shapes; `--copies` switches uniqueness for copies of the same shape. They are independent knobs,
39
+ // which is why editions get no entry of their own here rather than a fourth dimension.
37
40
  export const DIMENSIONS = {
38
41
  '1of1': { label: '1/1 image NFT', aliases: ['one-of-one', 'oneofone', '1-of-1'] },
39
42
  series: { label: 'multi-token image Series', aliases: ['multi', 'collection'] },
@@ -46,6 +49,43 @@ export function resolveDimension(type) {
46
49
  }
47
50
  return key;
48
51
  }
52
+ // ── --copies: the flag that routes a deploy to its ERC-1155 edition twin ─────────────────────────
53
+ // The kernel (specs/protocol — the parity plan): a Series is many UNIQUE tokens; an Edition is many
54
+ // COPIES of a token. The creator's trigger word is "copies" — nobody says "ERC-1155" or "1155",
55
+ // and this flag is the only surface that exists: no `--standard` flag, no separate top-level
56
+ // commands. Absent → the 721 lane, byte-identical to before `--copies` existed.
57
+ /** Parse `--copies <N|open>` into the `editionSize` InitParams field (`0n` = open/uncapped). Throws
58
+ * with a pointed message on anything else — a bare `--copies` (no value), a non-integer, or the
59
+ * literal `0` (which already has an on-chain meaning — "open" says the same thing without the
60
+ * ambiguity of "did they mean zero copies, or unlimited?"). */
61
+ export function parseCopies(raw) {
62
+ if (raw === undefined)
63
+ throw new Error('--copies needs a value: a positive integer (copies per id), or "open" for an uncapped edition.');
64
+ const v = raw.trim();
65
+ if (v === 'true')
66
+ throw new Error('--copies needs a value: a positive integer (copies per id), or "open" for an uncapped edition — a bare --copies has neither.');
67
+ if (v.toLowerCase() === 'open')
68
+ return 0n;
69
+ if (!/^\d+$/.test(v))
70
+ throw new Error(`--copies must be a positive integer (copies per id) or "open" (uncapped); got '${raw}'.`);
71
+ if (v === '0')
72
+ throw new Error(`--copies 0 is ambiguous with the on-chain meaning of 0 (open/uncapped) — say what you mean: --copies open.`);
73
+ return BigInt(v);
74
+ }
75
+ /** `--copies 1` is legal — a single-copy ERC-1155 edition — but it is almost never what a creator
76
+ * actually wants (the plain 721 lane makes a simpler, more marketplace-familiar token for exactly
77
+ * one copy). Advisory only; never refused — `--copies` exists precisely so nobody has to reach for
78
+ * a different flag to get an edition. */
79
+ export function copiesOneNote(cmd) {
80
+ return `--copies 1 makes a single-copy ERC-1155 edition. If you want one unique token, drop --copies — \`abx ${cmd}\` (no --copies) is the simpler, more marketplace-familiar 721 lane.`;
81
+ }
82
+ /** A non-negative-integer flag (`--mint-amount`, …) — the shared parse + bound-check so every
83
+ * edition flag that takes "a count" reports the same, plain-language rejection. */
84
+ export function parseNonNegativeIntFlag(raw, flag) {
85
+ if (!/^\d+$/.test(raw.trim()) || raw.trim() === '')
86
+ throw new Error(`--${flag} must be a non-negative integer; got '${raw}'.`);
87
+ return BigInt(raw.trim());
88
+ }
49
89
  /**
50
90
  * A `--dry-run` computes the deterministic deploy address, which is a pure function of
51
91
  * (factory, salt, deployer) — so it needs a deployer even though it signs nothing. Resolve it the
@@ -468,6 +508,17 @@ export function describeTraits(traits, onchain) {
468
508
  }
469
509
  // ── deploy (+ optionally serve) ──────────────────────────────────────────────
470
510
  export async function cmdDeploy(flags, serveAfter) {
511
+ // `--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
+ // touches `flags`, so the 721 lane (`cmdDeployBody`) below is reached ONLY when `--copies` is
514
+ // absent, and is otherwise untouched by this feature — byte-identical to before editions shipped.
515
+ if (flags.copies !== undefined) {
516
+ if (serveAfter) {
517
+ throw new Error('`abx demo` has no --copies — demo is the zero-argument 721 1/1 walkthrough. Deploy an edition directly: ' +
518
+ '`abx deploy --copies <n|open> [--image <path>] --name "…" --symbol …` (see `abx help deploy`).');
519
+ }
520
+ return withJson(flags, async (emit) => cmdDeployOneOfOneEditionBody(flags, emit));
521
+ }
471
522
  // `--json`: the deployed ADDRESS is the value a program came for (B19). Narration goes to stderr,
472
523
  // so `ADDR=$(abx deploy … --json | jq -r .address)` works without parsing a success banner.
473
524
  return withJson(flags, async (emit) => cmdDeployBody(flags, serveAfter, emit));
@@ -993,7 +1044,11 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
993
1044
  attributes: offChainTraits,
994
1045
  };
995
1046
  indexer.register(baseReg);
996
- const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
1047
+ // Only the demo path (`abx demo`) reaches `walkthroughReadBack` below, and only its off-chain-
1048
+ // custody branch reads `token.tokenURI` off `state` — the on-chain-URI branch there does its own
1049
+ // independent `readContract`, so skip the ask when `onChainUri` too. A real `abx deploy` never
1050
+ // asks for it (SDK default: off).
1051
+ const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone, { readUriDocuments: serveAfter && !onChainUri });
997
1052
  if (serveAfter) {
998
1053
  if (state.eventCount > 0)
999
1054
  ok(`read ${bold(String(state.eventCount))} events straight off ${CHAIN} in ${elapsedMs}ms — no API key, no company's server`);
@@ -1092,6 +1147,309 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
1092
1147
  printServing(url, clone);
1093
1148
  keepAlive();
1094
1149
  }
1150
+ // ── deploy --copies: OneOfOneEdition (the flagship ERC-1155 product) ─────────────────────────────
1151
+ // Copies of ONE artwork — a priced open/limited edition. The edition twin of `cmdDeployBody`,
1152
+ // reusing every standard-agnostic helper it does (content custody, traits, authorship fields, the
1153
+ // `--721c`/`--1155c`-shared validator resolution, salt prediction, signing lanes) but written as its
1154
+ // own function rather than threaded into `cmdDeployBody` with `if (copies)` branches throughout —
1155
+ // so the 721 lane, above, stays provably byte-identical whether or not editions exist.
1156
+ //
1157
+ // v1 scope (reported, not hidden): `--onchain-image` (chunk-store staging) is wired for the hot
1158
+ // lane only — the wallet-lane session dance `cmdDeployBody` uses for it is a bigger lift than this
1159
+ // pass covers; staging + a wallet-signed deploy in two SEPARATE steps isn't offered here rather than
1160
+ // half-build it. Every other custody/signing combination the 1/1 lane supports works identically.
1161
+ // (`DEPLOY_EDITION_FLAGS` — the allowlist this function's `refuseStrayFlags` checks against — is
1162
+ // declared further down, alongside `DEPLOY_FLAGS`/`DEPLOY_SERIES_FLAGS`/`DEPLOY_CODE_FLAGS`: it
1163
+ // spreads `DEPLOY_FLAGS`, so it must come AFTER that declaration at module scope, same reason
1164
+ // `DEPLOY_SERIES_EDITION_FLAGS` sits next to it rather than beside `cmdDeployEditionImageBody`.)
1165
+ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
1166
+ refuseStrayFlags(flags, DEPLOY_EDITION_FLAGS, 'deploy');
1167
+ warnSignWithoutFor(flags);
1168
+ // `--type` is the 721 lane's OWN dimension seam (cmdDeployBody redirects `--type series` to
1169
+ // `deploy-series`) — it never even reaches this function for that redirect, since --copies routes
1170
+ // 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
+ // `deploy-series --copies`, not this 1/1-edition lane).
1173
+ 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
+ 'use `abx deploy-series --copies <n|open> --dir <folder>` instead. Drop --type here.');
1176
+ }
1177
+ const editionSize = parseCopies(flags.copies);
1178
+ const name = flags.name ?? 'ABX Edition';
1179
+ const symbol = flags.symbol ?? 'ABXE';
1180
+ const royaltyBps = Number(flags['royalty-bps'] ?? 500);
1181
+ const port = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
1182
+ const baseUrl = flags['public-base-url'] ?? resolveBaseUrl(port);
1183
+ const traits = parseDeployTraits(flags);
1184
+ 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`)}`));
1187
+ if (editionSize === 1n)
1188
+ info(copiesOneNote('deploy'));
1189
+ const lane = laneFromFlags(flags);
1190
+ const dryRun = isDryRun(flags);
1191
+ const onchainImage = !!flags['onchain-image'];
1192
+ // Pure flag-combo validation — checked BEFORE any chain read (predict/factory/renderer all touch
1193
+ // the network), same reasoning every other "this combo can never work" refusal in this file uses
1194
+ // (e.g. the localhost-URL checks). Sequential staging (chunk tx → deploy) can't be split across
1195
+ // the wallet-session/cold lanes in this pass (see the module-level scope note) — refuse rather
1196
+ // than half-build it.
1197
+ if (onchainImage && lane !== 'send') {
1198
+ throw new Error('--onchain-image on an edition deploy is wired for the hot lane only right now (a funded ABX_DEPLOYER_PK) — ' +
1199
+ 'drop --onchain-image (off-chain custody or --onchain-uri still work on every lane), or switch to the hot lane. Tracked as a follow-up.');
1200
+ }
1201
+ if (dryRun)
1202
+ assertPreviewDeployer(flags);
1203
+ const publicClient = makePublicClient({ chainKey: CHAIN });
1204
+ await assertChainId(CHAIN, { allowUnreachable: dryRun });
1205
+ // --721c reuses the SAME flag + value grammar to enroll ERC-1155C instead — see the locked
1206
+ // decision in the parity plan (P0): editions ship 1155C at launch with the 721C UX, unchanged.
1207
+ const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun, '1155C');
1208
+ assertRealIdentity(flags, { name, symbol, dryRun });
1209
+ if (!dryRun) {
1210
+ let signer = flags.for;
1211
+ if (!signer && lane === 'send') {
1212
+ try {
1213
+ signer = makeWalletClient({ chainKey: CHAIN }).account.address;
1214
+ }
1215
+ catch {
1216
+ /* no key yet — later steps handle it */
1217
+ }
1218
+ }
1219
+ if (signer)
1220
+ await warnUnfunded(publicClient, signer);
1221
+ }
1222
+ step('Trust anchor');
1223
+ let factory;
1224
+ if (dryRun) {
1225
+ const existing = oneOfOneEditionFactoryAddress(flags.factory);
1226
+ if (!existing) {
1227
+ info('no canonical 1/1-edition factory for this chain yet — a real deploy would deploy the trust anchor first.');
1228
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
1229
+ return;
1230
+ }
1231
+ factory = existing;
1232
+ if (!(await previewFactoryLive(publicClient, factory, '1/1-edition factory')))
1233
+ return;
1234
+ info(`would reuse canonical 1/1-edition factory ${factory}`);
1235
+ }
1236
+ else {
1237
+ factory = await ensureOneOfOneEditionFactory(flags.factory, !!flags['bootstrap-factory']);
1238
+ }
1239
+ const hasPublicUrl = !!(flags['public-base-url'] || process.env.ABX_PUBLIC_BASE_URL);
1240
+ const onChainUri = !!flags['onchain-uri'] || onchainImage;
1241
+ const traitsOnchain = traits.length > 0 && (!!flags['traits-onchain'] || onChainUri);
1242
+ let renderer = zeroAddress;
1243
+ if (onChainUri) {
1244
+ step('On-chain renderer');
1245
+ if (dryRun) {
1246
+ renderer = rendererAddress(flags.renderer) ?? zeroAddress;
1247
+ info(renderer === zeroAddress ? 'would deploy the canonical renderer first' : `would use renderer ${renderer}`);
1248
+ }
1249
+ else {
1250
+ renderer = await ensureRenderer(flags.renderer);
1251
+ }
1252
+ }
1253
+ if (!onChainUri) {
1254
+ const loopback = loopbackBaseUrl(baseUrl);
1255
+ if (!hasPublicUrl || loopback) {
1256
+ const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
1257
+ const msg = `Off-chain metadata needs a PUBLIC resolver URL baked on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one. ` +
1258
+ `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`;
1259
+ if (!devAllow) {
1260
+ if (dryRun)
1261
+ warn(`would REFUSE to deploy — ${msg}`);
1262
+ else
1263
+ throw new Error(msg);
1264
+ }
1265
+ else {
1266
+ warn(`DEV ONLY (ABX_DEV_ALLOW_LOCALHOST_URI): baking ${bold(baseUrl)} on-chain — resolves only on THIS machine.`);
1267
+ }
1268
+ }
1269
+ }
1270
+ if (onChainUri && !imageResolvesWithoutServer(flags, onchainImage)) {
1271
+ warn(`${bold('--onchain-uri puts the metadata JSON on-chain, not the image.')} With '${backendResolution(storageOverrides(flags)).backend}' custody the image is only ` +
1272
+ `a keccak256 anchor, so ${bold('uri(0)')} will serve a PLACEHOLDER. Two ways to a real image with no server: ${bold('--backend arweave')} (or ipfs/cloud) bakes the URL on-chain, or ${bold('--onchain-image --compress fastlz')} puts the bytes themselves on-chain.`);
1273
+ }
1274
+ step(`Deploy an edition of "${name}" to ${CHAIN}`);
1275
+ let bakedImage;
1276
+ if (onchainImage) {
1277
+ if (!flags.image)
1278
+ throw new Error('--onchain-image needs --image <path> (the bytes to put on-chain)');
1279
+ step('Stage on-chain image');
1280
+ if (dryRun) {
1281
+ info(previewImageStaging(flags.image, parseCompress(flags.compress)));
1282
+ }
1283
+ else {
1284
+ const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), envStagingSender());
1285
+ bakedImage = field;
1286
+ info(note);
1287
+ }
1288
+ }
1289
+ const explicitSalt = parseSaltFlag(flags.salt);
1290
+ const noMint = flags['no-mint'] !== undefined;
1291
+ if (noMint && flags['mint-amount'] !== undefined) {
1292
+ throw new Error('--no-mint and --mint-amount disagree — drop one. (--no-mint defers ALL minting at deploy; --mint-amount sets how many copies mint at deploy.)');
1293
+ }
1294
+ const mintAmount = noMint ? 0n : flags['mint-amount'] !== undefined ? parseNonNegativeIntFlag(flags['mint-amount'], 'mint-amount') : 1n;
1295
+ const minter = flags.minter ?? zeroAddress;
1296
+ const primaryPayee = flags['primary-payee'] ?? zeroAddress;
1297
+ const paused = flags.unpaused === undefined;
1298
+ const buildForDeployer = async (deployer) => {
1299
+ const salt = explicitSalt ?? saltFor(deployer);
1300
+ const clone = await predictClone(publicClient, { factory, salt });
1301
+ const { tokenFields, contentNote } = bakedImage
1302
+ ? { tokenFields: [bakedImage], contentNote: 'content: image staged ON-CHAIN via reader (self-resolving, no custody)' }
1303
+ : onchainImage
1304
+ ? { tokenFields: [], contentNote: 'content: image would be staged ON-CHAIN via reader (chunk store) — dry run skips the staging write' }
1305
+ : await prepareContent(flags.image, clone, storageOverrides(flags), !dryRun, onChainUri);
1306
+ if (flags.description && (onChainUri || flags['description-onchain'])) {
1307
+ tokenFields.push({ field: encodeTag(F.description), representation: encodeTag(R.inline), value: toHex(flags.description) });
1308
+ }
1309
+ if (traitsOnchain)
1310
+ tokenFields.push(attributesInlineField(traits));
1311
+ const params = {
1312
+ owner: deployer,
1313
+ mintTo: mintAmount > 0n ? deployer : zeroAddress,
1314
+ mintAmount,
1315
+ name,
1316
+ symbol,
1317
+ tokenURIBase: onChainUri && !hasPublicUrl ? '' : `${baseUrl}/t`,
1318
+ tokenURIRenderer: renderer,
1319
+ contractURIBase: onChainUri && !hasPublicUrl ? '' : `${baseUrl}/c`,
1320
+ contractURIRenderer: renderer,
1321
+ royaltyReceiver: deployer,
1322
+ royaltyBps,
1323
+ transferValidator,
1324
+ editionSize,
1325
+ primaryPayee,
1326
+ minter,
1327
+ paused,
1328
+ tokenFields,
1329
+ contractFields: authorshipContractFields(flags),
1330
+ };
1331
+ return { clone, params, salt, contentNote };
1332
+ };
1333
+ let clone;
1334
+ let blockNumber;
1335
+ if (dryRun) {
1336
+ let deployer;
1337
+ if (flags.for)
1338
+ deployer = flags.for;
1339
+ else {
1340
+ try {
1341
+ deployer = makeWalletClient({ chainKey: CHAIN }).account.address;
1342
+ }
1343
+ catch {
1344
+ throw new Error('dry run needs a deployer address to compute the deterministic deploy address — pass --for 0x.. ' +
1345
+ '(a preview signs nothing, so no key is needed). For the REAL deploy with no key in .env, use the ' +
1346
+ 'wallet lane: --sign --for 0x.. (you approve in your own wallet).');
1347
+ }
1348
+ }
1349
+ const { clone: predicted, params, salt, contentNote } = await buildForDeployer(deployer);
1350
+ info(`deployer ${deployer}`);
1351
+ await warnUnfunded(publicClient, deployer);
1352
+ if (explicitSalt)
1353
+ info(`deterministic address: ${predicted}`);
1354
+ info(`name "${name}"${flags.name ? '' : dim(' (default — pass --name)')}`);
1355
+ info(`symbol ${symbol}${flags.symbol ? '' : dim(' (default — pass --symbol)')}`);
1356
+ info(`copies ${editionSize === 0n ? 'open (uncapped)' : editionSize.toString()}`);
1357
+ info(describeDeployField('description', flags.description, onChainUri || !!flags['description-onchain']));
1358
+ authorshipReadout(flags).forEach((line) => info(line));
1359
+ info(describeTraits(traits, traitsOnchain));
1360
+ info(`royalty ${royaltyBps / 100}% → ${deployer}${flags['royalty-bps'] ? '' : dim(' (default 5%)')}`);
1361
+ info(contentNote);
1362
+ info(`tokenURI base ${params.tokenURIBase || dim('(empty — resolves on-chain via the renderer)')}`);
1363
+ info(`contractURI base ${params.contractURIBase || dim('(empty — resolves on-chain via the renderer)')}`);
1364
+ info(mintAmount > 0n ? `mint: ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} of #0 → ${deployer} at deploy` : 'mint: deferred (--no-mint, or --mint-amount 0)');
1365
+ if (minter !== zeroAddress)
1366
+ info(`minter ${minter}`);
1367
+ if (primaryPayee !== zeroAddress)
1368
+ info(`primary payee ${primaryPayee}`);
1369
+ info(`paused ${paused}${flags.unpaused ? '' : dim(' (default — pass --unpaused to open at deploy)')}`);
1370
+ if (!explicitSalt) {
1371
+ console.log(`\n ${bold('salt')} ${g(salt)}`);
1372
+ info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address), or ${bold(`abx predict --salt ${salt} --for ${deployer} --copies ${flags.copies}`)}.`);
1373
+ }
1374
+ info(dim('the values above are exactly what a real deploy writes — nothing else is added.'));
1375
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
1376
+ 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 }));
1377
+ return;
1378
+ }
1379
+ 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'}; owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
1380
+ (transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
1381
+ if (lane === 'send') {
1382
+ const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
1383
+ const { clone: predicted, params, salt, contentNote } = await buildForDeployer(account.address);
1384
+ info(`deterministic address: ${predicted}`);
1385
+ info(describeDeployField('description', flags.description, onChainUri || !!flags['description-onchain']));
1386
+ authorshipReadout(flags).forEach((line) => info(line));
1387
+ info(describeTraits(traits, traitsOnchain));
1388
+ info(contentNote);
1389
+ info(mintAmount > 0n ? `mint: ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} of #0 → ${account.address} at deploy` : 'mint: deferred — mint later with `abx mint`');
1390
+ const send = makeHotSender({ wallet, account, publicClient });
1391
+ const r = await deployOneOfOneEdition(send, publicClient, { factory, params, salt });
1392
+ clone = predicted;
1393
+ blockNumber = r.blockNumber;
1394
+ ok(`deployed ${clone}`);
1395
+ info(`tx ${explorerBase()}/tx/${r.txHash} (block ${blockNumber})`);
1396
+ }
1397
+ else {
1398
+ // wallet lane (no on-chain-image staging — refused above) or the cold lane: a single deploy tx.
1399
+ info(onChainUri ? 'a wallet will become the owner; the token resolves from chain — no URI base is baked in.' : `a wallet will become the owner; URIs point at ${baseUrl}`);
1400
+ const result = await signTx(async (signer) => {
1401
+ const { clone: predicted, params, salt } = await buildForDeployer(signer);
1402
+ return prepareDeployOneOfOneEdition({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
1403
+ }, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
1404
+ if (!result) {
1405
+ console.log(`\n${dim(` unsigned — broadcast it, then: abx add <clone> --factory ${factory} --from-block <deployBlock>`)}\n`);
1406
+ return;
1407
+ }
1408
+ clone = result.prepared.fields.clone;
1409
+ blockNumber = result.blockNumber;
1410
+ ok(`deployed ${clone}`);
1411
+ }
1412
+ emit(jsonSafe({ command: 'deploy', kind: '1of1-edition', copies: editionSize.toString(), address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol }));
1413
+ step('Index it — replay the event spine from chain');
1414
+ const indexer = localIndexer();
1415
+ const offChainTraits = !traitsOnchain && traits.length ? JSON.stringify(traits) : undefined;
1416
+ const baseReg = {
1417
+ address: clone,
1418
+ chainKey: CHAIN,
1419
+ fromBlock: blockNumber.toString(),
1420
+ factory,
1421
+ label: name,
1422
+ description: flags.description,
1423
+ externalUrl: flags['external-url'],
1424
+ attributes: offChainTraits,
1425
+ };
1426
+ indexer.register(baseReg);
1427
+ const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
1428
+ if (state.eventCount > 0)
1429
+ ok(`reconstructed ${state.eventCount} events in ${elapsedMs}ms — no provider involved`);
1430
+ info(`name "${state.name}" · owner ${state.owner} · canonical: ${canonicalLabel(state.isCanonical)}`);
1431
+ const contentLocators = !onChainUri && !onchainImage ? await collectContentLocators(state, resolveBackend(storageOptions(storageOverrides(flags)))) : {};
1432
+ if (Object.keys(contentLocators).length) {
1433
+ indexer.register({ ...baseReg, contentLocators: JSON.stringify(contentLocators) });
1434
+ info(`content locator: image → ${Object.values(contentLocators)[0]} ${dim('(durable; points off this node)')}`);
1435
+ }
1436
+ if (onChainUri) {
1437
+ if (imageEndsUpOnChain(flags, onchainImage)) {
1438
+ console.log(`\n${g('Done — fully on-chain.')} ${dim('The edition self-resolves; no server or hosting needed.')}`);
1439
+ }
1440
+ else {
1441
+ console.log(`\n${g('Done — metadata on-chain.')} ${dim('uri(0) resolves from the chain with no server.')}`);
1442
+ }
1443
+ console.log(` ${bold(`abx tokenuri ${clone}`)} ${dim('# read uri(0) straight from the contract + decode the JSON')}`);
1444
+ }
1445
+ else {
1446
+ console.log(`\n${g('Done.')} ${bold(`abx verify ${clone}`)} ${dim('to confirm integrity · ')}${bold('abx serve')}${dim(' to host LOCALLY')}`);
1447
+ }
1448
+ if (mintAmount === 0n)
1449
+ console.log(` ${bold(`abx mint ${clone} --amount <n>`)} ${dim('# issue the first copies of #0')}`);
1450
+ console.log(` ${bold(`abx minter configure ${clone} --token-id 0 --price <eth> --allocation <n>`)} ${dim('# set up the priced sale on the shared edition minter')}`);
1451
+ console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces to index it')}\n`);
1452
+ }
1095
1453
  // ── deploy-series ─────────────────────────────────────────────────────────────
1096
1454
  // Point at a directory of media and deploy a multi-token Series. Each file becomes a
1097
1455
  // token (`0..N-1`, natural-sorted) — a token's metadata is its token id, and tokens mint
@@ -1113,6 +1471,11 @@ export const tokenFieldOf = (tokenId, f) => ({
1113
1471
  value: f.value,
1114
1472
  });
1115
1473
  export async function cmdDeploySeries(flags) {
1474
+ // `--copies <N|open>` routes to EditionImage — N ids from the folder × copies each. See the
1475
+ // module note on `cmdDeployOneOfOneEditionBody` for why this is a separate function rather than
1476
+ // `if (copies)` branches threaded through `cmdDeploySeriesBody` (byte-identical 721 lane).
1477
+ if (flags.copies !== undefined)
1478
+ return withJson(flags, async (emit) => cmdDeployEditionImageBody(flags, emit));
1116
1479
  return withJson(flags, async (emit) => cmdDeploySeriesBody(flags, emit));
1117
1480
  }
1118
1481
  export async function cmdDeploySeriesBody(flags, emit) {
@@ -1630,40 +1993,324 @@ export async function cmdDeploySeriesBody(flags, emit) {
1630
1993
  }
1631
1994
  console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces once metadata is live')}\n`);
1632
1995
  }
1633
- // ── deploy-code ──────────────────────────────────────────────────────────────
1634
- /**
1635
- * `abx deploy-code` deploy a code project ({SeriesCode}) end to end, hot lane:
1636
- * the canonical factory + seed source from the manifest, fat initialize (the `code`
1637
- * field rides it in directory mode), then ONE post-deploy multicall for the script
1638
- * chunks (template mode) + any PostParam schemas, optional reserve mints, and local
1639
- * register + index. Grown directly from the Sepolia drop rehearsal
1640
- * (`packages/cli/scripts/code-drop-rehearsal.ts`).
1641
- *
1642
- * Modes (exactly one):
1643
- * --script <file> template mode — the program stored on-chain in chunks
1644
- * --code-dir <dir> directory mode — the build uploaded via the storage backend
1645
- * (`putDirectory`; ipfs/arweave), its root as the `code` field
1646
- *
1647
- * Schemas: --schema key:Type:Auth[,key:Type:Auth…] (e.g. palette:HexColor:TokenOwner).
1648
- * Seeds: canonical randomizer by default; --no-seed opts out.
1649
- */
1650
- // ── the demo walkthrough: teaching sections, demo-only ────────────────────────
1996
+ // ── deploy-series --copies: EditionImage ──────────────────────────────────────────────────────────
1997
+ // N distinct artworks (one per file, like a Series) × copies each. The edition twin of
1998
+ // `cmdDeploySeriesBody`, kept as its OWN function for the same reason `cmdDeployOneOfOneEditionBody`
1999
+ // is (byte-identical 721 lane see that function's module note).
1651
2000
  //
1652
- // `abx demo` is a TEACHING command, not a shortcut the docs point a first-time reader here to
1653
- // learn what the toolkit does on their behalf. Its old form asserted the interesting claims
1654
- // ("reconstructed 9 events no provider involved") without ever showing them, which made it a
1655
- // smoke test wearing a demo's clothes. These sections demonstrate instead: print the spine the
1656
- // chain now holds, throw the local projection away and rebuild it, then read the token back the way
1657
- // a marketplace would. They run only for `demo` (never `deploy`), and never pause an agent or CI
1658
- // run has to behave identically.
1659
- /**
1660
- * The deterministic part of a projection: everything that is a pure function of the chain.
1661
- * Deliberately EXCLUDES `reconstructedAt`, `rpcUrl` and `toBlock` — a timestamp, the endpoint that
1662
- * happened to answer, and the head at scan time all legitimately differ between two replays, so
1663
- * folding them in would make the rebuild proof fail for reasons that aren't about correctness.
1664
- */
1665
- export function projectionFingerprint(s) {
1666
- const canonical = {
2001
+ // v1 scope (reported, not hidden): custody is off-chain (keccak + optional backend upload, ONE
2002
+ // `backend.put` per file) or `--onchain-uri` (inline SVG). `cmdDeploySeriesBody`'s O(1) uniform-
2003
+ // extension directory upload and `--onchain-image` chunk staging are NOT ported here both are
2004
+ // real engineering (a shared chunk store / a shared directory manifest across N ids), and a per-file
2005
+ // fallback already works correctly, just at O(N) upload calls instead of O(1). Tracked as a follow-up.
2006
+ // (`DEPLOY_SERIES_EDITION_FLAGS` is declared further down, beside `DEPLOY_EDITION_FLAGS` — see that
2007
+ // const's own note on why.)
2008
+ export async function cmdDeployEditionImageBody(flags, emit) {
2009
+ refuseStrayFlags(flags, DEPLOY_SERIES_EDITION_FLAGS, 'deploy-series');
2010
+ warnSignWithoutFor(flags);
2011
+ const editionSize = parseCopies(flags.copies);
2012
+ const name = flags.name ?? 'ABX Edition Series';
2013
+ const symbol = flags.symbol ?? 'ABXES';
2014
+ const royaltyBps = Number(flags['royalty-bps'] ?? 500);
2015
+ const port = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
2016
+ const baseUrl = flags['public-base-url'] ?? resolveBaseUrl(port);
2017
+ console.log(bold(`\n ABX Self-Host Toolkit — deploy edition series\n ${dim(editionSize === 0n
2018
+ ? 'EditionImage (ERC-1155) — N ids from the folder, each an OPEN edition'
2019
+ : `EditionImage (ERC-1155) — N ids from the folder × ${editionSize} cop${editionSize === 1n ? 'y' : 'ies'} each`)}`));
2020
+ if (editionSize === 1n)
2021
+ info(copiesOneNote('deploy-series'));
2022
+ if (flags['onchain-image']) {
2023
+ throw new Error('--onchain-image is not yet supported for edition deploys (--copies) — use --onchain-uri (inline SVG) or off-chain custody (--backend ipfs|arweave|cloud) instead. Tracked as a follow-up.');
2024
+ }
2025
+ if (!flags.dir) {
2026
+ throw new Error('abx deploy-series --copies <n|open> --dir <media-dir> [--count N] [--mint-all | --mint-count N | --no-mint] [--mint-amount N] ' +
2027
+ '[--onchain-uri (inline SVG) | --backend ipfs|arweave|cloud (off-chain, hosted resolver) | --public-base-url https://…] ' +
2028
+ '[--minter 0x..] [--primary-payee 0x..] [--unpaused] [--721c recommended|0x..] --name "Title" --symbol SYM');
2029
+ }
2030
+ const dirPath = resolvePath(flags.dir);
2031
+ const files = readdirSync(dirPath)
2032
+ .filter((f) => !f.startsWith('.') && statSync(joinPath(dirPath, f)).isFile())
2033
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
2034
+ if (files.length === 0)
2035
+ throw new Error(`no media files in ${dirPath}`);
2036
+ const count = flags.count ? Number(flags.count) : files.length;
2037
+ if (!Number.isInteger(count) || count <= 0)
2038
+ throw new Error('--count must be a positive integer');
2039
+ if (count > files.length)
2040
+ throw new Error(`--count ${count} exceeds the ${files.length} media file(s) in ${dirPath}`);
2041
+ const slots = files.slice(0, count);
2042
+ const dryRun = isDryRun(flags);
2043
+ if (dryRun)
2044
+ assertPreviewDeployer(flags);
2045
+ assertRealIdentity(flags, { name, symbol, dryRun });
2046
+ const lane = laneFromFlags(flags);
2047
+ const publicClient = makePublicClient({ chainKey: CHAIN });
2048
+ await assertChainId(CHAIN, { allowUnreachable: dryRun });
2049
+ const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun, '1155C');
2050
+ // Mint timing: `mintCount` ids premint (SAME meaning as the 721 lane's --mint-all/--mint-count —
2051
+ // how many DISTINCT ids), `mintAmount` is the NEW knob: copies of EACH premint id.
2052
+ const mintCount = flags['mint-all'] !== undefined ? count : flags['mint-count'] ? Number(flags['mint-count']) : 0;
2053
+ if (mintCount > count)
2054
+ throw new Error(`--mint-count ${mintCount} exceeds the series size ${count}`);
2055
+ if (flags['no-mint'] !== undefined && (flags['mint-all'] !== undefined || flags['mint-count'] !== undefined)) {
2056
+ throw new Error('--no-mint contradicts --mint-all/--mint-count — drop one.');
2057
+ }
2058
+ const effectiveMintCount = flags['no-mint'] !== undefined ? 0 : mintCount;
2059
+ const mintAmount = effectiveMintCount > 0 ? (flags['mint-amount'] !== undefined ? parseNonNegativeIntFlag(flags['mint-amount'], 'mint-amount') : 1n) : 0n;
2060
+ if (effectiveMintCount > 0 && mintAmount === 0n) {
2061
+ throw new Error('--mint-amount 0 with ids being pre-minted at deploy makes no sense — pass a positive --mint-amount, or drop --mint-all/--mint-count to defer minting entirely.');
2062
+ }
2063
+ const onChainUri = !!flags['onchain-uri'];
2064
+ const minter = flags.minter ?? zeroAddress;
2065
+ const paused = flags.unpaused === undefined;
2066
+ const primaryPayee = flags['primary-payee'] ?? zeroAddress;
2067
+ const explicitSalt = parseSaltFlag(flags.salt);
2068
+ const hasPublicUrl = !!(flags['public-base-url'] || process.env.ABX_PUBLIC_BASE_URL);
2069
+ step('Trust anchor');
2070
+ let factory;
2071
+ if (dryRun) {
2072
+ const existing = editionFactoryAddress(flags.factory);
2073
+ if (!existing) {
2074
+ info('no canonical edition factory for this chain yet — a real deploy would deploy the trust anchor first.');
2075
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
2076
+ return;
2077
+ }
2078
+ factory = existing;
2079
+ if (!(await previewFactoryLive(publicClient, factory, 'edition factory')))
2080
+ return;
2081
+ info(`would reuse canonical edition factory ${factory}`);
2082
+ }
2083
+ else {
2084
+ factory = await ensureEditionFactory(flags.factory, !!flags['bootstrap-factory']);
2085
+ }
2086
+ let renderer = zeroAddress;
2087
+ if (onChainUri) {
2088
+ step('On-chain renderer');
2089
+ renderer = dryRun ? (rendererAddress(flags.renderer) ?? zeroAddress) : await ensureRenderer(flags.renderer);
2090
+ }
2091
+ if (!onChainUri) {
2092
+ const loopback = loopbackBaseUrl(baseUrl);
2093
+ if (!hasPublicUrl || loopback) {
2094
+ const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
2095
+ const msg = `Off-chain metadata needs a PUBLIC resolver URL baked on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one. ` +
2096
+ `Pick a real path:\n • Fully on-chain (SVG art): --onchain-uri\n • Hosted resolver: --public-base-url https://your.domain`;
2097
+ if (!devAllow) {
2098
+ if (dryRun)
2099
+ warn(`would REFUSE to deploy — ${msg}`);
2100
+ else
2101
+ throw new Error(msg);
2102
+ }
2103
+ else {
2104
+ warn(`DEV ONLY (ABX_DEV_ALLOW_LOCALHOST_URI): baking ${bold(baseUrl)} on-chain — resolves only on THIS machine.`);
2105
+ }
2106
+ }
2107
+ }
2108
+ step(`Prepare ${count} id(s) from ${basename(dirPath)}/`);
2109
+ const overrides = storageOverrides(flags);
2110
+ const opts = storageOptions(overrides);
2111
+ const backendId = overrides.backend ?? process.env.ABX_STORAGE_BACKEND ?? 'fs';
2112
+ const backend = !onChainUri && !dryRun ? resolveBackend(opts) : undefined;
2113
+ const tokenPaths = slots.map((s, i) => ({ tokenId: i, name: s, path: joinPath(dirPath, s) }));
2114
+ const seriesTraits = parseSeriesTraits(flags.attributes ? readFileSync(resolvePath(process.cwd(), flags.attributes), 'utf8') : undefined, slots);
2115
+ const seriesTraitsOnchain = seriesTraits.size > 0 && (!!flags['traits-onchain'] || onChainUri);
2116
+ const offChainTokenTraits = seriesTraits.size && !seriesTraitsOnchain
2117
+ ? JSON.stringify(Object.fromEntries([...seriesTraits].map(([id, a]) => [String(id), a])))
2118
+ : undefined;
2119
+ const tokenFields = [];
2120
+ const buildFields = async () => {
2121
+ for (const { tokenId, name: fname, path } of tokenPaths) {
2122
+ const bytes = new Uint8Array(readFileSync(path));
2123
+ const contentType = contentTypeFromPath(path);
2124
+ if (onChainUri) {
2125
+ const text = Buffer.from(bytes).toString('utf8');
2126
+ if (!looksLikeSvg(text)) {
2127
+ throw new Error(`--onchain-uri inlines SVG only; id ${tokenId} "${fname}" is ${contentType}. Drop --onchain-uri for off-chain custody instead.`);
2128
+ }
2129
+ tokenFields.push(tokenFieldOf(tokenId, imageInlineField(text)));
2130
+ }
2131
+ else {
2132
+ const hash = hashContent(bytes);
2133
+ if (backend)
2134
+ await backend.put(hash, { bytes, contentType });
2135
+ tokenFields.push(tokenFieldOf(tokenId, imageKeccakField(hash)));
2136
+ }
2137
+ info(`id ${tokenId} ← ${fname} ${dim(`(${bytes.length}B ${contentType})`)}`);
2138
+ }
2139
+ if (seriesTraitsOnchain)
2140
+ for (const [tokenId, attrs] of seriesTraits)
2141
+ tokenFields.push(tokenFieldOf(tokenId, attributesInlineField(attrs)));
2142
+ };
2143
+ if (!dryRun) {
2144
+ const custody = onChainUri ? 'inline SVG on-chain' : `off-chain custody (${backendId})`;
2145
+ await confirmSend(`About to deploy an edition series "${name}" (${symbol}) — ${count} id(s) × ${editionSize === 0n ? 'open' : editionSize.toString()} cop${editionSize === 1n ? 'y' : 'ies'} each; ${custody}; ` +
2146
+ `mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} at deploy` : 'deferred'}; owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
2147
+ (transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
2148
+ }
2149
+ await buildFields();
2150
+ const buildForDeployer = async (deployer) => {
2151
+ const salt = explicitSalt ?? saltFor(deployer);
2152
+ const clone = await predictClone(publicClient, { factory, salt });
2153
+ const params = {
2154
+ owner: deployer,
2155
+ name,
2156
+ symbol,
2157
+ tokenURIBase: onChainUri && !hasPublicUrl ? '' : `${baseUrl}/t`,
2158
+ tokenURIRenderer: renderer,
2159
+ contractURIBase: onChainUri && !hasPublicUrl ? '' : `${baseUrl}/c`,
2160
+ contractURIRenderer: renderer,
2161
+ royaltyReceiver: deployer,
2162
+ royaltyBps,
2163
+ transferValidator,
2164
+ maxInvocations: count,
2165
+ editionSize,
2166
+ primaryPayee,
2167
+ minter,
2168
+ paused,
2169
+ mintTo: effectiveMintCount > 0 ? deployer : zeroAddress,
2170
+ mintCount: effectiveMintCount,
2171
+ mintAmount,
2172
+ tokenFields,
2173
+ contractFields: authorshipContractFields(flags),
2174
+ };
2175
+ return { clone, params, salt };
2176
+ };
2177
+ let clone;
2178
+ let blockNumber;
2179
+ if (dryRun) {
2180
+ let deployer;
2181
+ if (flags.for)
2182
+ deployer = flags.for;
2183
+ else {
2184
+ try {
2185
+ deployer = makeWalletClient({ chainKey: CHAIN }).account.address;
2186
+ }
2187
+ catch {
2188
+ throw new Error('dry run needs a deployer address to compute the deterministic deploy address — pass --for 0x.. (a preview signs nothing).');
2189
+ }
2190
+ }
2191
+ const { clone: predicted, params, salt } = await buildForDeployer(deployer);
2192
+ info(`deployer ${deployer}`);
2193
+ await warnUnfunded(publicClient, deployer);
2194
+ if (explicitSalt)
2195
+ info(`deterministic address: ${predicted}`);
2196
+ info(`name "${name}"${flags.name ? '' : dim(' (default — pass --name)')}`);
2197
+ info(`symbol ${symbol}${flags.symbol ? '' : dim(' (default — pass --symbol)')}`);
2198
+ info(`ids ${count} (id-space cap)`);
2199
+ info(`copies ${editionSize === 0n ? 'open (uncapped) per id' : `${editionSize} per id`}`);
2200
+ info(`royalty ${royaltyBps / 100}% → ${deployer}${flags['royalty-bps'] ? '' : dim(' (default 5%)')}`);
2201
+ info(`tokenURI base ${params.tokenURIBase || dim('(empty — resolves on-chain via the renderer)')}`);
2202
+ info(`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${deployer} at deploy` : 'deferred'}`);
2203
+ if (minter !== zeroAddress)
2204
+ info(`minter ${minter}`);
2205
+ if (primaryPayee !== zeroAddress)
2206
+ info(`primary payee ${primaryPayee}`);
2207
+ if (!explicitSalt) {
2208
+ console.log(`\n ${bold('salt')} ${g(salt)}`);
2209
+ info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
2210
+ }
2211
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
2212
+ emit(jsonSafe({ command: 'deploy-series', kind: 'edition', copies: editionSize.toString(), ids: count, dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol }));
2213
+ return;
2214
+ }
2215
+ if (lane === 'send') {
2216
+ const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
2217
+ const { clone: predicted, params, salt } = await buildForDeployer(account.address);
2218
+ info(`deterministic address: ${predicted}`);
2219
+ info(`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${account.address} at deploy` : 'deferred'}`);
2220
+ const send = makeHotSender({ wallet, account, publicClient });
2221
+ const r = await deployEditionImage(send, publicClient, { factory, params, salt });
2222
+ clone = predicted;
2223
+ blockNumber = r.blockNumber;
2224
+ ok(`deployed ${clone}`);
2225
+ info(`tx ${explorerBase()}/tx/${r.txHash} (block ${blockNumber})`);
2226
+ }
2227
+ else {
2228
+ info(onChainUri ? 'a wallet will become the owner; the tokens resolve from chain — no URI base is baked in.' : `a wallet will become the owner; URIs point at ${baseUrl}`);
2229
+ const result = await signTx(async (signer) => {
2230
+ const { clone: predicted, params, salt } = await buildForDeployer(signer);
2231
+ return prepareDeployEditionImage({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
2232
+ }, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
2233
+ if (!result) {
2234
+ console.log(`\n${dim(` unsigned — broadcast it, then: abx add <clone> --factory ${factory} --from-block <deployBlock>`)}\n`);
2235
+ return;
2236
+ }
2237
+ clone = result.prepared.fields.clone;
2238
+ blockNumber = result.blockNumber;
2239
+ ok(`deployed ${clone}`);
2240
+ }
2241
+ emit(jsonSafe({ command: 'deploy-series', kind: 'edition', copies: editionSize.toString(), ids: count, address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol }));
2242
+ step('Index it — replay the event spine from chain');
2243
+ const indexer = localIndexer();
2244
+ const offChainTraits = !seriesTraitsOnchain && offChainTokenTraits ? offChainTokenTraits : undefined;
2245
+ const baseReg = {
2246
+ address: clone,
2247
+ chainKey: CHAIN,
2248
+ fromBlock: blockNumber.toString(),
2249
+ factory,
2250
+ label: name,
2251
+ description: flags.description,
2252
+ externalUrl: flags['external-url'],
2253
+ tokenAttributes: offChainTraits,
2254
+ };
2255
+ indexer.register(baseReg);
2256
+ const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
2257
+ if (state.eventCount > 0)
2258
+ ok(`reconstructed ${state.eventCount} events in ${elapsedMs}ms — no provider involved`);
2259
+ info(`name "${state.name}" · owner ${state.owner} · canonical: ${canonicalLabel(state.isCanonical)}`);
2260
+ const contentLocators = !onChainUri ? await collectContentLocators(state, resolveBackend(storageOptions(storageOverrides(flags)))) : {};
2261
+ if (Object.keys(contentLocators).length) {
2262
+ indexer.register({ ...baseReg, contentLocators: JSON.stringify(contentLocators) });
2263
+ }
2264
+ const isRemoteBase = !/^https?:\/\/(localhost|127\.0\.0\.1)\b/i.test(baseUrl);
2265
+ if (onChainUri) {
2266
+ console.log(`\n${g('Done — fully on-chain.')} ${dim('Every id self-resolves; no server needed.')}`);
2267
+ console.log(` ${bold(`abx tokenuri ${clone} --token 0`)} ${dim('# read uri(0) straight from the contract')}`);
2268
+ }
2269
+ else {
2270
+ console.log(`\n${g('Edition series deployed.')} ${effectiveMintCount > 0 ? '' : dim('(deferred mint — warm the resolver, then mint)')}`);
2271
+ if (isRemoteBase)
2272
+ console.log(` ${bold(`abx add ${clone} --remote`)} ${dim('# register with the hosted resolver')}`);
2273
+ else
2274
+ console.log(` ${bold('abx serve')} ${dim('# stand up the resolver locally')}`);
2275
+ }
2276
+ console.log(` ${bold(`abx mint ${clone} --token-id <id> --amount <n>`)} ${dim('# mint more copies of an id')}`);
2277
+ console.log(` ${bold(`abx minter configure ${clone} --token-id <id> --price <eth> --allocation <n>`)} ${dim('# set up a priced sale for one id')}`);
2278
+ console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces once metadata is live')}\n`);
2279
+ }
2280
+ // ── deploy-code ──────────────────────────────────────────────────────────────
2281
+ /**
2282
+ * `abx deploy-code` — deploy a code project ({SeriesCode}) end to end, hot lane:
2283
+ * the canonical factory + seed source from the manifest, fat initialize (the `code`
2284
+ * field rides it in directory mode), then ONE post-deploy multicall for the script
2285
+ * chunks (template mode) + any PostParam schemas, optional reserve mints, and local
2286
+ * register + index. Grown directly from the Sepolia drop rehearsal
2287
+ * (`packages/cli/scripts/code-drop-rehearsal.ts`).
2288
+ *
2289
+ * Modes (exactly one):
2290
+ * --script <file> template mode — the program stored on-chain in chunks
2291
+ * --code-dir <dir> directory mode — the build uploaded via the storage backend
2292
+ * (`putDirectory`; ipfs/arweave), its root as the `code` field
2293
+ *
2294
+ * Schemas: --schema key:Type:Auth[,key:Type:Auth…] (e.g. palette:HexColor:TokenOwner).
2295
+ * Seeds: canonical randomizer by default; --no-seed opts out.
2296
+ */
2297
+ // ── the demo walkthrough: teaching sections, demo-only ────────────────────────
2298
+ //
2299
+ // `abx demo` is a TEACHING command, not a shortcut — the docs point a first-time reader here to
2300
+ // learn what the toolkit does on their behalf. Its old form asserted the interesting claims
2301
+ // ("reconstructed 9 events — no provider involved") without ever showing them, which made it a
2302
+ // smoke test wearing a demo's clothes. These sections demonstrate instead: print the spine the
2303
+ // chain now holds, throw the local projection away and rebuild it, then read the token back the way
2304
+ // a marketplace would. They run only for `demo` (never `deploy`), and never pause — an agent or CI
2305
+ // run has to behave identically.
2306
+ /**
2307
+ * The deterministic part of a projection: everything that is a pure function of the chain.
2308
+ * Deliberately EXCLUDES `reconstructedAt`, `rpcUrl` and `toBlock` — a timestamp, the endpoint that
2309
+ * happened to answer, and the head at scan time all legitimately differ between two replays, so
2310
+ * folding them in would make the rebuild proof fail for reasons that aren't about correctness.
2311
+ */
2312
+ export function projectionFingerprint(s) {
2313
+ const canonical = {
1667
2314
  address: s.address.toLowerCase(),
1668
2315
  name: s.name,
1669
2316
  symbol: s.symbol,
@@ -1839,7 +2486,30 @@ export const DEPLOY_SERIES_FLAGS = new Set([
1839
2486
  ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag),
1840
2487
  ...SHARED_DEPLOY_FLAGS,
1841
2488
  ]);
2489
+ // The two edition allowlists that SPREAD an existing 721 allowlist (`DEPLOY_FLAGS`/
2490
+ // `DEPLOY_SERIES_FLAGS`) — declared here, AFTER both, so the spread isn't a module-scope
2491
+ // use-before-declaration (a plain function-body reference to a later `const` is fine; a spread
2492
+ // inside another top-level `const`'s OWN initializer is evaluated immediately, in declaration
2493
+ // order, so it isn't). `cmdDeployOneOfOneEditionBody`/`cmdDeployEditionImageBody` reference these
2494
+ // by name from higher up in the file — safe, since neither is CALLED until well after the whole
2495
+ // module has finished evaluating.
2496
+ export const DEPLOY_EDITION_FLAGS = new Set([...DEPLOY_FLAGS, 'copies', 'mint-amount', 'minter', 'primary-payee', 'unpaused']);
2497
+ export const DEPLOY_SERIES_EDITION_FLAGS = new Set([...DEPLOY_SERIES_FLAGS, 'copies', 'mint-amount']);
1842
2498
  export async function cmdDeployCode(flags) {
2499
+ // `--resume` targets an EXISTING contract — its standard (721 or edition) was fixed at THAT
2500
+ // contract's own deploy, so `--copies` (a creation-time choice) is nonsensical alongside it.
2501
+ // Refused HERE, before either body runs, so the message is specific rather than either body's
2502
+ // own (unrelated) stray-flag refusal: `cmdDeployEditionCodeBody` doesn't recognize --resume at
2503
+ // all yet, and `cmdDeployCodeBody` would otherwise reject bare --copies as an unrecognized flag.
2504
+ if (flags.resume !== undefined && flags.copies !== undefined) {
2505
+ throw new Error('--resume cannot be combined with --copies: the target already exists (and its standard was fixed at ITS OWN ' +
2506
+ 'deploy); --copies only chooses a standard at CREATION time. Drop --copies — `abx deploy-code --resume <address> …` ' +
2507
+ 'reads what the target already is.');
2508
+ }
2509
+ // `--copies <N|open>` routes to EditionCode. See the module note on `cmdDeployOneOfOneEditionBody`
2510
+ // for why this is a separate function rather than `if (copies)` branches (byte-identical 721 lane).
2511
+ if (flags.copies !== undefined)
2512
+ return withJson(flags, async (emit) => cmdDeployEditionCodeBody(flags, emit));
1843
2513
  return withJson(flags, async (emit) => cmdDeployCodeBody(flags, emit));
1844
2514
  }
1845
2515
  export async function cmdDeployCodeBody(flags, emit) {
@@ -2682,6 +3352,8 @@ export async function cmdDeployCodeBody(flags, emit) {
2682
3352
  // Deploy-time-only flags are refused rather than ignored: --salt/--721c/--bootstrap-factory all
2683
3353
  // describe how a contract is CREATED, and this creates nothing. 721C especially — enrollment is
2684
3354
  // permanent and deploy-time-only, so accepting the flag here would imply it can be added later.
3355
+ // (--copies + --resume is refused earlier, at cmdDeployCode's dispatch — before this function
3356
+ // even runs — so `flags.copies` is guaranteed undefined here; see that guard's own comment.)
2685
3357
  for (const [flag, why] of [
2686
3358
  ['salt', 'the address already exists, so no salt is used'],
2687
3359
  ['721c', 'ERC-721C enrollment is deploy-time-only and PERMANENT — it cannot be added to an existing collection'],
@@ -2691,6 +3363,18 @@ export async function cmdDeployCodeBody(flags, emit) {
2691
3363
  if (flags[flag] !== undefined)
2692
3364
  throw new Error(`--resume cannot be combined with --${flag}: ${why}.`);
2693
3365
  }
3366
+ // Edition resume is NOT supported: the SDK's `planResume`/`ResumeReader` (resume.ts) assume the
3367
+ // 721 shape throughout — a single sequential `mint(address)` shortfall against a whole-contract
3368
+ // `totalSupply()`, neither of which exists on EditionCode (`mint(address,uint256,uint256)` per id,
3369
+ // no whole-contract total — see tokens.ts's `TokenListing.totalSupply` doc). Refuse cleanly rather
3370
+ // than diff against the wrong shape and silently under- or over-report what's missing.
3371
+ if (await isEditionContract(publicClient, target)) {
3372
+ throw new Error(`--resume ${target}: this is an EditionCode contract, and resume isn't wired for editions yet — the diff logic ` +
3373
+ `(packages/sdk/src/resume.ts) assumes the 721 shape (one sequential mint, one whole-contract totalSupply), ` +
3374
+ `neither of which an edition has. If the setup multicall is what failed, the safest path right now is a hand-` +
3375
+ `assembled resend (the SAME idea resume.ts automates for 721 — see its class-level doc comment) or filing this ` +
3376
+ `as a gap: edition resume needs its own ResumeReader/SetupLegs shape (per-id totalSupply, a (to,id,amount) mint leg).`);
3377
+ }
2694
3378
  const owner = await publicClient
2695
3379
  .readContract({ address: target, abi: seriesCodeAbi, functionName: 'owner' })
2696
3380
  .catch(() => undefined);
@@ -2929,16 +3613,362 @@ export async function cmdDeployCodeBody(flags, emit) {
2929
3613
  console.log(`\n ${bold('Verify after minting token 0:')} ${bold(`abx verify ${clone}`)} ${dim('— confirms the thumbnail is a real render, not the placeholder')}`);
2930
3614
  console.log(` ${dim('live view:')} ${baseUrl}/a/${chainId}/${clone}/0 ${dim('· tokenURI:')} abx tokenuri ${clone}\n`);
2931
3615
  }
3616
+ // ── deploy-code --copies: EditionCode ─────────────────────────────────────────────────────────────
3617
+ // A generative/code drop as copies — the full generative stack (on-chain script, PostParams, a
3618
+ // mint-time seed) minus the parts this pass deliberately did not port (see below). Kept as its own
3619
+ // function for the same byte-identical-721-lane reason as the other two edition bodies.
3620
+ //
3621
+ // v1 scope (reported, not hidden), each refused in code rather than silently ignored:
3622
+ // • `--code-dir` (directory-build code drops) — not ported; use `--script`.
3623
+ // • `--dep` / `--dep-registry` (declared code dependencies) — not ported; ship a self-contained
3624
+ // script (the skill already recommends zero-dependency vanilla JS as the cleanest option).
3625
+ // • `--image-renderer` / `--attributes-renderer` / `--image-base` (the in-chain-Solidity-art and
3626
+ // deterministic-off-chain-thumbnail lanes) — not ported.
3627
+ // • `--no-delegation` — refused outright: EditionCode has no TokenOwner-leg delegation to opt out
3628
+ // of (its auth leg generalizes to "any holder", per the parity plan's ConfigurableParams note).
3629
+ // • `--resume` — refused (see the `isEditionContract` guard in `cmdDeployCodeBody`'s own --resume
3630
+ // handling): the SDK's resume.ts assumes the 721 mint/supply shape throughout.
3631
+ // Every other combination `--script` + `--onchain-uri`/`--public-base-url` supports on the 721 lane
3632
+ // (schemas, seed opt-out, mint timing, royalty, identity fields, --721c→1155C) works here too.
3633
+ export const DEPLOY_CODE_EDITION_FLAGS = new Set([
3634
+ 'script', 'name', 'symbol', 'description', 'external-url', 'description-onchain',
3635
+ ...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag),
3636
+ 'max', 'mint-count', 'mint-all', 'no-mint', 'mint-amount', 'unpaused', 'minter', 'primary-payee', 'royalty-bps', '721c',
3637
+ 'schema', 'no-seed',
3638
+ 'public-base-url', 'onchain-uri', 'generator', 'renderer', 'port',
3639
+ 'sign', 'unsigned', 'for', 'salt', 'factory', 'bootstrap-factory', 'sign-url-file',
3640
+ 'dry-run', 'confirm', 'yes', 'json', 'copies',
3641
+ // v1-scope-cut flags: kept in the ALLOWLIST (not stray) so they reach this function's own
3642
+ // pointed refusal below, rather than the generic "unrecognized flag" `refuseStrayFlags` would
3643
+ // give them otherwise — a real but less helpful refusal (it doesn't say WHY, or the way out).
3644
+ // (`resume` is NOT here: --resume + --copies is refused earlier, at cmdDeployCode's dispatch —
3645
+ // this function never runs with flags.resume set, so listing it here would just be dead.)
3646
+ 'code-dir', 'dep', 'dep-registry', 'image-renderer', 'attributes-renderer', 'image-base', 'no-delegation',
3647
+ ]);
3648
+ export async function cmdDeployEditionCodeBody(flags, emit) {
3649
+ const usage = 'abx deploy-code --copies <n|open> --script <file> --name "Title" --symbol SYM (--public-base-url https://your.resolver.domain | --onchain-uri) ' +
3650
+ '[--description "<s>"] [--external-url <url>] [--max N] [--mint-count N | --mint-all] [--mint-amount N] ' +
3651
+ '[--schema key:Type:Auth,…] [--no-seed] [--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..]';
3652
+ refuseStrayFlags(flags, DEPLOY_CODE_EDITION_FLAGS, 'deploy-code');
3653
+ warnSignWithoutFor(flags);
3654
+ const editionSize = parseCopies(flags.copies);
3655
+ if (flags['code-dir']) {
3656
+ throw new Error('--code-dir is not yet supported for edition code deploys (--copies) — use --script (on-chain chunks) instead. Tracked as a follow-up.');
3657
+ }
3658
+ if (!flags.script)
3659
+ throw new Error(usage);
3660
+ if (flags.dep || flags['dep-registry']) {
3661
+ throw new Error('--dep/--dep-registry are not yet supported for edition code deploys (--copies) — ship a self-contained script ' +
3662
+ '(no external dependency declarations) for now, or drop --copies for the 721 lane. Tracked as a follow-up.');
3663
+ }
3664
+ if (flags['image-renderer'] || flags['attributes-renderer'] || flags['image-base']) {
3665
+ throw new Error('--image-renderer/--attributes-renderer/--image-base are not yet supported for edition code deploys (--copies). Tracked as a follow-up.');
3666
+ }
3667
+ // Not a scope cut — semantically refused: EditionCode has no TokenOwner-leg delegation to opt
3668
+ // out of. Its Params auth leg generalizes to "any holder" (balanceOf(sender, id) > 0) instead of
3669
+ // the 721 delegate.xyz leg --no-delegation exists to disable (see EditionCodeInitParams's own
3670
+ // doc comment — it has no `disableTokenOwnerDelegation` field at all).
3671
+ if (flags['no-delegation'] !== undefined) {
3672
+ throw new Error('--no-delegation has nothing to disable on an EditionCode: its Params auth leg generalizes to "any holder" ' +
3673
+ '(balanceOf(sender, id) > 0) instead of the 721 TokenOwner leg\'s delegate.xyz resolution --no-delegation opts out ' +
3674
+ 'of. Drop the flag.');
3675
+ }
3676
+ const dryRun = isDryRun(flags);
3677
+ const name = flags.name ?? 'ABX Edition Code';
3678
+ const symbol = flags.symbol ?? 'ABXEC';
3679
+ assertRealIdentity(flags, { name, symbol, dryRun });
3680
+ const max = Number(flags.max ?? 16);
3681
+ if (!Number.isInteger(max) || max <= 0)
3682
+ throw new Error('--max must be a positive integer');
3683
+ if (flags['no-mint'] !== undefined && (flags['mint-all'] !== undefined || flags['mint-count'] !== undefined)) {
3684
+ throw new Error('--no-mint contradicts --mint-all/--mint-count — drop one.');
3685
+ }
3686
+ const mintCount = flags['no-mint'] !== undefined ? 0 : flags['mint-all'] !== undefined ? max : Number(flags['mint-count'] ?? 0);
3687
+ if (mintCount > max)
3688
+ throw new Error(`--mint-count ${mintCount} exceeds --max ${max}`);
3689
+ const mintAmount = mintCount > 0 ? (flags['mint-amount'] !== undefined ? parseNonNegativeIntFlag(flags['mint-amount'], 'mint-amount') : 1n) : 0n;
3690
+ if (mintCount > 0 && mintAmount === 0n) {
3691
+ throw new Error('--mint-amount 0 with ids being pre-minted at deploy makes no sense — pass a positive --mint-amount, or drop --mint-all/--mint-count to defer minting entirely.');
3692
+ }
3693
+ const paused = flags.unpaused === undefined;
3694
+ const royaltyBps = Number(flags['royalty-bps'] ?? 500);
3695
+ const port = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
3696
+ const baseUrl = (flags['public-base-url'] ?? resolveBaseUrl(port)).replace(/\/$/, '');
3697
+ const hasPublicUrl = !!(flags['public-base-url'] || process.env.ABX_PUBLIC_BASE_URL);
3698
+ const onChainUri = !!flags['onchain-uri'];
3699
+ console.log(bold(`\n ABX Self-Host Toolkit — deploy edition code project\n ${dim(editionSize === 0n
3700
+ ? 'EditionCode (ERC-1155) — a program is the content; each id an OPEN edition'
3701
+ : `EditionCode (ERC-1155) — a program is the content; ${editionSize} cop${editionSize === 1n ? 'y' : 'ies'} of each id`)}`));
3702
+ if (editionSize === 1n)
3703
+ info(copiesOneNote('deploy-code'));
3704
+ if (!onChainUri && (!hasPublicUrl || loopbackBaseUrl(baseUrl))) {
3705
+ const devAllow = process.env.ABX_DEV_ALLOW_LOCALHOST_URI === '1';
3706
+ const msg = `A code project resolves its metadata + live view through your resolver, so this deploy bakes that resolver's PUBLIC URL on-chain — ${hasPublicUrl ? baseUrl : 'localhost'} resolves for no one. ` +
3707
+ `Pick a real path:\n • Fully on-chain, no server: --onchain-uri\n • Hosted resolver: abx deploy-resolver --provider fly --domain meta.you.xyz, then --public-base-url https://meta.you.xyz`;
3708
+ if (!devAllow) {
3709
+ if (dryRun)
3710
+ warn(`would REFUSE to deploy — ${msg}`);
3711
+ else
3712
+ throw new Error(msg);
3713
+ }
3714
+ else {
3715
+ warn(`DEV ONLY (ABX_DEV_ALLOW_LOCALHOST_URI): baking ${bold(baseUrl)} on-chain — resolves only on THIS machine; not a real NFT.`);
3716
+ }
3717
+ }
3718
+ const publicClient = makePublicClient({ chainKey: CHAIN });
3719
+ await assertChainId(CHAIN, { allowUnreachable: dryRun });
3720
+ const chainId = dryRun ? resolveChain(CHAIN).id : (await publicClient.getChainId());
3721
+ const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun, '1155C');
3722
+ step('Trust anchor');
3723
+ let factory;
3724
+ if (dryRun) {
3725
+ const existing = editionCodeFactoryAddress(flags.factory);
3726
+ if (!existing) {
3727
+ info('no canonical EditionCode factory for this chain yet — a real deploy would deploy the trust anchor first (or --bootstrap-factory).');
3728
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
3729
+ return;
3730
+ }
3731
+ factory = existing;
3732
+ info(`would reuse canonical EditionCode factory ${factory}`);
3733
+ }
3734
+ else {
3735
+ factory = await ensureEditionCodeFactory(publicClient, flags.factory, !!flags['bootstrap-factory']);
3736
+ }
3737
+ const seedSource = flags['no-seed'] !== undefined
3738
+ ? zeroAddress
3739
+ : dryRun
3740
+ ? (resolveSeedSource(chainId) ?? zeroAddress)
3741
+ : await ensureSeedSource(publicClient);
3742
+ let generator = zeroAddress;
3743
+ let metadataRenderer = zeroAddress;
3744
+ if (onChainUri) {
3745
+ step('On-chain URI');
3746
+ const knownGenerator = resolveGenerator(chainId, flags.generator);
3747
+ if (!knownGenerator) {
3748
+ throw new Error(`--onchain-uri needs the canonical AbxGenerator for '${CHAIN}', and none is configured. Set ABX_GENERATOR / --generator, ` +
3749
+ `or check the shipped manifest (packages/sdk/src/deployments.ts).`);
3750
+ }
3751
+ generator = knownGenerator;
3752
+ if (!dryRun) {
3753
+ const generatorCode = await publicClient.getCode({ address: generator });
3754
+ if (!generatorCode || generatorCode === '0x')
3755
+ throw new Error(`the configured generator ${generator} has no code on '${CHAIN}' — check ABX_GENERATOR / --generator / the RPC endpoint.`);
3756
+ }
3757
+ info(dryRun ? `would point animation_url at the canonical generator ${generator} (computed on-chain)` : `animation_url computes ON-CHAIN via the canonical generator ${generator}`);
3758
+ metadataRenderer = dryRun ? (rendererAddress(flags.renderer) ?? zeroAddress) : await ensureRenderer(flags.renderer);
3759
+ info(metadataRenderer === zeroAddress
3760
+ ? 'would deploy the canonical metadata renderer first, then set the URI renderers to it in the setup multicall'
3761
+ : `uri()/contractURI() resolve ON-CHAIN via the renderer ${metadataRenderer} — no resolver in the token's graph.`);
3762
+ if (!hasPublicUrl)
3763
+ info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
3764
+ }
3765
+ const schemas = parseSchemaSpecs(flags.schema);
3766
+ step('Content');
3767
+ const scriptPath = flags.script;
3768
+ const source = readFileSync(resolvePath(scriptPath), 'utf8');
3769
+ const bytes = new TextEncoder().encode(source);
3770
+ const CHUNK = 22_000; // SSTORE2 headroom
3771
+ const scriptChunks = [];
3772
+ for (let off = 0; off < bytes.length; off += CHUNK)
3773
+ scriptChunks.push(toHex(bytes.slice(off, off + CHUNK)));
3774
+ ok(`script ${bytes.length} bytes → ${scriptChunks.length} on-chain chunk(s)`);
3775
+ const contractFields = [];
3776
+ if (flags.description)
3777
+ contractFields.push({ field: encodeTag(F.description), representation: encodeTag(R.inline), value: toHex(String(flags.description)) });
3778
+ if (flags['external-url'])
3779
+ contractFields.push({ field: encodeTag(F.externalUrl), representation: encodeTag(R.inline), value: toHex(String(flags['external-url'])) });
3780
+ contractFields.push(...authorshipContractFields(flags));
3781
+ const onchainUriLegs = onChainUri ? onchainUriSetupCalls({ generator, metadataRenderer }) : [];
3782
+ step('Deploy');
3783
+ const lane = laneFromFlags(flags);
3784
+ const minter = flags.minter ?? zeroAddress;
3785
+ const primaryPayee = flags['primary-payee'] ?? zeroAddress;
3786
+ const explicitSalt = parseSaltFlag(flags.salt);
3787
+ // Always TWO transactions (deploy + one setup multicall) — this leaner lane skips the 721 code
3788
+ // lane's "fold reserve mints + renderers into init" micro-optimization (only reachable there for
3789
+ // a renderer-only, no-program drop, which this function refuses anyway — every edition-code
3790
+ // deploy has a program).
3791
+ const initParamsFor = (owner) => ({
3792
+ owner,
3793
+ name,
3794
+ symbol,
3795
+ tokenURIBase: onChainUri && !hasPublicUrl ? '' : `${baseUrl}/t`,
3796
+ tokenURIRenderer: zeroAddress,
3797
+ contractURIBase: onChainUri && !hasPublicUrl ? '' : `${baseUrl}/c`,
3798
+ contractURIRenderer: zeroAddress,
3799
+ royaltyReceiver: owner,
3800
+ royaltyBps,
3801
+ transferValidator,
3802
+ maxInvocations: BigInt(max),
3803
+ editionSize,
3804
+ primaryPayee,
3805
+ minter,
3806
+ paused,
3807
+ seedSource,
3808
+ mintTo: zeroAddress,
3809
+ mintCount: 0n,
3810
+ mintAmount: 0n,
3811
+ tokenFields: [],
3812
+ contractFields,
3813
+ });
3814
+ const setupCallsFor = (owner) => {
3815
+ const calls = [];
3816
+ for (let i = 0; i < scriptChunks.length; i++) {
3817
+ calls.push(encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), scriptChunks[i]] }));
3818
+ }
3819
+ for (const s of schemas) {
3820
+ calls.push(encodeFunctionData({
3821
+ abi: seriesCodeAbi,
3822
+ functionName: 'setParamSchema',
3823
+ args: [encodeTag(s.key), s.paramType, s.auth, s.authAddress, s.lockAfter, s.min, s.max, s.selectOptions],
3824
+ }));
3825
+ }
3826
+ calls.push(...onchainUriLegs);
3827
+ // Reserve mints: one `mint(to, id, amount)` per premint id — the edition twin of the 721 lane's
3828
+ // N identical `mint(owner)` calls, one id finer (a specific amount of copies, not just "one").
3829
+ for (let id = 0; id < mintCount; id++) {
3830
+ calls.push(encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'mint', args: [owner, BigInt(id), mintAmount] }));
3831
+ }
3832
+ return calls;
3833
+ };
3834
+ const preparedFor = async (owner) => {
3835
+ const salt = explicitSalt ?? saltFor(owner);
3836
+ const clone = await predictClone(publicClient, { factory, salt });
3837
+ const calls = setupCallsFor(owner);
3838
+ const txs = [prepareDeployEditionCode({ factory, params: initParamsFor(owner), salt, chainId, clone })];
3839
+ if (calls.length) {
3840
+ txs.push(prepareCodeSetup({
3841
+ contract: clone,
3842
+ calls,
3843
+ chainId,
3844
+ chunkCount: scriptChunks.length,
3845
+ chunkBytes: scriptChunks.map((h) => (h.length - 2) / 2),
3846
+ schemaKeys: schemas.map((s) => s.key),
3847
+ onchainUri: onChainUri,
3848
+ }));
3849
+ }
3850
+ return { clone, txs };
3851
+ };
3852
+ const approvals = 1 + (scriptChunks.length + schemas.length + onchainUriLegs.length + mintCount > 0 ? 1 : 0);
3853
+ if (!dryRun) {
3854
+ await confirmSend(`About to deploy an edition code project "${name}" (${symbol}) — script ${bytes.length} bytes → ${scriptChunks.length} chunk(s); ` +
3855
+ `up to ${max} id(s) × ${editionSize === 0n ? 'open' : editionSize.toString()} cop${editionSize === 1n ? 'y' : 'ies'} each; ` +
3856
+ `${onChainUri ? `tokenURI ON-CHAIN via the renderer (generator ${generator})` : `resolver base ${baseUrl}`}; ` +
3857
+ `mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
3858
+ (transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
3859
+ }
3860
+ if (dryRun) {
3861
+ let deployer = null;
3862
+ if (flags.for)
3863
+ deployer = flags.for;
3864
+ else {
3865
+ try {
3866
+ deployer = makeWalletClient({ chainKey: CHAIN }).account.address;
3867
+ }
3868
+ catch {
3869
+ deployer = null;
3870
+ }
3871
+ }
3872
+ const salt = deployer ? (explicitSalt ?? saltFor(deployer)) : null;
3873
+ const predicted = salt ? await predictClone(publicClient, { factory, salt }) : null;
3874
+ const shownAddr = explicitSalt ? predicted : null;
3875
+ step(`Deploy plan — up to ${max} id(s), ${editionSize === 0n ? 'open' : editionSize.toString()} cop${editionSize === 1n ? 'y' : 'ies'} each, to ${CHAIN}`);
3876
+ if (deployer)
3877
+ info(`deployer ${deployer}`);
3878
+ else
3879
+ info(`deployer: ${dim('not set')} — pass ${bold('--for 0x..')} to preview the exact deterministic address.`);
3880
+ if (shownAddr)
3881
+ info(`deterministic address: ${shownAddr}`);
3882
+ info(`name "${name}" · symbol ${symbol} · max ${max} id(s) · royalty ${royaltyBps / 100}%`);
3883
+ info(`content: script ${bytes.length} bytes → ${scriptChunks.length} on-chain chunk(s)`);
3884
+ info(`seed source: ${seedSource === zeroAddress ? 'none (--no-seed / not configured)' : seedSource}`);
3885
+ info(`PostParam schema(s): ${schemas.length ? schemas.map((s) => describeSchema(s)).join(', ') : 'none'}`);
3886
+ info(`mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${deployer ?? 'your wallet'} at deploy` : 'deferred (mint later / external minter)'}`);
3887
+ info(`transactions: 2 — deploy + setup multicall (script chunks${schemas.length ? ' + schemas' : ''}${onChainUri ? ' + on-chain URI wiring' : ''}${mintCount > 0 ? ' + reserve mints' : ''})`);
3888
+ if (!explicitSalt && deployer) {
3889
+ console.log(`\n ${bold('salt')} ${g(salt)}`);
3890
+ info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
3891
+ }
3892
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
3893
+ emit(jsonSafe({ command: 'deploy-code', kind: 'edition-code', copies: editionSize.toString(), dryRun: true, sent: false, address: shownAddr, chain: CHAIN, chainId, factory, name, symbol }));
3894
+ return;
3895
+ }
3896
+ let clone;
3897
+ let deployBlock;
3898
+ if (lane === 'unsigned') {
3899
+ const signer = flags.for;
3900
+ if (!signer)
3901
+ throw new Error('--unsigned needs --for <signer> — the salt guard and ownership are keyed to the signing address');
3902
+ const { clone: predicted, txs } = await preparedFor(signer);
3903
+ for (const tx of txs)
3904
+ await signTx(() => tx, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: signer });
3905
+ console.log(`\n${dim(` unsigned — broadcast in order, then: abx add ${predicted} --factory ${factory} --from-block <deployBlock>`)}\n`);
3906
+ return;
3907
+ }
3908
+ if (lane === 'sign') {
3909
+ const session = await openWalletSession({ chainKey: CHAIN, expectedSigner: flags.for, total: approvals, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
3910
+ try {
3911
+ const signer = await session.connect();
3912
+ const { clone: predicted, txs } = await preparedFor(signer);
3913
+ let first;
3914
+ for (const tx of txs) {
3915
+ const sent = await session.send(tx);
3916
+ if (first === undefined)
3917
+ first = sent.receipt.blockNumber;
3918
+ }
3919
+ clone = predicted;
3920
+ deployBlock = first ?? 0n;
3921
+ }
3922
+ finally {
3923
+ session.close();
3924
+ }
3925
+ }
3926
+ else {
3927
+ const { account } = makeWalletClient({ chainKey: CHAIN });
3928
+ const { clone: predicted, txs } = await preparedFor(account.address);
3929
+ const results = await signHotSequence(txs, { chainKey: CHAIN, yes: !!flags.yes });
3930
+ clone = predicted;
3931
+ deployBlock = results[0]?.blockNumber ?? 0n;
3932
+ }
3933
+ const discovered = await discoverDeployBlock(publicClient, clone);
3934
+ if (discovered !== null)
3935
+ deployBlock = discovered;
3936
+ ok(`EditionCode live: ${clone} ${dim(`(from block ${deployBlock})`)}`);
3937
+ emit(jsonSafe({ command: 'deploy-code', kind: 'edition-code', copies: editionSize.toString(), address: clone, chain: CHAIN, chainId, factory, deployBlock, name, symbol, onChainUri }));
3938
+ step('Index');
3939
+ await registerAndIndexLocally(clone, { 'from-block': deployBlock.toString(), factory, label: name });
3940
+ if (onChainUri) {
3941
+ step('On-chain URI');
3942
+ ok(`uri()/contractURI() resolve ON-CHAIN via the renderer ${metadataRenderer} — any RPC returns the metadata, no server in the graph`);
3943
+ info(`animation_url computes on-chain via the generator ${generator} — it enumerates this token's params from chain, so there is no key list to maintain`);
3944
+ console.log(` ${bold(`abx tokenuri ${clone} --token 0`)} ${dim('# reads uri(0) + decodes the JSON straight from the contract')}`);
3945
+ }
3946
+ const isRemoteBase = !/^https?:\/\/(localhost|127\.0\.0\.1)\b/i.test(baseUrl);
3947
+ console.log(`\n${g('Edition code project deployed.')} ${dim(onChainUri ? 'tokenURI is on-chain; a resolver remains the kinder serving path (live view + thumbnail publishing):' : 'It resolves through your resolver — keep it running:')}`);
3948
+ if (isRemoteBase)
3949
+ console.log(` ${bold(`abx add ${clone} --remote`)} ${dim('# register with the hosted resolver')}`);
3950
+ else
3951
+ console.log(` ${bold('abx serve')} ${dim('# stand up the resolver locally')}`);
3952
+ console.log(` ${bold(`abx mint ${clone} --token-id <id> --amount <n>`)} ${dim('# mint more copies of an id')}`);
3953
+ console.log(` ${bold(`abx minter configure ${clone} --token-id <id> --price <eth> --allocation <n>`)} ${dim('# set up a priced sale for one id')}`);
3954
+ console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces once metadata is live')}\n`);
3955
+ }
2932
3956
  /**
2933
3957
  * Resolve `--721c` into the InitParams `transferValidator`. Absent → `zeroAddress`: a plain
2934
- * ERC-721, byte-for-byte the pre-721C deploy — no prompts, no output, no mention of 721C.
2935
- * `--721c` / `--721c recommended` → the per-chain recommended validator (refused, naming the
2936
- * chains that have one, when the manifest has no entry); `--721c 0x…` → checksum-validated AND
2937
- * pre-checked for code on this chain (the factory would revert `InvalidTransferValidator()` —
2938
- * surface it before any upload/staging/gas). When enrolling, prints the one plain statement of
2939
- * what enforcement means — once, adapted to the chosen validator.
3958
+ * ERC-721 (or, on an edition deploy, a plain ERC-1155), byte-for-byte the pre-721C deploy — no
3959
+ * prompts, no output, no mention of 721C. `--721c` / `--721c recommended` → the per-chain
3960
+ * recommended validator (refused, naming the chains that have one, when the manifest has no
3961
+ * entry); `--721c 0x…` → checksum-validated AND pre-checked for code on this chain (the factory
3962
+ * would revert `InvalidTransferValidator()` — surface it before any upload/staging/gas). When
3963
+ * enrolling, prints the one plain statement of what enforcement means — once, adapted to the
3964
+ * chosen validator.
3965
+ *
3966
+ * `standard` picks only the WORDING (`ERC-721C` vs `ERC-1155C`) — the flag, the value grammar, and
3967
+ * the resolved validator address are identical either way (the SAME `InitParams.transferValidator`
3968
+ * opt-in per the parity plan's locked decision 3: 1155C ships with the same UX as 721C). Editions
3969
+ * pass `'1155C'`; every 721 call site keeps the default, so their output is unchanged.
2940
3970
  */
2941
- export async function resolveTransferValidatorFlag(flags, publicClient, dryRun) {
3971
+ export async function resolveTransferValidatorFlag(flags, publicClient, dryRun, standard = '721C') {
2942
3972
  const raw = flags['721c'];
2943
3973
  if (raw === undefined)
2944
3974
  return zeroAddress;
@@ -2967,8 +3997,8 @@ export async function resolveTransferValidatorFlag(flags, publicClient, dryRun)
2967
3997
  }
2968
3998
  // The one plain statement — printed once, only when enrolling. Never appears un-enrolled.
2969
3999
  info(validator === resolveRecommendedTransferValidator(chainId)
2970
- ? `ERC-721C enforcement: only owner-initiated transfers and OpenSea-authorized sales will transfer; other marketplaces/operators are blocked. Manage with \`abx set-transfer-validator\`.`
2971
- : `ERC-721C enforcement: every non-mint transfer is checked by validator ${validator} — its policy decides which operators may transfer. Manage with \`abx set-transfer-validator\`.`);
4000
+ ? `ERC-${standard}: only owner-initiated transfers and OpenSea-authorized sales will transfer; other marketplaces/operators are blocked. Manage with \`abx set-transfer-validator\`.`
4001
+ : `ERC-${standard}: every non-mint transfer is checked by validator ${validator} — its policy decides which operators may transfer. Manage with \`abx set-transfer-validator\`.`);
2972
4002
  return validator;
2973
4003
  }
2974
4004
  //# sourceMappingURL=deploy.js.map