@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.
- package/CHANGELOG.md +68 -0
- package/dist/commands/deploy.d.ts +32 -7
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1079 -49
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/project.d.ts +107 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +141 -16
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +52 -20
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts +13 -11
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +73 -37
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/kind.d.ts +48 -0
- package/dist/kind.d.ts.map +1 -0
- package/dist/kind.js +69 -0
- package/dist/kind.js.map +1 -0
- package/dist/main.js +107 -23
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts +14 -1
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +166 -4
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +11 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +133 -3
- package/dist/output.js.map +1 -1
- package/dist/ownerops.d.ts +33 -2
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +356 -19
- package/dist/ownerops.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +5 -3
- package/skill/reference/creator-token.md +40 -25
- package/skill/reference/hosting.md +1 -1
- package/skill/reference/operating.md +20 -0
- package/skill/reference/troubleshooting.md +7 -0
package/dist/commands/deploy.js
CHANGED
|
@@ -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.
|
|
35
|
-
// (
|
|
36
|
-
//
|
|
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
|
-
|
|
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-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
//
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
//
|
|
1656
|
-
//
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const
|
|
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
|
|
2935
|
-
* `--721c` / `--721c recommended` → the per-chain
|
|
2936
|
-
* chains that have one, when the manifest has no
|
|
2937
|
-
* pre-checked for code on this chain (the factory
|
|
2938
|
-
* surface it before any upload/staging/gas). When
|
|
2939
|
-
* what enforcement means — once, adapted to the
|
|
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
|
|
2971
|
-
: `ERC
|
|
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
|