@artblocks/abx-cli 0.1.0-alpha.40 → 0.1.0-alpha.42
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 +222 -0
- package/assets/renderer-scaffold/README.md +12 -0
- package/assets/renderer-scaffold/foundry.toml +4 -0
- package/assets/renderer-scaffold/script/Preview.s.sol +99 -0
- package/dist/capabilities.d.ts +9 -4
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +13 -4
- package/dist/capabilities.js.map +1 -1
- package/dist/commands/deploy.d.ts +51 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1008 -172
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/maintenance.d.ts +4 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +114 -0
- package/dist/commands/maintenance.js.map +1 -0
- package/dist/commands/project.d.ts +175 -3
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +359 -29
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts +21 -0
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +171 -4
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +5 -0
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/service.d.ts.map +1 -1
- package/dist/commands/service.js +7 -0
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +32 -3
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/submit-app.d.ts +43 -0
- package/dist/commands/submit-app.d.ts.map +1 -1
- package/dist/commands/submit-app.js +115 -6
- package/dist/commands/submit-app.js.map +1 -1
- package/dist/config.d.ts +16 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +73 -7
- package/dist/config.js.map +1 -1
- package/dist/deploy-plan.d.ts +221 -0
- package/dist/deploy-plan.d.ts.map +1 -0
- package/dist/deploy-plan.js +20 -0
- package/dist/deploy-plan.js.map +1 -0
- package/dist/flag-allowlists.d.ts.map +1 -1
- package/dist/flag-allowlists.js +5 -1
- package/dist/flag-allowlists.js.map +1 -1
- package/dist/flags.d.ts +6 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +14 -0
- package/dist/flags.js.map +1 -1
- package/dist/jsonout.d.ts.map +1 -1
- package/dist/jsonout.js +11 -0
- package/dist/jsonout.js.map +1 -1
- package/dist/main.js +62 -13
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +139 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +382 -19
- package/dist/ownerops.js.map +1 -1
- package/dist/riskgate.d.ts +36 -0
- package/dist/riskgate.d.ts.map +1 -1
- package/dist/riskgate.js +91 -17
- package/dist/riskgate.js.map +1 -1
- package/dist/script-chunks.d.ts +1 -1
- package/dist/script-chunks.d.ts.map +1 -1
- package/dist/script-chunks.js +2 -2
- package/dist/script-chunks.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +6 -4
- package/skill/reference/code.md +31 -8
- package/skill/reference/deploy.md +9 -2
- package/skill/reference/diagnose.md +15 -2
- package/skill/reference/hosting.md +26 -0
- package/skill/reference/operate.md +55 -4
- package/skill/reference/setup.md +14 -0
package/dist/commands/deploy.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
16
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
17
|
import { basename, extname, join as joinPath, resolve as resolvePath } from 'node:path';
|
|
18
|
-
import { DEFAULT_CHAIN_KEY, DEP_RESOLUTION, METADATA_FIELD as F, contentIdFromLocator, METADATA_REPRESENTATION as R, analyzeScript, assertChainId, checkRegistryDeps, dependencySetupCalls, deployOneOfOne, deploySeries, deployOneOfOneEdition, deployEditionImage, discoverDeployBlock, encodeFieldRenderer, encodeTag, expectedChainComplete, makeHotSender, makePublicClient, makeWalletClient, normalizeAttributes, onchainUriSetupCalls, oneOfOneImageAbi, oneOfOneEditionAbi, parseTraitPairs, planResume, predictClone, probeSeedSource, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, prepareDeployOneOfOneEdition, prepareDeployEditionImage, prepareDeployEditionCode, redactRpcUrl, resolveChain, resolveDepRegistryPointer, resolveGenerator, probeTransferValidator, resolveRecommendedTransferValidator, resolveRpcUrl, resolveSeedSource, resolveSeriesCodeFactory, saltFor, seriesCodeAbi, seriesCodeFactoryAbi, tryReadContract, AbxServiceClient, } from '@artblocks/abx-sdk';
|
|
18
|
+
import { DEFAULT_CHAIN_KEY, DEP_RESOLUTION, METADATA_FIELD as F, contentIdFromLocator, METADATA_REPRESENTATION as R, analyzeScript, assertChainId, checkRegistryDeps, dependencySetupCalls, DEFAULT_TX_GAS_BUDGET, MEASURED_ESTIMATE_GAS_ALLOWANCE, SETUP_LEG_GAS, estimateChunkGasForBytes, packCallsByGas, deployOneOfOne, deploySeries, deployOneOfOneEdition, deployEditionImage, discoverDeployBlock, encodeFieldRenderer, encodeTag, expectedChainComplete, makeHotSender, makePublicClient, makeWalletClient, normalizeAttributes, onchainUriSetupCalls, oneOfOneImageAbi, oneOfOneEditionAbi, editionCodeAbi, parseTraitPairs, planResume, planEditionResume, predictClone, probeSeedSource, prepareCodeSetup, prepareDeployOneOfOne, prepareDeploySeries, prepareDeploySeriesCode, prepareDeployOneOfOneEdition, prepareDeployEditionImage, prepareDeployEditionCode, redactRpcUrl, resolveChain, resolveDepRegistryPointer, resolveGenerator, probeTransferValidator, resolveRecommendedTransferValidator, resolveRpcUrl, resolveSeedSource, resolveSeriesCodeFactory, saltFor, seriesCodeAbi, seriesCodeFactoryAbi, tryReadContract, AbxServiceClient, } from '@artblocks/abx-sdk';
|
|
19
19
|
import { DIRECT_URL_BACKENDS, contentTypeFromPath, decideImageContentLane, hashContent, isTurboArweave, resolveBackend, validateRenderStorageCombo, } from '@artblocks/abx-storage';
|
|
20
20
|
import { DEFAULT_PORT, contentHash, generateContent, resolveBaseUrl, startTokenApiServer } from '@artblocks/abx-token-api';
|
|
21
21
|
import { encodeFunctionData, getAddress, toHex, zeroAddress } from 'viem';
|
|
@@ -25,7 +25,8 @@ import { isDryRun, parseSaltFlag, refuseStrayFlags, warnSignWithoutFor } from '.
|
|
|
25
25
|
import { isEditionContract } from '../kind.js';
|
|
26
26
|
import { jsonSafe, withJson } from '../jsonout.js';
|
|
27
27
|
import { planOnChainScript } from '../script-chunks.js';
|
|
28
|
-
import { bold, c, dim, ensureFactory, ensureRenderer, ensureSeedSource, ensureSeriesCodeFactory, ensureSeriesFactory, ensureOneOfOneEditionFactory, ensureEditionFactory, ensureEditionCodeFactory, g, info, keepAlive, ok, p, portInUse, printServing, registerAndIndexLocally, reindexAfterDeploy, step, warn, } from '../output.js';
|
|
28
|
+
import { bold, c, dim, ensureFactory, ensureRenderer, ensureSeedSource, ensureSeriesCodeFactory, ensureSeriesFactory, ensureOneOfOneEditionFactory, ensureEditionFactory, ensureEditionCodeFactory, g, info, keepAlive, ok, p, portInUse, printServing, registerAndIndexLocally, reindexAfterDeploy, step, warn as printWarn, } from '../output.js';
|
|
29
|
+
import { DEPLOY_PLAN_SCHEMA_VERSION, stripAnsiForPlan, } from '../deploy-plan.js';
|
|
29
30
|
import { parseRoyaltyBps, AUTHORSHIP_DEPLOY_FIELDS, ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, tokenUriGasEstimate, authorshipContractFields, gatewayContractFields, servingGateway, computeContentPlan, envStagingSender, parseCompress, parseSeedSourceValue, canonicalSeedSource, refuseUnusableSeedSource, parseTransferValidatorValue, guardOnChainSize, previewImageStaging, refusePrewrappedImage, sessionStagingSender, stageImageField, stageImageFieldsBatch, } from '../ownerops.js';
|
|
30
31
|
import { canonicalLabel } from '../remote.js';
|
|
31
32
|
import { assertLaneCanSign, confirmSend, gatedSend, laneFromFlags } from '../riskgate.js';
|
|
@@ -33,6 +34,24 @@ import { describeSchema, editionSchemaAdvisory, parseSchemaSpecs } from '../sche
|
|
|
33
34
|
import { parseSeriesTraits } from '../series-traits.js';
|
|
34
35
|
import { decodeOnChainJson } from '../served.js';
|
|
35
36
|
import { openWalletSession, signHotSequence, signTx } from '../signer.js';
|
|
37
|
+
// ── plan-object warning capture (issue #123) ─────────────────────────────────────────────────────
|
|
38
|
+
// The structured `--json` plan object (deploy-plan.ts) reports "the warnings the lane raised"
|
|
39
|
+
// alongside every other computed field — but ~400 `warn()` call sites in this file exist purely as
|
|
40
|
+
// human prose, and rewriting each to ALSO push onto a collector would be exactly the kind of
|
|
41
|
+
// touch-every-call-site change the plan-object work was scoped to avoid (a wording regression is one
|
|
42
|
+
// typo away, across 17 test files that assert on this prose byte-for-byte). Re-binding the name
|
|
43
|
+
// instead means every existing `warn(...)` call — unchanged, still reading `warn` from this file's own
|
|
44
|
+
// scope — transparently also feeds the plan. `planWarnings` is reset at the top of each
|
|
45
|
+
// `cmdDeploy*Body` (`beginPlanWarnings`) and read once, right before that invocation's `emit()` call(s).
|
|
46
|
+
// Not reentrant/thread-safe by construction — fine, since one CLI process runs exactly one command.
|
|
47
|
+
let planWarnings = [];
|
|
48
|
+
function beginPlanWarnings() {
|
|
49
|
+
planWarnings = [];
|
|
50
|
+
}
|
|
51
|
+
function warn(s) {
|
|
52
|
+
planWarnings.push(stripAnsiForPlan(s));
|
|
53
|
+
printWarn(s);
|
|
54
|
+
}
|
|
36
55
|
// Product dimensions — what you can launch. Two concrete contract FAMILIES exist today: the 721
|
|
37
56
|
// ladder (`--type`, below — 1/1 image or Series) and, orthogonally, ERC-1155 editions (`--copies`,
|
|
38
57
|
// on ANY of the three deploy commands — see `parseCopies`). `--type` picks among unique-token
|
|
@@ -629,6 +648,7 @@ export async function cmdDeploy(flags, serveAfter) {
|
|
|
629
648
|
return withJson(flags, async (emit) => cmdDeployBody(flags, serveAfter, emit));
|
|
630
649
|
}
|
|
631
650
|
export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
651
|
+
beginPlanWarnings(); // see the collector's own module note, above the DIMENSIONS block
|
|
632
652
|
refuseStrayFlags(flags, DEPLOY_FLAGS, 'deploy');
|
|
633
653
|
warnSignWithoutFor(flags);
|
|
634
654
|
const dimension = resolveDimension(flags.type ?? '1of1');
|
|
@@ -906,10 +926,22 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
906
926
|
// never disagree. Counts TX signatures only: a storage upload signed by a connected wallet
|
|
907
927
|
// (Arweave via --storage-signer eth) is a message signature, not a transaction, and is already
|
|
908
928
|
// named separately (see the `remoteEthUpload` narration) — it does not add to this count.
|
|
929
|
+
// Also captures the plan object's `custody.image` file metadata (issue #123 follow-up): the SAME
|
|
930
|
+
// bytes read here for the tx-count math, hashed/typed locally (no chain read) — never re-derived
|
|
931
|
+
// at the emit site below.
|
|
932
|
+
let planImageFile = null;
|
|
909
933
|
const approvals = onchainImage
|
|
910
934
|
? (() => {
|
|
911
|
-
const
|
|
912
|
-
|
|
935
|
+
const bytes = readFileSync(resolvePath(flags.image));
|
|
936
|
+
const compress = parseCompress(flags.compress);
|
|
937
|
+
const contentPlan = computeContentPlan(bytes, compress);
|
|
938
|
+
planImageFile = {
|
|
939
|
+
bytes: bytes.length,
|
|
940
|
+
stagedBytes: contentPlan.stagedBytes,
|
|
941
|
+
mimeType: contentTypeFromPath(resolvePath(flags.image)),
|
|
942
|
+
contentHash: hashContent(bytes),
|
|
943
|
+
};
|
|
944
|
+
return (contentPlan.plan.mode === 'single' ? 1 : contentPlan.plan.txCount) + 1; // staging tx(s) + the deploy tx
|
|
913
945
|
})()
|
|
914
946
|
: 1;
|
|
915
947
|
// Mint-on-deploy is the default; --no-mint defers it so you can stand up + warm
|
|
@@ -978,6 +1010,10 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
978
1010
|
};
|
|
979
1011
|
let clone;
|
|
980
1012
|
let blockNumber;
|
|
1013
|
+
// The eventual owner/signer — set in whichever lane branch below learns it, so the SENT plan
|
|
1014
|
+
// object (further down) can report `roles` without re-deriving anything. `undefined` only while
|
|
1015
|
+
// no branch has run yet; every path that reaches the sent `emit()` has set it by then.
|
|
1016
|
+
let deployerAddr;
|
|
981
1017
|
if (dryRun) {
|
|
982
1018
|
let deployer;
|
|
983
1019
|
if (flags.for)
|
|
@@ -1035,7 +1071,41 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1035
1071
|
// A dry run DOES know the address when --salt pinned it (a pure function of factory+salt); without
|
|
1036
1072
|
// --salt it's null — reporting the freshly-reserved one would be a real-looking value a script
|
|
1037
1073
|
// could act on that the actual deploy will not land at. `saltPinned` still says why.
|
|
1038
|
-
emit(jsonSafe({
|
|
1074
|
+
emit(jsonSafe({
|
|
1075
|
+
command: 'deploy', dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol,
|
|
1076
|
+
// The structured plan (issue #123) — built from the SAME locals the readout above just
|
|
1077
|
+
// printed; nothing here is re-derived or newly computed. `deployer` is a real wallet address
|
|
1078
|
+
// regardless of salt pinning (only the PREDICTED CONTRACT address carries that caveat, and
|
|
1079
|
+
// this plan doesn't repeat it — see `address`, above), so roles are safe to report unconditionally.
|
|
1080
|
+
plan: {
|
|
1081
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1082
|
+
family: '1of1',
|
|
1083
|
+
lane,
|
|
1084
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging', 'deploy'] : null },
|
|
1085
|
+
// The 1/1 lane's InitParams has no minter/primaryPayee field at all — `null` here means
|
|
1086
|
+
// "no concept on this lane", not "unset".
|
|
1087
|
+
roles: { signer: deployer, owner: deployer, royaltyReceiver: deployer, primaryPayee: null, minter: null },
|
|
1088
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1089
|
+
custody: {
|
|
1090
|
+
onChainUri,
|
|
1091
|
+
imageOnChain: onchainImage,
|
|
1092
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1093
|
+
// `''` (on-chain lane, no explicit --public-base-url) is reported as `null` here — the
|
|
1094
|
+
// human line already spells out WHY (renderer-authoritative); a JSON consumer just needs
|
|
1095
|
+
// "nothing baked", not an empty string to interpret.
|
|
1096
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
1097
|
+
contractUriBase: params.contractURIBase || null,
|
|
1098
|
+
renderer: onChainUri ? renderer : null,
|
|
1099
|
+
image: planImageFile,
|
|
1100
|
+
},
|
|
1101
|
+
mint: { deferred: noMint, count: noMint ? 0 : 1, amountPerId: null, recipient: noMint ? null : deployer },
|
|
1102
|
+
estimate: { ethApprox: null, gasApprox: null }, // the 1/1 lane computes no cost estimate
|
|
1103
|
+
warnings: planWarnings.slice(),
|
|
1104
|
+
surfaces: null, // the code lanes' per-surface disposition has no equivalent here
|
|
1105
|
+
dependencies: null, // the 1/1 lane has no dependency-registry concept
|
|
1106
|
+
resume: null,
|
|
1107
|
+
},
|
|
1108
|
+
}));
|
|
1039
1109
|
return;
|
|
1040
1110
|
}
|
|
1041
1111
|
// opt-in --confirm: a final y/N before the real send (no-op without --confirm; never blocks scripts)
|
|
@@ -1044,6 +1114,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1044
1114
|
if (lane === 'send') {
|
|
1045
1115
|
// hot lane: the env key is the deployer, known up front.
|
|
1046
1116
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
1117
|
+
deployerAddr = account.address;
|
|
1047
1118
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(account.address);
|
|
1048
1119
|
info(`deterministic address: ${predicted}`);
|
|
1049
1120
|
info(describeDeployField('description', flags.description, onChainUri || !!flags['description-onchain']));
|
|
@@ -1081,6 +1152,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1081
1152
|
let r;
|
|
1082
1153
|
try {
|
|
1083
1154
|
const signer = await session.connect();
|
|
1155
|
+
deployerAddr = signer;
|
|
1084
1156
|
// Stage on-chain through the session (sets bakedImage, which buildForDeployer reads below).
|
|
1085
1157
|
const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sessionStagingSender(session));
|
|
1086
1158
|
bakedImage = field;
|
|
@@ -1113,6 +1185,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1113
1185
|
let r;
|
|
1114
1186
|
try {
|
|
1115
1187
|
const signer = await session.connect();
|
|
1188
|
+
deployerAddr = signer;
|
|
1116
1189
|
// Route Turbo uploads' signatures to this session before building content (which uploads).
|
|
1117
1190
|
uploadRemoteEth = { address: signer, signMessage: (m) => session.signMessage(m, 'Sign Arweave upload (paid from your Turbo credits)') };
|
|
1118
1191
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(signer);
|
|
@@ -1137,6 +1210,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1137
1210
|
? 'a wallet will become the owner; the token resolves from chain — no URI base is baked in.'
|
|
1138
1211
|
: `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
1139
1212
|
const result = await signTx(async (signer) => {
|
|
1213
|
+
deployerAddr = signer;
|
|
1140
1214
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
1141
1215
|
return prepareDeployOneOfOne({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
1142
1216
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -1150,7 +1224,34 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1150
1224
|
}
|
|
1151
1225
|
// Emitted the moment the address is known, and again below once minting is settled — so a crash in
|
|
1152
1226
|
// the indexing steps that follow still yields the address of a contract that really does exist.
|
|
1153
|
-
emit(jsonSafe({
|
|
1227
|
+
emit(jsonSafe({
|
|
1228
|
+
command: 'deploy', address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
1229
|
+
plan: {
|
|
1230
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1231
|
+
family: '1of1',
|
|
1232
|
+
lane,
|
|
1233
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging', 'deploy'] : null },
|
|
1234
|
+
roles: { signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null, primaryPayee: null, minter: null },
|
|
1235
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1236
|
+
custody: {
|
|
1237
|
+
onChainUri,
|
|
1238
|
+
imageOnChain: onchainImage,
|
|
1239
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1240
|
+
// Recomputed from the same top-level locals `buildForDeployer`'s `params` used (no branch
|
|
1241
|
+
// carries `params` out to this scope) — never a new decision, the identical ternary.
|
|
1242
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
1243
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
1244
|
+
renderer: onChainUri ? renderer : null,
|
|
1245
|
+
image: planImageFile,
|
|
1246
|
+
},
|
|
1247
|
+
mint: { deferred: noMint, count: noMint ? 0 : 1, amountPerId: null, recipient: noMint ? null : deployerAddr ?? null },
|
|
1248
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
1249
|
+
warnings: planWarnings.slice(),
|
|
1250
|
+
surfaces: null,
|
|
1251
|
+
dependencies: null,
|
|
1252
|
+
resume: null,
|
|
1253
|
+
},
|
|
1254
|
+
}));
|
|
1154
1255
|
step(serveAfter ? 'What the chain knows now' : 'Index it — replay the event spine from chain');
|
|
1155
1256
|
const indexer = localIndexer();
|
|
1156
1257
|
// Off-chain traits ride in the registration (on-chain ones are already in the contract fields).
|
|
@@ -1284,6 +1385,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1284
1385
|
// spreads `DEPLOY_FLAGS`, so it must come AFTER that declaration at module scope, same reason
|
|
1285
1386
|
// `DEPLOY_SERIES_EDITION_FLAGS` sits next to it rather than beside `cmdDeployEditionImageBody`.)
|
|
1286
1387
|
export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
1388
|
+
beginPlanWarnings();
|
|
1287
1389
|
refuseStrayFlags(flags, DEPLOY_EDITION_FLAGS, 'deploy');
|
|
1288
1390
|
warnSignWithoutFor(flags);
|
|
1289
1391
|
// `--type` is the 721 lane's OWN dimension seam (cmdDeployBody redirects `--type series` to
|
|
@@ -1436,10 +1538,21 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1436
1538
|
// The edition paths shipped without this and without the readout line below, so `--copies` previews
|
|
1437
1539
|
// silently dropped the approval count the skill promises "every preview" prints — leaving an agent
|
|
1438
1540
|
// with nothing to tell the creator about how many wallet prompts to expect.
|
|
1541
|
+
// Also captures `custody.image` file metadata (issue #123 follow-up) — see the 1/1 twin's identical
|
|
1542
|
+
// local for why it's computed here rather than re-derived at the emit site.
|
|
1543
|
+
let planImageFile = null;
|
|
1439
1544
|
const approvals = onchainImage
|
|
1440
1545
|
? (() => {
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1546
|
+
const bytes = readFileSync(resolvePath(flags.image));
|
|
1547
|
+
const compress = parseCompress(flags.compress);
|
|
1548
|
+
const contentPlan = computeContentPlan(bytes, compress);
|
|
1549
|
+
planImageFile = {
|
|
1550
|
+
bytes: bytes.length,
|
|
1551
|
+
stagedBytes: contentPlan.stagedBytes,
|
|
1552
|
+
mimeType: contentTypeFromPath(resolvePath(flags.image)),
|
|
1553
|
+
contentHash: hashContent(bytes),
|
|
1554
|
+
};
|
|
1555
|
+
return (contentPlan.plan.mode === 'single' ? 1 : contentPlan.plan.txCount) + 1;
|
|
1443
1556
|
})()
|
|
1444
1557
|
: 1;
|
|
1445
1558
|
if (onChainUri) {
|
|
@@ -1487,6 +1600,8 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1487
1600
|
};
|
|
1488
1601
|
let clone;
|
|
1489
1602
|
let blockNumber;
|
|
1603
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`).
|
|
1604
|
+
let deployerAddr;
|
|
1490
1605
|
if (dryRun) {
|
|
1491
1606
|
let deployer;
|
|
1492
1607
|
if (flags.for)
|
|
@@ -1531,13 +1646,43 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1531
1646
|
}
|
|
1532
1647
|
info(dim('the values above are exactly what a real deploy writes — nothing else is added.'));
|
|
1533
1648
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
|
|
1534
|
-
emit(jsonSafe({
|
|
1649
|
+
emit(jsonSafe({
|
|
1650
|
+
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,
|
|
1651
|
+
plan: {
|
|
1652
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1653
|
+
family: '1of1-edition',
|
|
1654
|
+
lane,
|
|
1655
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging', 'deploy'] : null },
|
|
1656
|
+
roles: {
|
|
1657
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
1658
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
1659
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
1660
|
+
},
|
|
1661
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1662
|
+
custody: {
|
|
1663
|
+
onChainUri,
|
|
1664
|
+
imageOnChain: onchainImage,
|
|
1665
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1666
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
1667
|
+
contractUriBase: params.contractURIBase || null,
|
|
1668
|
+
renderer: onChainUri ? renderer : null,
|
|
1669
|
+
image: planImageFile,
|
|
1670
|
+
},
|
|
1671
|
+
mint: { deferred: mintAmount === 0n, count: mintAmount > 0n ? 1 : 0, amountPerId: mintAmount.toString(), recipient: mintAmount > 0n ? deployer : null },
|
|
1672
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
1673
|
+
warnings: planWarnings.slice(),
|
|
1674
|
+
surfaces: null,
|
|
1675
|
+
dependencies: null,
|
|
1676
|
+
resume: null,
|
|
1677
|
+
},
|
|
1678
|
+
}));
|
|
1535
1679
|
return;
|
|
1536
1680
|
}
|
|
1537
1681
|
await confirmSend(`About to deploy an edition of "${name}" (${symbol}) — ${editionSize === 0n ? 'open' : editionSize.toString()} copies${flags.image ? ' with your image' : ' (generative demo content)'}; mint: ${mintAmount > 0n ? `${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
1538
1682
|
(transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
1539
1683
|
if (lane === 'send') {
|
|
1540
1684
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
1685
|
+
deployerAddr = account.address;
|
|
1541
1686
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(account.address);
|
|
1542
1687
|
info(`deterministic address: ${predicted}`);
|
|
1543
1688
|
info(describeDeployField('description', flags.description, onChainUri || !!flags['description-onchain']));
|
|
@@ -1568,6 +1713,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1568
1713
|
let r;
|
|
1569
1714
|
try {
|
|
1570
1715
|
const signer = await session.connect();
|
|
1716
|
+
deployerAddr = signer;
|
|
1571
1717
|
await stageOnChainImage(sessionStagingSender(session)); // sets bakedImage, read by buildForDeployer
|
|
1572
1718
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(signer);
|
|
1573
1719
|
info(contentNote);
|
|
@@ -1587,6 +1733,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1587
1733
|
// wallet lane without staging, or the cold lane: a single deploy tx.
|
|
1588
1734
|
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}`);
|
|
1589
1735
|
const result = await signTx(async (signer) => {
|
|
1736
|
+
deployerAddr = signer;
|
|
1590
1737
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
1591
1738
|
return prepareDeployOneOfOneEdition({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
1592
1739
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -1598,7 +1745,36 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1598
1745
|
blockNumber = result.blockNumber;
|
|
1599
1746
|
ok(`deployed ${clone}`);
|
|
1600
1747
|
}
|
|
1601
|
-
emit(jsonSafe({
|
|
1748
|
+
emit(jsonSafe({
|
|
1749
|
+
command: 'deploy', kind: '1of1-edition', copies: editionSize.toString(), address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
1750
|
+
plan: {
|
|
1751
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1752
|
+
family: '1of1-edition',
|
|
1753
|
+
lane,
|
|
1754
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging', 'deploy'] : null },
|
|
1755
|
+
roles: {
|
|
1756
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
1757
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
1758
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
1759
|
+
},
|
|
1760
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1761
|
+
custody: {
|
|
1762
|
+
onChainUri,
|
|
1763
|
+
imageOnChain: onchainImage,
|
|
1764
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1765
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
1766
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
1767
|
+
renderer: onChainUri ? renderer : null,
|
|
1768
|
+
image: planImageFile,
|
|
1769
|
+
},
|
|
1770
|
+
mint: { deferred: mintAmount === 0n, count: mintAmount > 0n ? 1 : 0, amountPerId: mintAmount.toString(), recipient: mintAmount > 0n ? deployerAddr ?? null : null },
|
|
1771
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
1772
|
+
warnings: planWarnings.slice(),
|
|
1773
|
+
surfaces: null,
|
|
1774
|
+
dependencies: null,
|
|
1775
|
+
resume: null,
|
|
1776
|
+
},
|
|
1777
|
+
}));
|
|
1602
1778
|
step('Index it — replay the event spine from chain');
|
|
1603
1779
|
const indexer = localIndexer();
|
|
1604
1780
|
const offChainTraits = !traitsOnchain && traits.length ? JSON.stringify(traits) : undefined;
|
|
@@ -1668,6 +1844,7 @@ export async function cmdDeploySeries(flags) {
|
|
|
1668
1844
|
return withJson(flags, async (emit) => cmdDeploySeriesBody(flags, emit));
|
|
1669
1845
|
}
|
|
1670
1846
|
export async function cmdDeploySeriesBody(flags, emit) {
|
|
1847
|
+
beginPlanWarnings();
|
|
1671
1848
|
refuseStrayFlags(flags, DEPLOY_SERIES_FLAGS, 'deploy-series');
|
|
1672
1849
|
warnSignWithoutFor(flags);
|
|
1673
1850
|
const name = flags.name ?? 'ABX Series';
|
|
@@ -2070,14 +2247,50 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2070
2247
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored.')}\n`);
|
|
2071
2248
|
// Without --salt, `address` would be a freshly-reserved value a real deploy will NOT land at —
|
|
2072
2249
|
// report null rather than a real-looking value a script could wrongly act on.
|
|
2073
|
-
emit(jsonSafe({
|
|
2250
|
+
emit(jsonSafe({
|
|
2251
|
+
command: 'deploy-series', dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol,
|
|
2252
|
+
plan: {
|
|
2253
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2254
|
+
family: 'series',
|
|
2255
|
+
lane,
|
|
2256
|
+
// A staged on-chain collection is many chunk-write txs, one per file — that per-token detail
|
|
2257
|
+
// is printed above (`previewImageStaging`), not re-derived here; `legs` just names the shape.
|
|
2258
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per token)', 'deploy'] : null },
|
|
2259
|
+
roles: {
|
|
2260
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
2261
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2262
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2263
|
+
},
|
|
2264
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2265
|
+
custody: {
|
|
2266
|
+
onChainUri,
|
|
2267
|
+
imageOnChain: onchainImage,
|
|
2268
|
+
backend: offchainImageOnchainJson || !onChainUri ? backendId : null,
|
|
2269
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
2270
|
+
contractUriBase: params.contractURIBase || null,
|
|
2271
|
+
renderer: onChainUri ? renderer : null,
|
|
2272
|
+
// Series stages ONE FILE PER TOKEN under --onchain-image — no single-file shape fits (see
|
|
2273
|
+
// this field's own doc comment) — so this stays null even when `imageOnChain` is true.
|
|
2274
|
+
image: null,
|
|
2275
|
+
},
|
|
2276
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployer : null },
|
|
2277
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2278
|
+
warnings: planWarnings.slice(),
|
|
2279
|
+
surfaces: null,
|
|
2280
|
+
dependencies: null,
|
|
2281
|
+
resume: null,
|
|
2282
|
+
},
|
|
2283
|
+
}));
|
|
2074
2284
|
return;
|
|
2075
2285
|
}
|
|
2076
2286
|
let clone;
|
|
2077
2287
|
let blockNumber;
|
|
2288
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`).
|
|
2289
|
+
let deployerAddr;
|
|
2078
2290
|
if (lane === 'send') {
|
|
2079
2291
|
// hot lane: the env key stages every token up front (deployer-independent), then deploys.
|
|
2080
2292
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
2293
|
+
deployerAddr = account.address;
|
|
2081
2294
|
if (onchainImage)
|
|
2082
2295
|
await buildFields(envStagingSender());
|
|
2083
2296
|
const { clone: predicted, params, salt } = await buildForDeployer(account.address);
|
|
@@ -2104,6 +2317,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2104
2317
|
});
|
|
2105
2318
|
try {
|
|
2106
2319
|
const signer = await session.connect();
|
|
2320
|
+
deployerAddr = signer;
|
|
2107
2321
|
if (onchainImage)
|
|
2108
2322
|
await buildFields(sessionStagingSender(session));
|
|
2109
2323
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
@@ -2129,6 +2343,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2129
2343
|
});
|
|
2130
2344
|
try {
|
|
2131
2345
|
const signer = await session.connect();
|
|
2346
|
+
deployerAddr = signer;
|
|
2132
2347
|
// Route Turbo uploads through the wallet (mutate the SAME opts.arweave the backend captured).
|
|
2133
2348
|
if (opts.arweave)
|
|
2134
2349
|
opts.arweave.remoteEth = { address: signer, signMessage: (m) => session.signMessage(m, 'Sign Arweave upload (paid from your Turbo credits)') };
|
|
@@ -2151,6 +2366,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2151
2366
|
? 'a wallet will become the owner; tokens resolve from chain — no URI base is baked in.'
|
|
2152
2367
|
: `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
2153
2368
|
const result = await signTx(async (signer) => {
|
|
2369
|
+
deployerAddr = signer;
|
|
2154
2370
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2155
2371
|
return prepareDeploySeries({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
2156
2372
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -2168,7 +2384,36 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2168
2384
|
info(`permanent: size ${count} · royalty ${royaltyBps / 100}%${flags['royalty-bps'] === undefined ? ' ⚠ default' : ''} → the deploying wallet` +
|
|
2169
2385
|
` · cap ${maxRoyaltyBps / 100}%${burnable ? ' · burnable' : ''}` +
|
|
2170
2386
|
dim(' (rate is changeable with `abx set-royalty`; the CAP only ever goes down)'));
|
|
2171
|
-
emit(jsonSafe({
|
|
2387
|
+
emit(jsonSafe({
|
|
2388
|
+
command: 'deploy-series', address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
2389
|
+
plan: {
|
|
2390
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2391
|
+
family: 'series',
|
|
2392
|
+
lane,
|
|
2393
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per token)', 'deploy'] : null },
|
|
2394
|
+
roles: {
|
|
2395
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
2396
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2397
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2398
|
+
},
|
|
2399
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2400
|
+
custody: {
|
|
2401
|
+
onChainUri,
|
|
2402
|
+
imageOnChain: onchainImage,
|
|
2403
|
+
backend: offchainImageOnchainJson || !onChainUri ? backendId : null,
|
|
2404
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
2405
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
2406
|
+
renderer: onChainUri ? renderer : null,
|
|
2407
|
+
image: null, // per-token staging — see the dry-run emit's identical comment
|
|
2408
|
+
},
|
|
2409
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployerAddr ?? null : null },
|
|
2410
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2411
|
+
warnings: planWarnings.slice(),
|
|
2412
|
+
surfaces: null,
|
|
2413
|
+
dependencies: null,
|
|
2414
|
+
resume: null,
|
|
2415
|
+
},
|
|
2416
|
+
}));
|
|
2172
2417
|
step('Index it — replay the event spine from chain');
|
|
2173
2418
|
const indexer = localIndexer();
|
|
2174
2419
|
const baseReg = {
|
|
@@ -2244,6 +2489,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2244
2489
|
// (`DEPLOY_SERIES_EDITION_FLAGS` is declared further down, beside `DEPLOY_EDITION_FLAGS` — see that
|
|
2245
2490
|
// const's own note on why.)
|
|
2246
2491
|
export async function cmdDeployEditionImageBody(flags, emit) {
|
|
2492
|
+
beginPlanWarnings();
|
|
2247
2493
|
refuseStrayFlags(flags, DEPLOY_SERIES_EDITION_FLAGS, 'deploy-series');
|
|
2248
2494
|
warnSignWithoutFor(flags);
|
|
2249
2495
|
const editionSize = parseCopies(flags.copies);
|
|
@@ -2563,6 +2809,8 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2563
2809
|
};
|
|
2564
2810
|
let clone;
|
|
2565
2811
|
let blockNumber;
|
|
2812
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`).
|
|
2813
|
+
let deployerAddr;
|
|
2566
2814
|
if (dryRun) {
|
|
2567
2815
|
let deployer;
|
|
2568
2816
|
if (flags.for)
|
|
@@ -2602,11 +2850,48 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2602
2850
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
|
|
2603
2851
|
}
|
|
2604
2852
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
|
|
2605
|
-
emit(jsonSafe({
|
|
2853
|
+
emit(jsonSafe({
|
|
2854
|
+
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,
|
|
2855
|
+
plan: {
|
|
2856
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2857
|
+
family: 'series-edition',
|
|
2858
|
+
lane,
|
|
2859
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per id)', 'deploy'] : null },
|
|
2860
|
+
roles: {
|
|
2861
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
2862
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2863
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2864
|
+
},
|
|
2865
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2866
|
+
custody: {
|
|
2867
|
+
onChainUri,
|
|
2868
|
+
imageOnChain: onchainImage,
|
|
2869
|
+
backend: !onchainImage && (offchainImageOnchainJson || !onChainUri) ? backendId : null,
|
|
2870
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
2871
|
+
contractUriBase: params.contractURIBase || null,
|
|
2872
|
+
renderer: onChainUri ? renderer : null,
|
|
2873
|
+
// Series-edition stages ONE FILE PER id under --onchain-image — no single-file shape fits
|
|
2874
|
+
// (see this field's own doc comment) — so this stays null even when `imageOnChain` is true.
|
|
2875
|
+
image: null,
|
|
2876
|
+
},
|
|
2877
|
+
mint: {
|
|
2878
|
+
deferred: effectiveMintCount === 0,
|
|
2879
|
+
count: effectiveMintCount,
|
|
2880
|
+
amountPerId: mintAmount.toString(),
|
|
2881
|
+
recipient: effectiveMintCount > 0 ? deployer : null,
|
|
2882
|
+
},
|
|
2883
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2884
|
+
warnings: planWarnings.slice(),
|
|
2885
|
+
surfaces: null,
|
|
2886
|
+
dependencies: null,
|
|
2887
|
+
resume: null,
|
|
2888
|
+
},
|
|
2889
|
+
}));
|
|
2606
2890
|
return;
|
|
2607
2891
|
}
|
|
2608
2892
|
if (lane === 'send') {
|
|
2609
2893
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
2894
|
+
deployerAddr = account.address;
|
|
2610
2895
|
// hot lane: the env key stages every id up front (deployer-independent), then deploys.
|
|
2611
2896
|
if (onchainImage)
|
|
2612
2897
|
await buildFields(envStagingSender());
|
|
@@ -2635,6 +2920,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2635
2920
|
let r;
|
|
2636
2921
|
try {
|
|
2637
2922
|
const signer = await session.connect();
|
|
2923
|
+
deployerAddr = signer;
|
|
2638
2924
|
await buildFields(sessionStagingSender(session));
|
|
2639
2925
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2640
2926
|
info(`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${signer} at deploy` : 'deferred'}`);
|
|
@@ -2652,6 +2938,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2652
2938
|
else {
|
|
2653
2939
|
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}`);
|
|
2654
2940
|
const result = await signTx(async (signer) => {
|
|
2941
|
+
deployerAddr = signer;
|
|
2655
2942
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2656
2943
|
return prepareDeployEditionImage({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
2657
2944
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -2663,7 +2950,41 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2663
2950
|
blockNumber = result.blockNumber;
|
|
2664
2951
|
ok(`deployed ${clone}`);
|
|
2665
2952
|
}
|
|
2666
|
-
emit(jsonSafe({
|
|
2953
|
+
emit(jsonSafe({
|
|
2954
|
+
command: 'deploy-series', kind: 'edition', copies: editionSize.toString(), ids: count, address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
2955
|
+
plan: {
|
|
2956
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2957
|
+
family: 'series-edition',
|
|
2958
|
+
lane,
|
|
2959
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per id)', 'deploy'] : null },
|
|
2960
|
+
roles: {
|
|
2961
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
2962
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2963
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2964
|
+
},
|
|
2965
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2966
|
+
custody: {
|
|
2967
|
+
onChainUri,
|
|
2968
|
+
imageOnChain: onchainImage,
|
|
2969
|
+
backend: !onchainImage && (offchainImageOnchainJson || !onChainUri) ? backendId : null,
|
|
2970
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
2971
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
2972
|
+
renderer: onChainUri ? renderer : null,
|
|
2973
|
+
image: null, // per-id staging — see the dry-run emit's identical comment
|
|
2974
|
+
},
|
|
2975
|
+
mint: {
|
|
2976
|
+
deferred: effectiveMintCount === 0,
|
|
2977
|
+
count: effectiveMintCount,
|
|
2978
|
+
amountPerId: mintAmount.toString(),
|
|
2979
|
+
recipient: effectiveMintCount > 0 ? deployerAddr ?? null : null,
|
|
2980
|
+
},
|
|
2981
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2982
|
+
warnings: planWarnings.slice(),
|
|
2983
|
+
surfaces: null,
|
|
2984
|
+
dependencies: null,
|
|
2985
|
+
resume: null,
|
|
2986
|
+
},
|
|
2987
|
+
}));
|
|
2667
2988
|
step('Index it — replay the event spine from chain');
|
|
2668
2989
|
const indexer = localIndexer();
|
|
2669
2990
|
const offChainTraits = !seriesTraitsOnchain && offChainTokenTraits ? offChainTokenTraits : undefined;
|
|
@@ -2919,6 +3240,10 @@ export const DEPLOY_CODE_FLAGS = new Set([
|
|
|
2919
3240
|
'dry-run', 'confirm', 'yes',
|
|
2920
3241
|
'json', // the deployed address as data, narration to stderr
|
|
2921
3242
|
'resume', // finish an existing contract whose setup transaction never landed (no deploy)
|
|
3243
|
+
// Meaningful ONLY on `--resume` against an EditionCode target (per-id copies for the premint
|
|
3244
|
+
// shortfall) — kept in the allowlist (not stray) so a misuse elsewhere gets this function's own
|
|
3245
|
+
// pointed refusal instead of the generic "unrecognized flag". See the --resume block.
|
|
3246
|
+
'mint-amount',
|
|
2922
3247
|
]);
|
|
2923
3248
|
// Storage + signing + preview flags shared by every deploy path (mirrors storageOverrides() + the lanes).
|
|
2924
3249
|
export const SHARED_DEPLOY_FLAGS = [
|
|
@@ -2963,6 +3288,76 @@ export const DEPLOY_SERIES_FLAGS = new Set([
|
|
|
2963
3288
|
// module has finished evaluating.
|
|
2964
3289
|
export const DEPLOY_EDITION_FLAGS = new Set([...DEPLOY_FLAGS, 'copies', 'mint-amount', 'minter', 'primary-payee', 'unpaused']);
|
|
2965
3290
|
export const DEPLOY_SERIES_EDITION_FLAGS = new Set([...DEPLOY_SERIES_FLAGS, 'copies', 'mint-amount']);
|
|
3291
|
+
export const chunkSetupLeg = (index, hex, data) => {
|
|
3292
|
+
const bytes = (hex.length - 2) / 2;
|
|
3293
|
+
return { calls: [data], gas: estimateChunkGasForBytes(bytes), label: `script chunk [${index}] (${bytes} bytes)`, kind: 'chunk', chunkBytes: [bytes] };
|
|
3294
|
+
};
|
|
3295
|
+
export const schemaSetupLeg = (key, data) => ({
|
|
3296
|
+
calls: [data], gas: SETUP_LEG_GAS.schema, label: `param schema '${key}'`, kind: 'schema', schemaKey: key,
|
|
3297
|
+
});
|
|
3298
|
+
export const depsSetupLeg = (calls) => ({
|
|
3299
|
+
calls, gas: calls.length * SETUP_LEG_GAS.dependency, label: `${calls.length} dependency declaration(s)`, kind: 'deps',
|
|
3300
|
+
});
|
|
3301
|
+
export const uriSetupLeg = (calls) => ({
|
|
3302
|
+
calls, gas: calls.length * SETUP_LEG_GAS.onchainUri, label: 'on-chain URI wiring', kind: 'uri',
|
|
3303
|
+
});
|
|
3304
|
+
export const mintSetupLeg = (data) => ({ calls: [data], gas: SETUP_LEG_GAS.mint, label: 'reserve mint', kind: 'mint' });
|
|
3305
|
+
/** `SetupLegsCore` (resume.ts's id-agnostic four leg groups, shared by both 721 and EditionCode) →
|
|
3306
|
+
* the gas-batching shape above — schemas individually (resume diffs them per-key too), deps and
|
|
3307
|
+
* on-chain-URI as one atomic leg apiece (resume diffs them as one group too — see resume.ts's
|
|
3308
|
+
* `planCoreLegs`). Mints are NOT included here: their shape differs by lane (721 whole-total vs
|
|
3309
|
+
* EditionCode per-id vs a `--resume` shortfall) — every caller builds its own mint legs. */
|
|
3310
|
+
export function coreSetupLegs(legs) {
|
|
3311
|
+
return {
|
|
3312
|
+
chunks: legs.chunks.map((c) => chunkSetupLeg(c.index, c.hex, c.data)),
|
|
3313
|
+
config: [
|
|
3314
|
+
...legs.schemas.map((s) => schemaSetupLeg(s.key, s.data)),
|
|
3315
|
+
...(legs.deps.calls.length ? [depsSetupLeg(legs.deps.calls)] : []),
|
|
3316
|
+
...(legs.uri.calls.length ? [uriSetupLeg(legs.uri.calls)] : []),
|
|
3317
|
+
],
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Split three ordered leg groups into gas-bounded batches — chunks, then config, then mints, NEVER
|
|
3322
|
+
* mixed (see the module note above). Refuses BEFORE building anything if a single leg's own gas
|
|
3323
|
+
* estimate alone exceeds the binding `eth_estimateGas` ceiling: batching only helps when the problem
|
|
3324
|
+
* is too MANY legs, not one leg too big, and that case must never reach a raw "gas limit too high".
|
|
3325
|
+
* Deterministic in the legs' `.gas` values alone (never their `.calls`), so calling this once early
|
|
3326
|
+
* (to size `approvals`, before an owner is even known) and again later with the real calldata
|
|
3327
|
+
* produces the IDENTICAL batch shape both times — no drift between what's previewed and what's sent.
|
|
3328
|
+
*/
|
|
3329
|
+
export function planCodeSetupBatches(groups) {
|
|
3330
|
+
const all = [...groups.chunks, ...groups.config, ...groups.mints];
|
|
3331
|
+
if (all.length === 0)
|
|
3332
|
+
return [];
|
|
3333
|
+
const oversized = all.find((leg) => leg.gas > MEASURED_ESTIMATE_GAS_ALLOWANCE);
|
|
3334
|
+
if (oversized) {
|
|
3335
|
+
throw new Error(`setup leg — ${oversized.label} — needs ~${oversized.gas.toLocaleString()} gas on its own, which exceeds the ` +
|
|
3336
|
+
`~${MEASURED_ESTIMATE_GAS_ALLOWANCE.toLocaleString()} eth_estimateGas ceiling every transaction is bound by ` +
|
|
3337
|
+
`(that RPC allowance is the binding constraint — not the higher block gas limit). No amount of batching can ` +
|
|
3338
|
+
`make ONE leg fit a transaction by itself — shrink it before deploying (a smaller chunk size, fewer items in ` +
|
|
3339
|
+
`this leg), then re-run.`);
|
|
3340
|
+
}
|
|
3341
|
+
const total = all.reduce((g, l) => g + l.gas, 0);
|
|
3342
|
+
if (total <= DEFAULT_TX_GAS_BUDGET)
|
|
3343
|
+
return [all]; // fits one tx — unchanged common-case behavior
|
|
3344
|
+
return [...packCallsByGas(groups.chunks), ...packCallsByGas(groups.config), ...packCallsByGas(groups.mints)].filter((b) => b.length > 0);
|
|
3345
|
+
}
|
|
3346
|
+
/** Turn ordered setup-leg batches into the actual `prepareCodeSetup` transactions — rebuilding each
|
|
3347
|
+
* batch's chunk/schema/dependency/on-chain-URI labeling from its OWN legs, since a batch born from
|
|
3348
|
+
* the split may carry only part of a group (see {@link planCodeSetupBatches}). */
|
|
3349
|
+
export function codeSetupTxsFromBatches(batches, args) {
|
|
3350
|
+
return batches.map((batch) => prepareCodeSetup({
|
|
3351
|
+
contract: args.contract,
|
|
3352
|
+
chainId: args.chainId,
|
|
3353
|
+
calls: batch.flatMap((l) => l.calls),
|
|
3354
|
+
chunkCount: batch.filter((l) => l.kind === 'chunk').length,
|
|
3355
|
+
chunkBytes: batch.filter((l) => l.kind === 'chunk').flatMap((l) => l.chunkBytes ?? []),
|
|
3356
|
+
schemaKeys: batch.filter((l) => l.kind === 'schema').map((l) => l.schemaKey),
|
|
3357
|
+
deps: batch.some((l) => l.kind === 'deps') ? args.deps : [],
|
|
3358
|
+
onchainUri: batch.some((l) => l.kind === 'uri'),
|
|
3359
|
+
}));
|
|
3360
|
+
}
|
|
2966
3361
|
export async function cmdDeployCode(flags) {
|
|
2967
3362
|
// `--resume` targets an EXISTING contract — its standard (721 or edition) was fixed at THAT
|
|
2968
3363
|
// contract's own deploy, so `--copies` (a creation-time choice) is nonsensical alongside it.
|
|
@@ -2981,6 +3376,7 @@ export async function cmdDeployCode(flags) {
|
|
|
2981
3376
|
return withJson(flags, async (emit) => cmdDeployCodeBody(flags, emit));
|
|
2982
3377
|
}
|
|
2983
3378
|
export async function cmdDeployCodeBody(flags, emit) {
|
|
3379
|
+
beginPlanWarnings();
|
|
2984
3380
|
const usage = 'abx deploy-code (--script <file> | --code-dir <dir>) --name "Title" --symbol SYM ' +
|
|
2985
3381
|
'(--public-base-url https://your.resolver.domain | --onchain-uri) ' +
|
|
2986
3382
|
'[--description "<s>"] [--external-url <url>] [--image-base <url> | --image-renderer 0x..] [--attributes-renderer 0x..] ' +
|
|
@@ -3004,6 +3400,16 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3004
3400
|
// Shared path so the "did you mean --max / price is post-deploy" hints fire here too.
|
|
3005
3401
|
refuseStrayFlags(flags, DEPLOY_CODE_FLAGS, 'deploy-code');
|
|
3006
3402
|
warnSignWithoutFor(flags);
|
|
3403
|
+
// --mint-amount only means anything on a --resume against an EXISTING EditionCode target (a per-id
|
|
3404
|
+
// premint shortfall needs an amount; a fresh 721 deploy has no such field, and a 721 resume's mint
|
|
3405
|
+
// leg is a single whole-contract shortfall with no amount either). Checked HERE, before the dry-run
|
|
3406
|
+
// preview branch below returns early for a fresh deploy — a --resume against an actual EditionCode
|
|
3407
|
+
// (where the flag DOES apply) gets the more specific 721-vs-edition guard further down instead.
|
|
3408
|
+
if (flags['mint-amount'] !== undefined && flags.resume === undefined) {
|
|
3409
|
+
throw new Error('--mint-amount only applies to `abx deploy-code --resume <address>` against an EXISTING EditionCode contract — ' +
|
|
3410
|
+
'a fresh 721 deploy (this command, without --copies) has no per-id mint amount. For a NEW edition deploy, use ' +
|
|
3411
|
+
'`abx deploy-code --copies <n|open> --mint-amount <n>`.');
|
|
3412
|
+
}
|
|
3007
3413
|
const dryRun = isDryRun(flags);
|
|
3008
3414
|
const name = flags.name ?? 'ABX Code';
|
|
3009
3415
|
const symbol = flags.symbol ?? 'ABXC';
|
|
@@ -3393,7 +3799,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3393
3799
|
if (flags['image-renderer'] || flags['attributes-renderer']) {
|
|
3394
3800
|
warn(`${bold('renderer check — yours to verify')} (the CLI confirms code at the address, NOT that render() behaves): an IAbxFieldRenderer must ` +
|
|
3395
3801
|
`${bold('NEVER revert')} for any token/param state incl. the collection surface (tokenId = type(uint256).max) — a revert bricks the WHOLE tokenURI (no try/catch) — ` +
|
|
3396
|
-
`and must return the right content-type (image → ${bold('image/svg+xml')} · attributes → a JSON array). Review the render function + forge-test it before shipping. Invariants: reference/code
|
|
3802
|
+
`and must return the right content-type (image → ${bold('image/svg+xml')} · attributes → a JSON array). Review the render function + forge-test it before shipping. Invariants: reference/code.md.`);
|
|
3397
3803
|
}
|
|
3398
3804
|
// ── Surfaces disposition — resolve EVERY product dimension explicitly, up front ──────────────
|
|
3399
3805
|
// A code project has four surfaces that each land somewhere (or nowhere). A real session shipped
|
|
@@ -3458,6 +3864,10 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3458
3864
|
// PostParams it reads (unlike the JS lane's static paramHints), so nudge — the palette-miss.
|
|
3459
3865
|
? `⚠ none declared — if your Solidity renderer reads ANY collector PostParam, declare EACH with ${bold('--schema <key>:Type:Auth')} (matching the key your renderer reads — e.g. ${bold('palette:HexColor:TokenOwner')}) or that input is FIXED at the renderer's default forever and collectors can't set it (the CLI can't detect the key — a Solidity renderer is opaque, so this is on you to declare)`
|
|
3460
3866
|
: 'none declared';
|
|
3867
|
+
// Hoisted here (rather than beside its one printed use, in the Surfaces block below) so BOTH the
|
|
3868
|
+
// dry-run's Surfaces block and the plan object's `surfaces.postParams.ok` (populated at every one
|
|
3869
|
+
// of this lane's emit sites, dry-run/sent/resume alike) read the exact same verdict.
|
|
3870
|
+
const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
|
|
3461
3871
|
// TOKENURI PUBLIC-READ GAS — a large --onchain-uri document assembles on-chain per call; an
|
|
3462
3872
|
// unauthenticated public read (Etherscan "Read Contract" with no wallet connected) can hit a client
|
|
3463
3873
|
// gas cap and appear to REVERT. Expected, not a bug — a real session mis-diagnosed it as indexing lag.
|
|
@@ -3493,7 +3903,60 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3493
3903
|
// one whenever a dependency leg rode the multicall alongside a renderer-only (`foldIntoInit`) fold.
|
|
3494
3904
|
const depLegs = deps.length + (depRegistry ? 1 : 0);
|
|
3495
3905
|
const setupLen = scriptChunks.length + schemas.length + depLegs + (onchainUriLegs?.length ?? 0) + setupMintCount;
|
|
3496
|
-
|
|
3906
|
+
// The setup legs, GROUPED. The normal deploy flattens them; `--resume` diffs them against chain
|
|
3907
|
+
// state and sends only what is missing (see resume.ts). One builder for both, so a resume can never
|
|
3908
|
+
// drift from what a fresh deploy would have written — a second implementation of this sequence is
|
|
3909
|
+
// the failure mode a repair verb most easily introduces. Moved up here (it used to sit right before
|
|
3910
|
+
// `preparedFor`, far below) so `approvals`/`planSetupLegs`/the dry-run's tx-count line can all size
|
|
3911
|
+
// themselves off the SAME gas-bounded batch plan `preparedFor` actually sends — see
|
|
3912
|
+
// `planCodeSetupBatches`'s own doc for why calling it here with a placeholder owner and again below
|
|
3913
|
+
// with the real one can never disagree (it's a pure function of each leg's `.gas`, never its
|
|
3914
|
+
// `.calls`, and owner only ever appears in a mint call's calldata, never its gas).
|
|
3915
|
+
const setupLegGroups = (owner) => ({
|
|
3916
|
+
chunks: scriptChunks.map((chunk, i) => ({
|
|
3917
|
+
index: i,
|
|
3918
|
+
hex: chunk,
|
|
3919
|
+
data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] }),
|
|
3920
|
+
})),
|
|
3921
|
+
schemas: schemas.map(({ key, paramType, auth, authAddress, lockAfter, min, max, selectOptions }) => ({
|
|
3922
|
+
key,
|
|
3923
|
+
data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth, authAddress, lockAfter, min, max, selectOptions] }),
|
|
3924
|
+
})),
|
|
3925
|
+
deps: { count: deps.length, registry: depRegistry ?? null, calls: dependencySetupCalls(deps, depRegistry) },
|
|
3926
|
+
// program lane only: animation_url field · the URI renderers (before the mints)
|
|
3927
|
+
uri: { calls: onchainUriLegs ?? [], animationField: onchainUriLegs?.length ? F.animationUrl : null },
|
|
3928
|
+
// The INTENDED TOTAL, not a count to add — a resume mints the shortfall. `mintCount` covers both
|
|
3929
|
+
// lanes: folded-into-init (renderer-only) and setup-carried, since either way it is what the
|
|
3930
|
+
// creator asked for.
|
|
3931
|
+
mints: { intendedTotal: mintCount, data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'mint', args: [owner] }) },
|
|
3932
|
+
});
|
|
3933
|
+
// Gas-bounded setup-transaction batches for a FRESH deploy — chunks, then config, then mints, never
|
|
3934
|
+
// mixed (see `planCodeSetupBatches`). `owner` is irrelevant to the batch SHAPE (only a mint call's
|
|
3935
|
+
// calldata embeds it, never its gas), so `zeroAddress` here is a placeholder that's never sent —
|
|
3936
|
+
// it exists purely so `approvals`/`planSetupLegs` (and the dry-run prose) can report the REAL
|
|
3937
|
+
// transaction count before a deployer is even known, with zero risk of drifting from what
|
|
3938
|
+
// `preparedFor` below actually builds and sends.
|
|
3939
|
+
const freshDeployBatches = planCodeSetupBatches({
|
|
3940
|
+
...coreSetupLegs(setupLegGroups(zeroAddress)),
|
|
3941
|
+
mints: Array.from({ length: setupMintCount }, () => mintSetupLeg('0x')),
|
|
3942
|
+
});
|
|
3943
|
+
const approvals = 1 + freshDeployBatches.length; // deploy tx + however many gas-bounded setup batches
|
|
3944
|
+
// Ordered legs riding the setup (if any), for the plan object's `transactions.legs` — GROUP names,
|
|
3945
|
+
// not a literal per-transaction count (a group can span more than one batch — see deploy-plan.ts's
|
|
3946
|
+
// own doc comment on `legs`). The SAME booleans `setupLen`/`setupBits` (the dry-run's own
|
|
3947
|
+
// `transactions:` line, below) already use, so this can't drift from the tx(es) the wallet actually
|
|
3948
|
+
// signs. Always ends with `'deploy'` when non-null — the setup batch(es) (if any) ride AHEAD of the
|
|
3949
|
+
// deploy tx, never instead of it.
|
|
3950
|
+
const planSetupLegs = setupLen > 0
|
|
3951
|
+
? [
|
|
3952
|
+
scriptChunks.length ? 'chunks' : null,
|
|
3953
|
+
schemas.length ? 'param-schemas' : null,
|
|
3954
|
+
depLegs ? 'dependencies' : null,
|
|
3955
|
+
onchainUriLegs ? 'onchain-uri' : null,
|
|
3956
|
+
setupMintCount ? 'mints' : null,
|
|
3957
|
+
'deploy',
|
|
3958
|
+
].filter((leg) => leg !== null)
|
|
3959
|
+
: null;
|
|
3497
3960
|
// opt-in --confirm: one y/N before ANY upload or send (no-op without --confirm; never blocks scripts).
|
|
3498
3961
|
if (!dryRun) {
|
|
3499
3962
|
const renderHome = process.env.ABX_STORAGE_BACKEND || 'fs'; // empty string counts as unset (=fs)
|
|
@@ -3609,13 +4072,20 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3609
4072
|
onchainUriLegs ? 'on-chain-uri' : '',
|
|
3610
4073
|
setupMintCount ? 'mints' : '',
|
|
3611
4074
|
].filter(Boolean).join('/');
|
|
3612
|
-
info(`transactions: ${
|
|
3613
|
-
(
|
|
4075
|
+
info(`transactions: ${approvals} (deploy${foldIntoInit ? ' — on-chain-uri wiring + mint fold into it' : ''}` +
|
|
4076
|
+
(freshDeployBatches.length > 0
|
|
4077
|
+
? ` + ${freshDeployBatches.length} setup ${freshDeployBatches.length === 1 ? 'transaction' : 'transactions (gas-bounded)'}: ${setupBits}`
|
|
4078
|
+
: '') +
|
|
4079
|
+
')' +
|
|
4080
|
+
(foldIntoInit && freshDeployBatches.length === 0 ? ` ${dim('— single transaction, fully self-contained')}` : ''));
|
|
3614
4081
|
info(`approvals ${approvals} wallet approval(s)`); // TX signatures only; the same number the tx-count line above implies
|
|
3615
4082
|
// Best-effort cost guidance (owner: a bonus in dry-run, never a blocker — silently skip if the
|
|
3616
4083
|
// RPC can't price gas; see cli-ux-decisions). On-chain byte storage dominates a template drop,
|
|
3617
4084
|
// so apply the skill's own ~200 gas/byte heuristic to the stored bytes + coarse per-op costs,
|
|
3618
4085
|
// priced at the live gas price. Clearly hedged — it's an order-of-magnitude figure, not a quote.
|
|
4086
|
+
// `planEstimate` carries the SAME figures into the plan object below — `null` (not computed)
|
|
4087
|
+
// when the RPC can't price gas, exactly mirroring the human line's own silent skip.
|
|
4088
|
+
let planEstimate = null;
|
|
3619
4089
|
try {
|
|
3620
4090
|
const chunkBytes = scriptChunks.reduce((n, c) => n + (c.length - 2) / 2, 0);
|
|
3621
4091
|
const fieldBytes = contractFields.reduce((n, f) => n + (f.value.length - 2) / 2, 0);
|
|
@@ -3632,6 +4102,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3632
4102
|
const ethStr = eth >= 0.00001 ? eth.toFixed(5) : '<0.00001';
|
|
3633
4103
|
info(`est. on-chain cost: ${dim('~')}${ethStr} ETH ` +
|
|
3634
4104
|
dim(`(very rough — ~${Number(roughGas).toLocaleString()} gas @ ${(Number(gasPrice) / 1e9).toFixed(2)} gwei; excludes off-chain storage${codeDir ? ', the dominant cost for a directory drop' : ''})`));
|
|
4105
|
+
planEstimate = { ethApprox: ethStr, gasApprox: roughGas.toString() };
|
|
3635
4106
|
}
|
|
3636
4107
|
catch { /* gas pricing unavailable — cost guidance is a bonus, skip silently */ }
|
|
3637
4108
|
// ── Surfaces — every marketplace-facing dimension, resolved NOW (none is backfillable) ──────
|
|
@@ -3675,8 +4146,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3675
4146
|
info(` ${dim('render storage home:')} ${bold(renderHome)} ${dim('— a public home the runner uploads to ✓')}`);
|
|
3676
4147
|
}
|
|
3677
4148
|
}
|
|
3678
|
-
(traitsBroken ? warn : info)(`traits: ${traitsDisposition}`);
|
|
3679
|
-
const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
|
|
4149
|
+
(traitsBroken ? warn : info)(`traits: ${traitsDisposition}`); // `paramsNudge` is hoisted above (with `paramsDisposition`) — shared with the plan object
|
|
3680
4150
|
((undeclaredParams.length || paramsNudge ? warn : info))(`postparams: ${paramsDisposition}`);
|
|
3681
4151
|
if (anySurfaceBroken) {
|
|
3682
4152
|
const remedies = [
|
|
@@ -3769,7 +4239,46 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3769
4239
|
// `address` is null without --salt pinning it or in the wallet lane with no --for (the
|
|
3770
4240
|
// signer decides the salt, so no address exists yet) — reporting null is the honest answer;
|
|
3771
4241
|
// inventing one would be a wrong reservation.
|
|
3772
|
-
emit(jsonSafe({
|
|
4242
|
+
emit(jsonSafe({
|
|
4243
|
+
command: 'deploy-code', dryRun: true, sent: false, address: shownAddr, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt: salt ?? null, saltPinned: !!explicitSalt, name, symbol, onChainUri,
|
|
4244
|
+
plan: {
|
|
4245
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
4246
|
+
family: 'code',
|
|
4247
|
+
// `lane` (the hoisted const) isn't declared until AFTER this early-return block — reading it
|
|
4248
|
+
// here would hit its temporal dead zone. `laneFromFlags` is pure (flags only), so call it
|
|
4249
|
+
// directly; the sent-side emit, below `const lane = …`, uses the hoisted binding instead.
|
|
4250
|
+
lane: laneFromFlags(flags),
|
|
4251
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
4252
|
+
roles: {
|
|
4253
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
4254
|
+
primaryPayee: flags['primary-payee'] ?? null,
|
|
4255
|
+
minter: flags.minter ?? null,
|
|
4256
|
+
},
|
|
4257
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
4258
|
+
custody: {
|
|
4259
|
+
onChainUri,
|
|
4260
|
+
// The code lane's image is one of several independently-disposed surfaces, not a single
|
|
4261
|
+
// boolean — see `surfaces.thumbnail` below.
|
|
4262
|
+
imageOnChain: null,
|
|
4263
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
4264
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
4265
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
4266
|
+
renderer: onChainUri ? metadataRenderer : null,
|
|
4267
|
+
// This lane has no single `--image` file — see `surfaces.thumbnail` below instead.
|
|
4268
|
+
image: null,
|
|
4269
|
+
},
|
|
4270
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployer : null },
|
|
4271
|
+
estimate: planEstimate ?? { ethApprox: null, gasApprox: null },
|
|
4272
|
+
warnings: planWarnings.slice(),
|
|
4273
|
+
surfaces: {
|
|
4274
|
+
thumbnail: { ok: !imageOrphaned, detail: stripAnsiForPlan(imageDisposition) },
|
|
4275
|
+
traits: { ok: !traitsBroken, detail: stripAnsiForPlan(traitsDisposition) },
|
|
4276
|
+
postParams: { ok: !(undeclaredParams.length > 0 || paramsNudge), detail: stripAnsiForPlan(paramsDisposition) },
|
|
4277
|
+
},
|
|
4278
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
4279
|
+
resume: null,
|
|
4280
|
+
},
|
|
4281
|
+
}));
|
|
3773
4282
|
return;
|
|
3774
4283
|
}
|
|
3775
4284
|
// Real deploy: perform the deferred directory upload now (past the confirm gate).
|
|
@@ -3820,47 +4329,21 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3820
4329
|
tokenFields: [],
|
|
3821
4330
|
contractFields,
|
|
3822
4331
|
});
|
|
3823
|
-
//
|
|
3824
|
-
//
|
|
3825
|
-
//
|
|
3826
|
-
//
|
|
3827
|
-
const setupLegGroups = (owner) => ({
|
|
3828
|
-
chunks: scriptChunks.map((chunk, i) => ({
|
|
3829
|
-
index: i,
|
|
3830
|
-
hex: chunk,
|
|
3831
|
-
data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] }),
|
|
3832
|
-
})),
|
|
3833
|
-
schemas: schemas.map(({ key, paramType, auth, authAddress, lockAfter, min, max, selectOptions }) => ({
|
|
3834
|
-
key,
|
|
3835
|
-
data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth, authAddress, lockAfter, min, max, selectOptions] }),
|
|
3836
|
-
})),
|
|
3837
|
-
deps: { count: deps.length, registry: depRegistry ?? null, calls: dependencySetupCalls(deps, depRegistry) },
|
|
3838
|
-
// program lane only: animation_url field · the URI renderers (before the mints)
|
|
3839
|
-
uri: { calls: onchainUriLegs ?? [], animationField: onchainUriLegs?.length ? F.animationUrl : null },
|
|
3840
|
-
// The INTENDED TOTAL, not a count to add — a resume mints the shortfall. `mintCount` covers both
|
|
3841
|
-
// lanes: folded-into-init (renderer-only) and setup-carried, since either way it is what the
|
|
3842
|
-
// creator asked for.
|
|
3843
|
-
mints: { intendedTotal: mintCount, data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'mint', args: [owner] }) },
|
|
3844
|
-
});
|
|
3845
|
-
const setupCalls = (owner) => {
|
|
3846
|
-
const g = setupLegGroups(owner);
|
|
3847
|
-
return [
|
|
3848
|
-
...g.chunks.map((c) => c.data),
|
|
3849
|
-
...g.schemas.map((x) => x.data),
|
|
3850
|
-
...g.deps.calls,
|
|
3851
|
-
...g.uri.calls,
|
|
3852
|
-
// folded mints ride init, not here — so this uses setupMintCount, not the intended total.
|
|
3853
|
-
...Array.from({ length: setupMintCount }, () => g.mints.data),
|
|
3854
|
-
];
|
|
3855
|
-
};
|
|
4332
|
+
// `setupLegGroups` (the shared, resume-compatible builder) and the fresh-deploy batch shape
|
|
4333
|
+
// (`freshDeployBatches`) are hoisted above — see the comment there for why. Build the REAL
|
|
4334
|
+
// transactions for the actual signer from the SAME batch shape (identical `.gas` sequence ⇒
|
|
4335
|
+
// identical split — see `planCodeSetupBatches`'s doc), this time with real mint calldata.
|
|
3856
4336
|
const cid = resolveChain(CHAIN).id;
|
|
3857
4337
|
const preparedFor = async (owner) => {
|
|
3858
4338
|
const salt = parseSaltFlag(flags.salt) ?? saltFor(owner);
|
|
3859
4339
|
const clone = (await publicClient.readContract({ address: factory, abi: seriesCodeFactoryAbi, functionName: 'predictDeterministicAddress', args: [salt] }));
|
|
3860
|
-
const
|
|
3861
|
-
const
|
|
3862
|
-
|
|
3863
|
-
|
|
4340
|
+
const g = setupLegGroups(owner);
|
|
4341
|
+
const batches = planCodeSetupBatches({
|
|
4342
|
+
...coreSetupLegs(g),
|
|
4343
|
+
mints: Array.from({ length: setupMintCount }, () => mintSetupLeg(g.mints.data)),
|
|
4344
|
+
});
|
|
4345
|
+
const setupTxs = codeSetupTxsFromBatches(batches, { contract: clone, chainId: cid, deps: deps.map((d) => d.display) });
|
|
4346
|
+
const txs = [prepareDeploySeriesCode({ factory, params: initParamsFor(owner), salt, chainId: cid, clone }), ...setupTxs];
|
|
3864
4347
|
return { clone, txs };
|
|
3865
4348
|
};
|
|
3866
4349
|
// ── --resume: finish an EXISTING contract, deploy nothing ──────────────────────────────────
|
|
@@ -3894,36 +4377,39 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3894
4377
|
if (flags[flag] !== undefined)
|
|
3895
4378
|
throw new Error(`--resume cannot be combined with --${flag}: ${why}.`);
|
|
3896
4379
|
}
|
|
3897
|
-
//
|
|
3898
|
-
//
|
|
3899
|
-
// `totalSupply()
|
|
3900
|
-
// no whole-contract total — see tokens.ts's
|
|
3901
|
-
//
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
`
|
|
3907
|
-
|
|
3908
|
-
|
|
4380
|
+
// 721 SeriesCode vs EditionCode: same setup shape (script chunks, schemas, deps, on-chain URI
|
|
4381
|
+
// legs), different mint shape — a 721 mints ONE sequential token against a whole-contract
|
|
4382
|
+
// `totalSupply()`; an edition mints AMOUNT copies of a specific id against that id's own
|
|
4383
|
+
// `totalSupply(id)` (there is no whole-contract total on an edition — see tokens.ts's
|
|
4384
|
+
// `TokenListing.totalSupply` doc). `resume.ts` models both (`planResume`/`planEditionResume`,
|
|
4385
|
+
// sharing the four non-mint legs via `planCoreLegs`); this is the one place that decides which.
|
|
4386
|
+
const isEdition = await isEditionContract(publicClient, target);
|
|
4387
|
+
if (!isEdition && flags['mint-amount'] !== undefined) {
|
|
4388
|
+
throw new Error(`--resume ${target}: this is a 721 SeriesCode contract, so --mint-amount has nothing to address — its mint leg ` +
|
|
4389
|
+
`is a single whole-contract shortfall with no per-id amount. Drop --mint-amount (use --mint-count).`);
|
|
4390
|
+
}
|
|
4391
|
+
const abi = isEdition ? editionCodeAbi : seriesCodeAbi;
|
|
3909
4392
|
const owner = await publicClient
|
|
3910
|
-
.readContract({ address: target, abi
|
|
4393
|
+
.readContract({ address: target, abi, functionName: 'owner' })
|
|
3911
4394
|
.catch(() => undefined);
|
|
3912
4395
|
if (!owner)
|
|
3913
|
-
throw new Error(`--resume ${target}: could not read owner() — is this an ABX SeriesCode contract?`);
|
|
4396
|
+
throw new Error(`--resume ${target}: could not read owner() — is this an ABX SeriesCode/EditionCode contract?`);
|
|
3914
4397
|
step('Resume — read what is missing, send only that');
|
|
3915
|
-
info(`target ${bold(target)} ${dim(`· owner ${owner}`)}`);
|
|
4398
|
+
info(`target ${bold(target)} ${dim(`· owner ${owner}${isEdition ? ' · EditionCode (ERC-1155)' : ''}`)}`);
|
|
3916
4399
|
// Every read is a view on the target; the intended legs come from the SAME builder a fresh deploy
|
|
3917
|
-
// uses, so what gets sent is by construction what the deploy would have written.
|
|
4400
|
+
// uses, so what gets sent is by construction what the deploy would have written. Only the four
|
|
4401
|
+
// non-mint groups are used on the edition branch below — `legs.mints` is the 721 shape and is
|
|
4402
|
+
// rebuilt per-id there instead (see resume.ts's class doc for why the split).
|
|
3918
4403
|
const legs = setupLegGroups(owner);
|
|
3919
|
-
//
|
|
3920
|
-
//
|
|
4404
|
+
// The contract is `T | null` (the planner distinguishes "read failed" from a legitimately-falsy
|
|
4405
|
+
// result); tryReadContract's is `T | undefined` — adapt at the boundary.
|
|
3921
4406
|
const readAt = async (functionName, args = []) => {
|
|
3922
|
-
const result = await tryReadContract(publicClient, { address: target, abi
|
|
4407
|
+
const result = await tryReadContract(publicClient, { address: target, abi, functionName, args });
|
|
3923
4408
|
return result === undefined ? null : result;
|
|
3924
4409
|
};
|
|
3925
4410
|
const nonZero = (a) => (a && a !== zeroAddress ? a : null);
|
|
3926
|
-
|
|
4411
|
+
// The four id-agnostic reads, shared verbatim by both standards (see resume.ts's `ResumeReaderCore`).
|
|
4412
|
+
const coreReads = {
|
|
3927
4413
|
scriptChunkCount: async () => Number((await readAt('scriptChunkCount')) ?? 0n),
|
|
3928
4414
|
scriptChunk: (index) => readAt('scriptChunk', [BigInt(index)]),
|
|
3929
4415
|
schemaExists: async (key) => {
|
|
@@ -3938,53 +4424,180 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3938
4424
|
const r = await readAt('contractField', [encodeTag(field)]);
|
|
3939
4425
|
return !!r && r[0] !== `0x${'0'.repeat(64)}`;
|
|
3940
4426
|
},
|
|
3941
|
-
totalSupply: async () => Number((await readAt('totalSupply')) ?? 0n),
|
|
3942
4427
|
};
|
|
3943
|
-
const plan =
|
|
4428
|
+
const plan = isEdition
|
|
4429
|
+
? await (async () => {
|
|
4430
|
+
// `--mint-count`/`--mint-amount` are the SAME flags a fresh `--copies` deploy reads (see
|
|
4431
|
+
// cmdDeployEditionCodeBody) — so re-running the original command with `--copies` swapped
|
|
4432
|
+
// for `--resume <address>` reproduces the identical intended mint plan: ids 0..mintCount-1,
|
|
4433
|
+
// each an intended TOTAL of mintAmount copies.
|
|
4434
|
+
const mintAmount = flags['mint-amount'] !== undefined ? parseNonNegativeIntFlag(String(flags['mint-amount']), 'mint-amount') : 1n;
|
|
4435
|
+
if (mintCount > 0 && mintAmount === 0n) {
|
|
4436
|
+
throw new Error('--mint-amount 0 with ids being pre-minted makes no sense — pass a positive --mint-amount, or drop --mint-count/--no-mint it entirely.');
|
|
4437
|
+
}
|
|
4438
|
+
const editionLegs = {
|
|
4439
|
+
chunks: legs.chunks,
|
|
4440
|
+
schemas: legs.schemas,
|
|
4441
|
+
deps: legs.deps,
|
|
4442
|
+
uri: legs.uri,
|
|
4443
|
+
mints: Array.from({ length: mintCount }, (_, id) => ({
|
|
4444
|
+
id: BigInt(id),
|
|
4445
|
+
intendedAmount: mintAmount,
|
|
4446
|
+
dataForAmount: (amount) => encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'mint', args: [owner, BigInt(id), amount] }),
|
|
4447
|
+
})),
|
|
4448
|
+
};
|
|
4449
|
+
const reader = {
|
|
4450
|
+
...coreReads,
|
|
4451
|
+
totalSupplyForId: async (id) => Number((await readAt('totalSupply', [id])) ?? 0n),
|
|
4452
|
+
};
|
|
4453
|
+
return planEditionResume(reader, editionLegs);
|
|
4454
|
+
})()
|
|
4455
|
+
: await planResume({ ...coreReads, totalSupply: async () => Number((await readAt('totalSupply')) ?? 0n) }, legs);
|
|
3944
4456
|
for (const line of plan.done)
|
|
3945
4457
|
ok(line);
|
|
3946
4458
|
for (const line of plan.todo)
|
|
3947
4459
|
info(`will send — ${line}`);
|
|
4460
|
+
// A resume plan object (below) reports its OWN family/roles/custody, deliberately lighter than a
|
|
4461
|
+
// fresh deploy's: royalty, mint, and byte custody are InitParams fields fixed at the ORIGINAL
|
|
4462
|
+
// deploy — a resume only ever finishes MISSING setup legs, so those fields are explicit `null`
|
|
4463
|
+
// (see deploy-plan.ts's own doc comments) rather than restating what this invocation's flags say,
|
|
4464
|
+
// which a resume does not actually write.
|
|
4465
|
+
// ANNOTATED, not inferred: this object is spread into four `satisfies DeployPlan` emits below, and
|
|
4466
|
+
// an un-annotated literal WIDENS its own properties (`schemaVersion` 1 -> number, `family`
|
|
4467
|
+
// 'code'|'code-edition' -> string), so every one of those spreads would fail to satisfy the plan.
|
|
4468
|
+
// The Omit names exactly the three keys each call site supplies for itself.
|
|
4469
|
+
const resumePlanBase = {
|
|
4470
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
4471
|
+
family: isEdition ? 'code-edition' : 'code',
|
|
4472
|
+
lane,
|
|
4473
|
+
roles: { signer: owner, owner, royaltyReceiver: null, primaryPayee: null, minter: null },
|
|
4474
|
+
royalty: null,
|
|
4475
|
+
// renderer/image: null — both are InitParams facts fixed at the ORIGINAL deploy (like
|
|
4476
|
+
// royalty/mint above); a resume doesn't re-read or re-decide either here.
|
|
4477
|
+
custody: { onChainUri, imageOnChain: null, backend: null, tokenUriBase: null, contractUriBase: null, renderer: null, image: null },
|
|
4478
|
+
mint: null,
|
|
4479
|
+
warnings: planWarnings.slice(),
|
|
4480
|
+
// Still reflects what THIS invocation's flags say about every marketplace-facing surface —
|
|
4481
|
+
// computed once, unconditionally, at the top of the function (see `imageDisposition` et al.),
|
|
4482
|
+
// so it's available whether this run turns out to be a fresh deploy or a `--resume`.
|
|
4483
|
+
surfaces: {
|
|
4484
|
+
thumbnail: { ok: !imageOrphaned, detail: stripAnsiForPlan(imageDisposition) },
|
|
4485
|
+
traits: { ok: !traitsBroken, detail: stripAnsiForPlan(traitsDisposition) },
|
|
4486
|
+
postParams: { ok: !(undeclaredParams.length > 0 || paramsNudge), detail: stripAnsiForPlan(paramsDisposition) },
|
|
4487
|
+
},
|
|
4488
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
4489
|
+
};
|
|
3948
4490
|
if (!plan.calls.length) {
|
|
3949
4491
|
ok(`nothing missing — this contract's setup is already complete. ${dim('Confirm with `abx verify ' + target + '`.')}`);
|
|
3950
|
-
emit(jsonSafe({
|
|
4492
|
+
emit(jsonSafe({
|
|
4493
|
+
command: 'deploy-code', kind: isEdition ? 'edition-code' : 'code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, sent: false, complete: true, sentLegs: 0,
|
|
4494
|
+
plan: {
|
|
4495
|
+
...resumePlanBase,
|
|
4496
|
+
transactions: { approvals: 0, legs: null },
|
|
4497
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4498
|
+
resume: { target, sentLegs: 0, complete: true },
|
|
4499
|
+
},
|
|
4500
|
+
}));
|
|
3951
4501
|
console.log('');
|
|
3952
4502
|
return;
|
|
3953
4503
|
}
|
|
3954
|
-
|
|
4504
|
+
// Reconstruct `plan.calls` (one flat, ORDERED array) back into the four leg groups it was built
|
|
4505
|
+
// from — chunks, schemas, deps, uri, in that exact order (resume.ts's `planCoreLegs` guarantees
|
|
4506
|
+
// it; see its module doc) — using the counts `plan.sending` already reports, so this can't drift
|
|
4507
|
+
// from what the diff actually decided to send. Mints (whatever is left) are appended by
|
|
4508
|
+
// `planResume`/`planEditionResume` AFTER these four, also guaranteed by resume.ts.
|
|
4509
|
+
let resumeCallPtr = 0;
|
|
4510
|
+
const takeResumeCalls = (n) => {
|
|
4511
|
+
const slice = plan.calls.slice(resumeCallPtr, resumeCallPtr + n);
|
|
4512
|
+
resumeCallPtr += n;
|
|
4513
|
+
return slice;
|
|
4514
|
+
};
|
|
4515
|
+
const missingChunkCalls = takeResumeCalls(plan.sending.chunkIndices.length);
|
|
4516
|
+
const missingSchemaCalls = takeResumeCalls(plan.sending.schemaKeys.length);
|
|
4517
|
+
const missingDepsCalls = takeResumeCalls(plan.sending.deps ? legs.deps.calls.length : 0);
|
|
4518
|
+
const missingUriCalls = takeResumeCalls(plan.sending.uri ? legs.uri.calls.length : 0);
|
|
4519
|
+
const missingMintCalls = takeResumeCalls(plan.sending.mints);
|
|
4520
|
+
// Same gas-bounded batching a fresh deploy uses (`planCodeSetupBatches`) — a `--resume` whose
|
|
4521
|
+
// setup never landed AT ALL is missing exactly as much as a fresh deploy would have sent, so it
|
|
4522
|
+
// needs the identical split to clear the same `eth_estimateGas` ceiling.
|
|
4523
|
+
const resumeBatches = planCodeSetupBatches({
|
|
4524
|
+
chunks: missingChunkCalls.map((data, i) => {
|
|
4525
|
+
const bytes = plan.sending.chunkBytes[i];
|
|
4526
|
+
return { calls: [data], gas: estimateChunkGasForBytes(bytes), label: `script chunk [${plan.sending.chunkIndices[i]}] (${bytes} bytes)`, kind: 'chunk', chunkBytes: [bytes] };
|
|
4527
|
+
}),
|
|
4528
|
+
config: [
|
|
4529
|
+
...missingSchemaCalls.map((data, i) => schemaSetupLeg(plan.sending.schemaKeys[i], data)),
|
|
4530
|
+
...(missingDepsCalls.length ? [depsSetupLeg(missingDepsCalls)] : []),
|
|
4531
|
+
...(missingUriCalls.length ? [uriSetupLeg(missingUriCalls)] : []),
|
|
4532
|
+
],
|
|
4533
|
+
mints: missingMintCalls.map(mintSetupLeg),
|
|
4534
|
+
});
|
|
4535
|
+
const setupTxs = codeSetupTxsFromBatches(resumeBatches, {
|
|
3955
4536
|
contract: target,
|
|
3956
|
-
calls: plan.calls,
|
|
3957
4537
|
chainId: resolveChain(CHAIN).id,
|
|
3958
|
-
chunkCount: plan.sending.chunkIndices.length,
|
|
3959
|
-
chunkBytes: plan.sending.chunkBytes,
|
|
3960
|
-
schemaKeys: plan.sending.schemaKeys,
|
|
3961
4538
|
deps: plan.sending.deps ? deps.map((d) => d.display) : [],
|
|
3962
|
-
onchainUri: plan.sending.uri,
|
|
3963
4539
|
});
|
|
4540
|
+
// `transactions.legs` names WHICH groups ride the setup, from the same `plan.sending` flags that
|
|
4541
|
+
// build the batches above (never re-derived) — GROUP names, not a literal per-transaction count
|
|
4542
|
+
// (see deploy-plan.ts's own doc comment on `legs`; a `--resume` may now send more than one setup
|
|
4543
|
+
// transaction, same as a fresh deploy).
|
|
4544
|
+
const resumeLegs = [
|
|
4545
|
+
plan.sending.chunkIndices.length ? 'chunks' : null,
|
|
4546
|
+
plan.sending.schemaKeys.length ? 'param-schemas' : null,
|
|
4547
|
+
plan.sending.deps ? 'dependencies' : null,
|
|
4548
|
+
plan.sending.uri ? 'onchain-uri' : null,
|
|
4549
|
+
].filter((leg) => leg !== null);
|
|
3964
4550
|
// Routed through the shared risk gate — the same choke point ownerops.ts's `runWrite` uses for
|
|
3965
4551
|
// every owner-op: --dry-run preview / --confirm prompt / lane selection / signing, all one way.
|
|
3966
4552
|
// This used to be its own hand-rolled copy of that exact shape (a dry-run print then confirmSend
|
|
3967
4553
|
// then signTx, duplicated from ownerops.ts's `runWrite`); a deploy-family write is a write like
|
|
3968
4554
|
// any other, so it takes the identical gate (the preview also gains the `to`/`owner` lines
|
|
3969
|
-
// `runWrite`'s preview always printed).
|
|
4555
|
+
// `runWrite`'s preview always printed) — called once per gas-bounded batch, in order.
|
|
3970
4556
|
if (isDryRun(flags)) {
|
|
3971
|
-
emit(jsonSafe({
|
|
4557
|
+
emit(jsonSafe({
|
|
4558
|
+
command: 'deploy-code', kind: isEdition ? 'edition-code' : 'code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, dryRun: true, sent: false, complete: false, sentLegs: plan.calls.length,
|
|
4559
|
+
plan: {
|
|
4560
|
+
...resumePlanBase,
|
|
4561
|
+
transactions: { approvals: setupTxs.length, legs: resumeLegs },
|
|
4562
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4563
|
+
resume: { target, sentLegs: plan.calls.length, complete: false },
|
|
4564
|
+
},
|
|
4565
|
+
}));
|
|
3972
4566
|
}
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
4567
|
+
let lastResult = null;
|
|
4568
|
+
for (const tx of setupTxs) {
|
|
4569
|
+
const result = await gatedSend(() => tx, flags, { chainKey: CHAIN, expectedSigner: owner });
|
|
4570
|
+
if (result) {
|
|
4571
|
+
ok(`${tx.summary}`);
|
|
4572
|
+
lastResult = result;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
if (isDryRun(flags))
|
|
4576
|
+
return;
|
|
4577
|
+
if (!lastResult) {
|
|
4578
|
+
// The cold ('unsigned') lane: every batch printed its own unsigned tx above (in order — they
|
|
4579
|
+
// target the SAME already-existing contract, so unlike a fresh deploy's deploy→setup dependency
|
|
4580
|
+
// there's no reason to withhold the later ones). Nothing was broadcast, so there is no block to
|
|
4581
|
+
// scan from and no indexing to do yet.
|
|
4582
|
+
console.log(`\n${dim(` unsigned — broadcast ${setupTxs.length > 1 ? 'these, in order,' : 'it'} then re-run \`abx verify ${target}\` to confirm the setup completed.`)}\n`);
|
|
4583
|
+
emit(jsonSafe({
|
|
4584
|
+
command: 'deploy-code', kind: isEdition ? 'edition-code' : 'code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, sent: false, complete: false, sentLegs: plan.calls.length,
|
|
4585
|
+
plan: {
|
|
4586
|
+
...resumePlanBase,
|
|
4587
|
+
transactions: { approvals: setupTxs.length, legs: resumeLegs },
|
|
4588
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4589
|
+
resume: { target, sentLegs: plan.calls.length, complete: false },
|
|
4590
|
+
},
|
|
4591
|
+
}));
|
|
3979
4592
|
return;
|
|
3980
4593
|
}
|
|
3981
|
-
ok(`${tx.summary}`);
|
|
3982
4594
|
// Locals, not the outer clone/deployBlock: this branch never falls through to the deploy path, and
|
|
3983
4595
|
// the authoritative scan floor is still the clone's CREATION block (getCode search), not this
|
|
3984
4596
|
// repair tx's block — flooring the resolver above the deploy would hide the `code` field again.
|
|
3985
|
-
const resumedFloor = (await discoverDeployBlock(publicClient, target)) ??
|
|
4597
|
+
const resumedFloor = (await discoverDeployBlock(publicClient, target)) ?? lastResult.blockNumber;
|
|
3986
4598
|
emit(jsonSafe({
|
|
3987
4599
|
command: 'deploy-code',
|
|
4600
|
+
kind: isEdition ? 'edition-code' : 'code',
|
|
3988
4601
|
resumed: target,
|
|
3989
4602
|
address: target,
|
|
3990
4603
|
chain: CHAIN,
|
|
@@ -3993,16 +4606,34 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3993
4606
|
sent: true,
|
|
3994
4607
|
complete: true,
|
|
3995
4608
|
sentLegs: plan.calls.length,
|
|
3996
|
-
txHash:
|
|
4609
|
+
txHash: lastResult.txHash,
|
|
4610
|
+
plan: {
|
|
4611
|
+
...resumePlanBase,
|
|
4612
|
+
transactions: { approvals: setupTxs.length, legs: resumeLegs },
|
|
4613
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4614
|
+
resume: { target, sentLegs: plan.calls.length, complete: true },
|
|
4615
|
+
},
|
|
3997
4616
|
}));
|
|
3998
4617
|
step('Index');
|
|
3999
|
-
|
|
4618
|
+
// The factory HINT registerAndIndexLocally passes through to detectCanonicalFactory is taken as
|
|
4619
|
+
// an UNVERIFIED override when present (see anchors.ts) — so it must name the RIGHT factory family
|
|
4620
|
+
// for this target. `factory` (outer scope) is the 721 SeriesCode factory this function resolved at
|
|
4621
|
+
// its own "Trust anchor" step; an EditionCode target needs its own factory's address instead (a
|
|
4622
|
+
// read-only manifest lookup — never bootstraps one, unlike a real edition deploy).
|
|
4623
|
+
await registerAndIndexLocally(target, {
|
|
4624
|
+
'from-block': resumedFloor.toString(),
|
|
4625
|
+
factory: isEdition ? editionCodeFactoryAddress(flags.factory) : factory,
|
|
4626
|
+
label: name,
|
|
4627
|
+
});
|
|
4000
4628
|
console.log(`\n ${g('\u2713 setup finished')} \u2014 ${dim('confirm it resolves:')} ${bold(`abx verify ${target}`)}` +
|
|
4001
4629
|
`${onChainUri ? dim(' (chain-complete + the on-chain tokenURI)') : ''}\n`);
|
|
4002
4630
|
return;
|
|
4003
4631
|
}
|
|
4004
4632
|
let clone;
|
|
4005
4633
|
let deployBlock;
|
|
4634
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`). The
|
|
4635
|
+
// 'unsigned' branch below returns before the sent `emit()`, so it never needs to set this.
|
|
4636
|
+
let deployerAddr;
|
|
4006
4637
|
if (lane === 'unsigned') {
|
|
4007
4638
|
// cold lane: deterministic deploy makes the whole sequence pre-computable — the clone
|
|
4008
4639
|
// address is a pure function of (factory, salt), so setup + mints target it up front.
|
|
@@ -4027,6 +4658,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
4027
4658
|
const session = await openWalletSession({ chainKey: CHAIN, expectedSigner: flags.for, total: approvals, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
4028
4659
|
try {
|
|
4029
4660
|
const signer = await session.connect();
|
|
4661
|
+
deployerAddr = signer;
|
|
4030
4662
|
const { clone: predicted, txs } = await preparedFor(signer);
|
|
4031
4663
|
// The scan floor is the clone-CREATION block (txs[0]) — NOT the last tx. A wallet session can
|
|
4032
4664
|
// span blocks (deploy at N, setup+mint at N+2), and the `code` field is written in the deploy
|
|
@@ -4052,6 +4684,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
4052
4684
|
// `signHotSequence` (signer.ts) / `makeHotSender` (sdk execute.ts) for the read-after-write-lag
|
|
4053
4685
|
// reasoning this used to be a hand-rolled loop for.
|
|
4054
4686
|
const { account } = makeWalletClient({ chainKey: CHAIN });
|
|
4687
|
+
deployerAddr = account.address;
|
|
4055
4688
|
const { clone: predicted, txs } = await preparedFor(account.address);
|
|
4056
4689
|
const results = await signHotSequence(txs, { chainKey: CHAIN, yes: !!flags.yes });
|
|
4057
4690
|
clone = predicted;
|
|
@@ -4087,6 +4720,39 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
4087
4720
|
name,
|
|
4088
4721
|
symbol,
|
|
4089
4722
|
onChainUri,
|
|
4723
|
+
plan: {
|
|
4724
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
4725
|
+
family: 'code',
|
|
4726
|
+
lane,
|
|
4727
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
4728
|
+
roles: {
|
|
4729
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
4730
|
+
primaryPayee: flags['primary-payee'] ?? null,
|
|
4731
|
+
minter: flags.minter ?? null,
|
|
4732
|
+
},
|
|
4733
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
4734
|
+
custody: {
|
|
4735
|
+
onChainUri,
|
|
4736
|
+
imageOnChain: null, // see `surfaces.thumbnail`
|
|
4737
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
4738
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
4739
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
4740
|
+
renderer: onChainUri ? metadataRenderer : null,
|
|
4741
|
+
image: null, // see `surfaces.thumbnail`
|
|
4742
|
+
},
|
|
4743
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployerAddr ?? null : null },
|
|
4744
|
+
// Unlike the dry run, the sent path computes no cost estimate (the gas-guidance try/catch is
|
|
4745
|
+
// dry-run-only — a real send just pays whatever `estimateGas` returns at signing time).
|
|
4746
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4747
|
+
warnings: planWarnings.slice(),
|
|
4748
|
+
surfaces: {
|
|
4749
|
+
thumbnail: { ok: !imageOrphaned, detail: stripAnsiForPlan(imageDisposition) },
|
|
4750
|
+
traits: { ok: !traitsBroken, detail: stripAnsiForPlan(traitsDisposition) },
|
|
4751
|
+
postParams: { ok: !(undeclaredParams.length > 0 || paramsNudge), detail: stripAnsiForPlan(paramsDisposition) },
|
|
4752
|
+
},
|
|
4753
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
4754
|
+
resume: null,
|
|
4755
|
+
},
|
|
4090
4756
|
}));
|
|
4091
4757
|
step('Index');
|
|
4092
4758
|
await registerAndIndexLocally(clone, { 'from-block': deployBlock.toString(), factory, label: name });
|
|
@@ -4175,43 +4841,51 @@ export const DEPLOY_CODE_EDITION_FLAGS = new Set([
|
|
|
4175
4841
|
'public-base-url', 'onchain-uri', 'generator', 'renderer', 'port',
|
|
4176
4842
|
'sign', 'unsigned', 'for', 'salt', 'factory', 'bootstrap-factory', 'sign-url-file',
|
|
4177
4843
|
'dry-run', 'confirm', 'yes', 'json', 'copies',
|
|
4178
|
-
//
|
|
4179
|
-
//
|
|
4180
|
-
|
|
4844
|
+
// storage (directory mode + --image-base's off-chain still) — mirrors storageOverrides(), same
|
|
4845
|
+
// allowlist entries as the 721 twin's DEPLOY_CODE_FLAGS.
|
|
4846
|
+
'code-dir', 'backend', 'endpoint', 'bucket', 'region', 'prefix', 'public-base', 'gateway', 'mode', 'api-url', 'upload-url', 'provider', 'storage-signer',
|
|
4847
|
+
'ipfs-gateway', 'arweave-gateway',
|
|
4848
|
+
// NOT scope cuts: --image-renderer/--attributes-renderer (on-chain field renderers) and
|
|
4849
|
+
// --image-base (an off-chain deterministic-per-id still) are all fully wired on EditionCode —
|
|
4850
|
+
// `abx help deploy-code` documents them as working. --dep/--dep-registry likewise: EditionCode
|
|
4851
|
+
// inherits the same Dependencies extension, and the legs ride the same setup multicall.
|
|
4852
|
+
'image-renderer', 'attributes-renderer', 'image-base', 'dep', 'dep-registry',
|
|
4853
|
+
// v1-scope-cut flag: kept in the ALLOWLIST (not stray) so it reaches this function's own pointed
|
|
4854
|
+
// refusal below, rather than the generic "unrecognized flag" `refuseStrayFlags` would give it
|
|
4855
|
+
// otherwise — a real but less helpful refusal (it doesn't say WHY, or the way out).
|
|
4181
4856
|
// (`resume` is NOT here: --resume + --copies is refused earlier, at cmdDeployCode's dispatch —
|
|
4182
4857
|
// this function never runs with flags.resume set, so listing it here would just be dead.)
|
|
4183
|
-
'
|
|
4184
|
-
// NOT scope cuts: --dep/--dep-registry are fully wired on EditionCode (it inherits the same
|
|
4185
|
-
// Dependencies extension, and the legs ride the same setup multicall — see the Dependencies step
|
|
4186
|
-
// below). They're plain supported flags; `abx help deploy-code` documents them as working.
|
|
4187
|
-
'dep', 'dep-registry',
|
|
4858
|
+
'no-delegation',
|
|
4188
4859
|
]);
|
|
4189
4860
|
export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
4190
|
-
|
|
4861
|
+
beginPlanWarnings();
|
|
4862
|
+
const usage = 'abx deploy-code --copies <n|open> (--script <file> | --code-dir <dir>) --name "Title" --symbol SYM (--public-base-url https://your.resolver.domain | --onchain-uri) ' +
|
|
4191
4863
|
'[--description "<s>"] [--external-url <url>] [--max N] [--mint-count N | --mint-all] [--mint-amount N] ' +
|
|
4192
4864
|
'[--schema key:Type:Auth,…] [--no-seed | --seed-source 0x..] [--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..]';
|
|
4193
4865
|
refuseStrayFlags(flags, DEPLOY_CODE_EDITION_FLAGS, 'deploy-code');
|
|
4194
4866
|
warnSignWithoutFor(flags);
|
|
4195
4867
|
const editionSize = parseCopies(flags.copies);
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
// effect runner's per-token still + the url-template's `{id}` substitution over an id SPACE rather
|
|
4201
|
-
// than a token sequence. That port is real work; these two were not.
|
|
4202
|
-
if (flags['image-base']) {
|
|
4203
|
-
throw new Error('--image-base is not wired for edition code deploys (--copies) yet — it needs the effect runner to write a\n' +
|
|
4204
|
-
' still per id, which the edition lane does not drive. The two flags beside it DO work here:\n' +
|
|
4205
|
-
' `--image-renderer 0x.. --attributes-renderer 0x.. --onchain-uri` computes image + traits in Solidity, on\n' +
|
|
4206
|
-
' chain, with nothing to render or host. Otherwise use --public-base-url and a resolver.');
|
|
4207
|
-
}
|
|
4868
|
+
const scriptPath = flags.script;
|
|
4869
|
+
const codeDir = flags['code-dir'];
|
|
4870
|
+
if (scriptPath && codeDir)
|
|
4871
|
+
throw new Error(usage);
|
|
4208
4872
|
// The in-chain Solidity renderer lane, same as the 721 twin: EditionCode composes the same
|
|
4209
4873
|
// OnChainMetadata extension and the same URI-renderer slots, so `image`/`attributes` at a
|
|
4210
4874
|
// `renderer` representation resolve through the canonical metadata renderer with no server in the
|
|
4211
4875
|
// graph. Proven end-to-end in contracts/test/EditionOnChainRender.t.sol.
|
|
4212
4876
|
const imageRendererFlag = flags['image-renderer'] ? String(flags['image-renderer']) : undefined;
|
|
4213
4877
|
const attributesRendererFlag = flags['attributes-renderer'] ? String(flags['attributes-renderer']) : undefined;
|
|
4214
|
-
|
|
4878
|
+
// --image-renderer and --image-base both set the `image` field — pick ONE, same rule as the 721
|
|
4879
|
+
// twin. Checked before either is resolved so the refusal is immediate rather than after a renderer
|
|
4880
|
+
// code-presence probe.
|
|
4881
|
+
if (imageRendererFlag && flags['image-base']) {
|
|
4882
|
+
throw new Error('--image-renderer and --image-base both set the `image` field — pick ONE: an ON-CHAIN Solidity render (--image-renderer, no infra) OR an off-chain bucket URL (--image-base).');
|
|
4883
|
+
}
|
|
4884
|
+
// A JS program either way (template chunks or a hosted build) — same "hasProgram" test as the 721
|
|
4885
|
+
// twin (see its own `const hasProgram = !!(scriptPath || codeDir);`), so every downstream
|
|
4886
|
+
// conditional gated on it (generator resolution, animation_url, the on-chain-URI legs) already
|
|
4887
|
+
// does the right thing for a directory build without a SEPARATE flag to thread through.
|
|
4888
|
+
const hasProgram = !!(scriptPath || codeDir);
|
|
4215
4889
|
const hasFieldRenderers = !!(imageRendererFlag || attributesRendererFlag);
|
|
4216
4890
|
if (!hasProgram && !hasFieldRenderers) {
|
|
4217
4891
|
throw new Error(`${usage}\n (or, for a FULLY on-chain Solidity render with no program: --copies <n|open> --image-renderer 0x.. ` +
|
|
@@ -4373,11 +5047,30 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4373
5047
|
}
|
|
4374
5048
|
}
|
|
4375
5049
|
}
|
|
5050
|
+
// Content: directory mode uploads the build (its root locator → the `code` collection field);
|
|
5051
|
+
// template mode chunks the script for on-chain storage. The directory UPLOAD is deferred until
|
|
5052
|
+
// after the --confirm gate (and skipped entirely on --dry-run) so a preview never pins bytes —
|
|
5053
|
+
// same shape as the 721 twin's identical block (see its own comment for the full reasoning).
|
|
4376
5054
|
step('Content');
|
|
4377
5055
|
let scriptChunks = [];
|
|
5056
|
+
let dirUpload = null;
|
|
4378
5057
|
let contentSummary;
|
|
4379
|
-
if (
|
|
4380
|
-
const
|
|
5058
|
+
if (codeDir) {
|
|
5059
|
+
const dirPath = resolvePath(codeDir);
|
|
5060
|
+
const files = readdirSync(dirPath).filter((f) => !f.startsWith('.') && statSync(joinPath(dirPath, f)).isFile());
|
|
5061
|
+
if (!files.includes('index.html'))
|
|
5062
|
+
throw new Error(`${dirPath} has no index.html — the directory entry, by convention`);
|
|
5063
|
+
const backend = resolveBackend(storageOptions(storageOverrides(flags)));
|
|
5064
|
+
if (!backend.putDirectory)
|
|
5065
|
+
throw new Error(`storage backend '${backend.id}' has no directory upload — use --backend ipfs (pinata) or arweave`);
|
|
5066
|
+
const entries = files.map((f) => ({ name: f, bytes: new Uint8Array(readFileSync(joinPath(dirPath, f))), contentType: contentTypeFromPath(joinPath(dirPath, f)) }));
|
|
5067
|
+
dirUpload = { backend, entries, sizes: entries.map((e) => e.bytes.length) };
|
|
5068
|
+
contentSummary = `${entries.length} file(s) → ${backend.id} directory (code field)`;
|
|
5069
|
+
info(`${entries.length} file(s) from ${basename(dirPath)}/ → ${backend.id} directory (code field)`);
|
|
5070
|
+
info(dim('the live view 302s through the gateway — it must serve HTML (the shared Pinata public gateway does not; use a dedicated gateway or arweave).'));
|
|
5071
|
+
}
|
|
5072
|
+
else if (scriptPath) {
|
|
5073
|
+
const source = readFileSync(resolvePath(scriptPath), 'utf8');
|
|
4381
5074
|
scriptChunks = planOnChainScript(source);
|
|
4382
5075
|
contentSummary = `script ${Buffer.byteLength(source, 'utf8')} bytes → ${scriptChunks.length} on-chain chunk(s)`;
|
|
4383
5076
|
ok(contentSummary);
|
|
@@ -4431,16 +5124,67 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4431
5124
|
if (hasFieldRenderers && !onChainUri) {
|
|
4432
5125
|
warn('--image-renderer/--attributes-renderer compute fields ON-CHAIN, but without --onchain-uri the URI renderers are never wired, so uri() still resolves through your resolver and nothing reads them. Add --onchain-uri.');
|
|
4433
5126
|
}
|
|
5127
|
+
// --image-base: bake the on-chain `image` as a url-template (`{base}/{id}.png`) — same field write
|
|
5128
|
+
// as the 721 twin's identical block (see its own comment for the full reasoning), one id space
|
|
5129
|
+
// finer: `{id}` substitutes the EditionCode id, not a sequential 721 token number, and the effect
|
|
5130
|
+
// runner's render sweep already excludes an id with no live copies (harness.ts) — an id with no
|
|
5131
|
+
// mint-time seed drawn yet has nothing to render FROM, the same reason a burned 721 token is
|
|
5132
|
+
// excluded. There is no per-copy addressing: every copy of an id shares one image, same as every
|
|
5133
|
+
// other field on an id.
|
|
5134
|
+
if (flags['image-base']) {
|
|
5135
|
+
const base = String(flags['image-base']);
|
|
5136
|
+
// --image-base needs a mutable, path-addressed URL (the effect runner overwrites the SAME key in
|
|
5137
|
+
// place) — a content-addressed gateway URL (ipfs/arweave) can't back a FIXED per-id address, no
|
|
5138
|
+
// matter what THIS deploy's own --backend happens to be. One shared validator decides this
|
|
5139
|
+
// (packages/storage/content-plan.ts), so this can never disagree with the 721 lane's own refusal.
|
|
5140
|
+
const imageBaseCombo = validateRenderStorageCombo({ imageBaseUrl: base });
|
|
5141
|
+
if (!imageBaseCombo.ok) {
|
|
5142
|
+
if (dryRun)
|
|
5143
|
+
warn(`would REFUSE — ${imageBaseCombo.reason}`);
|
|
5144
|
+
else
|
|
5145
|
+
throw new Error(imageBaseCombo.reason);
|
|
5146
|
+
}
|
|
5147
|
+
// NEVER bake a localhost/loopback image host on-chain — the token's `image` would resolve for no
|
|
5148
|
+
// one. This is the marketplace still's PERMANENT address.
|
|
5149
|
+
if (loopbackBaseUrl(base)) {
|
|
5150
|
+
const m = `--image-base ${base} is localhost/loopback — the on-chain image URL would resolve for no marketplace. Use a PUBLIC bucket (S3/R2/CDN) you control.`;
|
|
5151
|
+
if (dryRun)
|
|
5152
|
+
warn(`would REFUSE — ${m}`);
|
|
5153
|
+
else
|
|
5154
|
+
throw new Error(m);
|
|
5155
|
+
}
|
|
5156
|
+
// The exact trap a real agent hit on the 721 lane: it baked the S3 API endpoint (auth-only) as
|
|
5157
|
+
// --image-base. Refuse the R2 API form (never public); warn on the S3 API form.
|
|
5158
|
+
if (/\.r2\.cloudflarestorage\.com/i.test(base)) {
|
|
5159
|
+
const m = `--image-base ${base} is R2's S3 API endpoint (auth-only) — marketplaces get 403, it is NEVER public. Put THIS URL in ${bold('ABX_S3_ENDPOINT')} (uploads), and pass --image-base your bucket's PUBLIC read URL — enable an ${bold('r2.dev')} public URL (\`https://pub-<hash>.r2.dev\`) or a custom domain in the R2 dashboard.`;
|
|
5160
|
+
if (dryRun)
|
|
5161
|
+
warn(`would REFUSE — ${m}`);
|
|
5162
|
+
else
|
|
5163
|
+
throw new Error(m);
|
|
5164
|
+
}
|
|
5165
|
+
else if (/(^|\/\/)s3[.-][^/]*amazonaws\.com/i.test(base) || /\.s3[.-][^/]*amazonaws\.com/i.test(base)) {
|
|
5166
|
+
warn(`--image-base ${base} looks like the S3 API host — marketplaces can read it ONLY if the bucket has public-read (or you front it with CloudFront/a domain). If it's not public, use that URL in ${bold('ABX_S3_ENDPOINT')} and pass a public URL here instead.`);
|
|
5167
|
+
}
|
|
5168
|
+
const imageTemplate = base.includes('{id}') ? base : `${base.replace(/\/+$/, '')}/{id}.png`;
|
|
5169
|
+
contractFields.push({ field: encodeTag(F.image), representation: encodeTag(R.urlTemplate), value: toHex(imageTemplate) });
|
|
5170
|
+
info(`collection image → on-chain url-template ${bold(imageTemplate)} ${dim('(off-chain thumbnail at a deterministic per-id URL; the effect runner overwrites each key once a copy is minted)')}`);
|
|
5171
|
+
info(` ${dim('the runner uploads stills here via')} ${bold('ABX_S3_ENDPOINT · ABX_S3_BUCKET · ABX_S3_ACCESS_KEY_ID · ABX_S3_SECRET_ACCESS_KEY')} ${dim('(set ABX_S3_PUBLIC_BASE to this same --image-base; `abx storage show` checks them; details → reference/hosting.md)')}`);
|
|
5172
|
+
const s3Base = process.env.ABX_S3_PUBLIC_BASE?.replace(/\/+$/, '');
|
|
5173
|
+
if (s3Base && !imageTemplate.startsWith(`${s3Base}/`)) {
|
|
5174
|
+
warn(`--image-base isn't under ABX_S3_PUBLIC_BASE (${s3Base}) — the runner keys its deterministic publish off that base, so it won't auto-upload here unless they share an origin.`);
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
4434
5177
|
const onchainUriLegs = onChainUri ? onchainUriSetupCalls({ generator, metadataRenderer }) : [];
|
|
4435
5178
|
step('Deploy');
|
|
4436
5179
|
const lane = laneFromFlags(flags);
|
|
4437
5180
|
const minter = flags.minter ?? zeroAddress;
|
|
4438
5181
|
const primaryPayee = flags['primary-payee'] ?? zeroAddress;
|
|
4439
5182
|
const explicitSalt = parseSaltFlag(flags.salt);
|
|
4440
|
-
//
|
|
4441
|
-
// lane's "fold reserve mints + renderers into init" micro-optimization (only reachable
|
|
4442
|
-
// a renderer-only, no-program drop, which this function refuses anyway — every
|
|
4443
|
-
// deploy has a program).
|
|
5183
|
+
// Normally TWO transactions (deploy + one gas-bounded setup batch) — this leaner lane skips the
|
|
5184
|
+
// 721 code lane's "fold reserve mints + renderers into init" micro-optimization (only reachable
|
|
5185
|
+
// there for a renderer-only, no-program drop, which this function refuses anyway — every
|
|
5186
|
+
// edition-code deploy has a program). A setup large enough to miss the `eth_estimateGas` ceiling
|
|
5187
|
+
// (a big script) splits into MORE than one setup transaction — see `planCodeSetupBatches`.
|
|
4444
5188
|
const initParamsFor = (owner) => ({
|
|
4445
5189
|
owner,
|
|
4446
5190
|
name,
|
|
@@ -4466,53 +5210,52 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4466
5210
|
tokenFields: [],
|
|
4467
5211
|
contractFields,
|
|
4468
5212
|
});
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
for (let id = 0; id < mintCount; id++) {
|
|
4489
|
-
calls.push(encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'mint', args: [owner, BigInt(id), mintAmount] }));
|
|
4490
|
-
}
|
|
4491
|
-
return calls;
|
|
4492
|
-
};
|
|
5213
|
+
// Gas-bounded setup-transaction legs — script chunks, schemas, deps, on-chain-URI wiring are all
|
|
5214
|
+
// OWNER-INDEPENDENT (only a mint call's calldata embeds the owner+id+amount; its gas is a flat
|
|
5215
|
+
// constant regardless), so they're built once, up front, and reused both to size
|
|
5216
|
+
// `approvals`/`planSetupLegs` now and to build the real transactions in `preparedFor` below —
|
|
5217
|
+
// identical `.gas` sequence ⇒ identical split (see `planCodeSetupBatches`'s own doc for why that
|
|
5218
|
+
// can never drift).
|
|
5219
|
+
const chunkLegs = scriptChunks.map((chunk, i) => chunkSetupLeg(i, chunk, encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] })));
|
|
5220
|
+
const depCallsForEdition = dependencySetupCalls(deps, depRegistry);
|
|
5221
|
+
const configLegs = [
|
|
5222
|
+
...schemas.map((s) => schemaSetupLeg(s.key, encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(s.key), s.paramType, s.auth, s.authAddress, s.lockAfter, s.min, s.max, s.selectOptions] }))),
|
|
5223
|
+
...(depCallsForEdition.length ? [depsSetupLeg(depCallsForEdition)] : []),
|
|
5224
|
+
...(onchainUriLegs.length ? [uriSetupLeg(onchainUriLegs)] : []),
|
|
5225
|
+
];
|
|
5226
|
+
// Reserve mints: one `mint(to, id, amount)` per premint id — the edition twin of the 721 lane's N
|
|
5227
|
+
// identical `mint(owner)` calls, one id finer (a specific amount of copies, not just "one").
|
|
5228
|
+
const mintLegsFor = (owner) => Array.from({ length: mintCount }, (_, id) => mintSetupLeg(encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'mint', args: [owner, BigInt(id), mintAmount] })));
|
|
5229
|
+
// `owner` is irrelevant to the batch SHAPE (see the comment above), so `zeroAddress` here is a
|
|
5230
|
+
// placeholder used purely to size `approvals` before a deployer is known — never sent.
|
|
5231
|
+
const editionSetupBatches = planCodeSetupBatches({ chunks: chunkLegs, config: configLegs, mints: mintLegsFor(zeroAddress) });
|
|
4493
5232
|
const preparedFor = async (owner) => {
|
|
4494
5233
|
const salt = explicitSalt ?? saltFor(owner);
|
|
4495
5234
|
const clone = await predictClone(publicClient, { factory, salt });
|
|
4496
|
-
const
|
|
4497
|
-
const txs = [prepareDeployEditionCode({ factory, params: initParamsFor(owner), salt, chainId, clone })];
|
|
4498
|
-
if (calls.length) {
|
|
4499
|
-
txs.push(prepareCodeSetup({
|
|
4500
|
-
contract: clone,
|
|
4501
|
-
calls,
|
|
4502
|
-
chainId,
|
|
4503
|
-
chunkCount: scriptChunks.length,
|
|
4504
|
-
chunkBytes: scriptChunks.map((h) => (h.length - 2) / 2),
|
|
4505
|
-
schemaKeys: schemas.map((s) => s.key),
|
|
4506
|
-
onchainUri: onChainUri,
|
|
4507
|
-
}));
|
|
4508
|
-
}
|
|
5235
|
+
const setupTxs = codeSetupTxsFromBatches(planCodeSetupBatches({ chunks: chunkLegs, config: configLegs, mints: mintLegsFor(owner) }), { contract: clone, chainId, deps: deps.map((d) => d.display) });
|
|
5236
|
+
const txs = [prepareDeployEditionCode({ factory, params: initParamsFor(owner), salt, chainId, clone }), ...setupTxs];
|
|
4509
5237
|
return { clone, txs };
|
|
4510
5238
|
};
|
|
4511
|
-
// deps ride the same
|
|
4512
|
-
// but they must be COUNTED in the "is there a setup
|
|
4513
|
-
// (a script with no schemas and no premints) would
|
|
4514
|
-
|
|
4515
|
-
const
|
|
5239
|
+
// deps ride the same setup batch as chunks/schemas/mints when everything fits one transaction, so
|
|
5240
|
+
// they don't add an approval on their own — but they must be COUNTED in the "is there a setup
|
|
5241
|
+
// transaction at all" test, or a deps-only project (a script with no schemas and no premints) would
|
|
5242
|
+
// report 1 approval and send 2 transactions.
|
|
5243
|
+
const depLegCount = depCallsForEdition.length;
|
|
5244
|
+
const approvals = 1 + editionSetupBatches.length; // deploy tx + however many gas-bounded setup batches
|
|
5245
|
+
// Ordered legs riding the setup (if any) — GROUP names, not a literal per-transaction count (see
|
|
5246
|
+
// deploy-plan.ts's own doc comment on `legs`); same booleans `approvals` above is built from, so the
|
|
5247
|
+
// plan object's `transactions.legs` can never drift from the tx(es) the wallet actually signs.
|
|
5248
|
+
// Always ends with `'deploy'` when non-null.
|
|
5249
|
+
const planSetupLegs = approvals > 1
|
|
5250
|
+
? [
|
|
5251
|
+
scriptChunks.length ? 'chunks' : null,
|
|
5252
|
+
schemas.length ? 'param-schemas' : null,
|
|
5253
|
+
depLegCount ? 'dependencies' : null,
|
|
5254
|
+
onchainUriLegs.length ? 'onchain-uri' : null,
|
|
5255
|
+
mintCount ? 'mints' : null,
|
|
5256
|
+
'deploy',
|
|
5257
|
+
].filter((leg) => leg !== null)
|
|
5258
|
+
: null;
|
|
4516
5259
|
if (!dryRun) {
|
|
4517
5260
|
await confirmSend(
|
|
4518
5261
|
// Second site of the same missing binding as the plan line above — and the worse one: this is
|
|
@@ -4551,18 +5294,70 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4551
5294
|
printSeedSourcePlan(seedSource, chainId);
|
|
4552
5295
|
info(`PostParam schema(s): ${schemas.length ? schemas.map((s) => describeSchema(s)).join(', ') : 'none'}`);
|
|
4553
5296
|
info(`mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${deployer ?? 'your wallet'} at deploy` : 'deferred (mint later / external minter)'}`);
|
|
4554
|
-
info(`transactions:
|
|
5297
|
+
info(`transactions: ${approvals} — deploy + ${editionSetupBatches.length} setup ${editionSetupBatches.length === 1 ? 'transaction' : 'transactions (gas-bounded)'} ` +
|
|
5298
|
+
`(${[scriptChunks.length ? 'script chunks' : null, schemas.length ? 'schemas' : null, onChainUri ? 'on-chain URI wiring' : null, mintCount > 0 ? 'reserve mints' : null].filter(Boolean).join(' + ') || 'nothing else'})`);
|
|
4555
5299
|
info(`approvals ${approvals} wallet approval(s)`); // parity with the 721 code twin — it had this only in the --confirm sentence
|
|
4556
5300
|
if (!explicitSalt && deployer) {
|
|
4557
5301
|
console.log(`\n ${bold('salt')} ${g(salt)}`);
|
|
4558
5302
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
|
|
4559
5303
|
}
|
|
4560
5304
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
|
|
4561
|
-
emit(jsonSafe({
|
|
5305
|
+
emit(jsonSafe({
|
|
5306
|
+
command: 'deploy-code', kind: 'edition-code', copies: editionSize.toString(), dryRun: true, sent: false, address: shownAddr, chain: CHAIN, chainId, factory, name, symbol,
|
|
5307
|
+
plan: {
|
|
5308
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
5309
|
+
family: 'code-edition',
|
|
5310
|
+
lane,
|
|
5311
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
5312
|
+
roles: {
|
|
5313
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
5314
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
5315
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
5316
|
+
},
|
|
5317
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
5318
|
+
custody: {
|
|
5319
|
+
onChainUri,
|
|
5320
|
+
imageOnChain: null, // this lane has no surfaces block (see deploy-plan.ts); a field-renderer image is still `hasFieldRenderers` in the prose, not tracked here
|
|
5321
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
5322
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
5323
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
5324
|
+
renderer: onChainUri ? metadataRenderer : null,
|
|
5325
|
+
image: null, // no single --image file on this lane (see custody.image's own doc comment)
|
|
5326
|
+
},
|
|
5327
|
+
mint: {
|
|
5328
|
+
deferred: mintCount === 0,
|
|
5329
|
+
count: mintCount,
|
|
5330
|
+
amountPerId: mintAmount.toString(),
|
|
5331
|
+
recipient: mintCount > 0 ? deployer : null,
|
|
5332
|
+
},
|
|
5333
|
+
estimate: { ethApprox: null, gasApprox: null }, // this lane computes no cost estimate
|
|
5334
|
+
warnings: planWarnings.slice(),
|
|
5335
|
+
surfaces: null, // only the fresh 721 code lane computes the per-surface disposition today
|
|
5336
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
5337
|
+
resume: null,
|
|
5338
|
+
},
|
|
5339
|
+
}));
|
|
4562
5340
|
return;
|
|
4563
5341
|
}
|
|
5342
|
+
// Real deploy: perform the deferred directory upload now (past the confirm gate). `contractFields`
|
|
5343
|
+
// is captured by reference in `initParamsFor` above — pushing to it here still lands in the CREATE
|
|
5344
|
+
// tx's init params, since that closure isn't actually CALLED until the signing lane below runs.
|
|
5345
|
+
if (dirUpload) {
|
|
5346
|
+
const { base } = await dirUpload.backend.putDirectory(dirUpload.entries);
|
|
5347
|
+
// Store the BARE locator (CID / arweave txid), never a full gateway URL — the resolver applies
|
|
5348
|
+
// the gateway at serve time per `representation`.
|
|
5349
|
+
const ipfsCid = base.match(/\/ipfs\/([^/]+)/)?.[1];
|
|
5350
|
+
const arTxid = base.match(/arweave\.net\/([^/?#]+)/i)?.[1];
|
|
5351
|
+
const rep = ipfsCid ? 'ipfs' : dirUpload.backend.id === 'arweave' ? 'arweave' : 'url';
|
|
5352
|
+
const value = ipfsCid ?? (rep === 'arweave' ? (arTxid ?? base) : base);
|
|
5353
|
+
contractFields.push({ field: encodeTag('code'), representation: encodeTag(rep), value: toHex(value) });
|
|
5354
|
+
ok(`uploaded ${dirUpload.entries.length} file(s) → code field (${rep}): ${value}`);
|
|
5355
|
+
}
|
|
4564
5356
|
let clone;
|
|
4565
5357
|
let deployBlock;
|
|
5358
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`). The
|
|
5359
|
+
// 'unsigned' branch below returns before the sent `emit()`, so it never needs to set this.
|
|
5360
|
+
let deployerAddr;
|
|
4566
5361
|
if (lane === 'unsigned') {
|
|
4567
5362
|
const signer = flags.for;
|
|
4568
5363
|
if (!signer)
|
|
@@ -4577,6 +5372,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4577
5372
|
const session = await openWalletSession({ chainKey: CHAIN, expectedSigner: flags.for, total: approvals, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
4578
5373
|
try {
|
|
4579
5374
|
const signer = await session.connect();
|
|
5375
|
+
deployerAddr = signer;
|
|
4580
5376
|
const { clone: predicted, txs } = await preparedFor(signer);
|
|
4581
5377
|
let first;
|
|
4582
5378
|
for (const tx of txs) {
|
|
@@ -4593,6 +5389,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4593
5389
|
}
|
|
4594
5390
|
else {
|
|
4595
5391
|
const { account } = makeWalletClient({ chainKey: CHAIN });
|
|
5392
|
+
deployerAddr = account.address;
|
|
4596
5393
|
const { clone: predicted, txs } = await preparedFor(account.address);
|
|
4597
5394
|
const results = await signHotSequence(txs, { chainKey: CHAIN, yes: !!flags.yes });
|
|
4598
5395
|
clone = predicted;
|
|
@@ -4606,7 +5403,41 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4606
5403
|
info(`permanent: max ${max} id(s) · royalty ${royaltyBps / 100}%${flags['royalty-bps'] === undefined ? ' ⚠ default' : ''} → the deploying wallet` +
|
|
4607
5404
|
` · cap ${maxRoyaltyBps / 100}%${burnable ? ' · burnable' : ''}` +
|
|
4608
5405
|
dim(' (rate is changeable with `abx set-royalty`; the CAP only ever goes down)'));
|
|
4609
|
-
emit(jsonSafe({
|
|
5406
|
+
emit(jsonSafe({
|
|
5407
|
+
command: 'deploy-code', kind: 'edition-code', copies: editionSize.toString(), address: clone, chain: CHAIN, chainId, factory, deployBlock, name, symbol, onChainUri,
|
|
5408
|
+
plan: {
|
|
5409
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
5410
|
+
family: 'code-edition',
|
|
5411
|
+
lane,
|
|
5412
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
5413
|
+
roles: {
|
|
5414
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
5415
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
5416
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
5417
|
+
},
|
|
5418
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
5419
|
+
custody: {
|
|
5420
|
+
onChainUri,
|
|
5421
|
+
imageOnChain: null,
|
|
5422
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
5423
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
5424
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
5425
|
+
renderer: onChainUri ? metadataRenderer : null,
|
|
5426
|
+
image: null,
|
|
5427
|
+
},
|
|
5428
|
+
mint: {
|
|
5429
|
+
deferred: mintCount === 0,
|
|
5430
|
+
count: mintCount,
|
|
5431
|
+
amountPerId: mintAmount.toString(),
|
|
5432
|
+
recipient: mintCount > 0 ? deployerAddr ?? null : null,
|
|
5433
|
+
},
|
|
5434
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
5435
|
+
warnings: planWarnings.slice(),
|
|
5436
|
+
surfaces: null,
|
|
5437
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
5438
|
+
resume: null,
|
|
5439
|
+
},
|
|
5440
|
+
}));
|
|
4610
5441
|
step('Index');
|
|
4611
5442
|
await registerAndIndexLocally(clone, { 'from-block': deployBlock.toString(), factory, label: name });
|
|
4612
5443
|
if (onChainUri) {
|
|
@@ -4634,6 +5465,11 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4634
5465
|
console.log(` ${bold(`abx mint ${clone} --token-id <id> --amount <n>`)} ${dim('# mint more copies of an id')}`);
|
|
4635
5466
|
console.log(` ${bold(`abx minter configure ${clone} --token-id <id> --price <eth> --allocation <n>`)} ${dim('# set up a priced sale for one id')}`);
|
|
4636
5467
|
console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces once metadata is live')}\n`);
|
|
5468
|
+
// Same Arweave-propagation caveat as the 721 twin (see its own comment): the gateway 404s until
|
|
5469
|
+
// Turbo settles the bundle, so a render run right after deploy correctly finds nothing servable yet.
|
|
5470
|
+
if (dirUpload?.backend.id === 'arweave') {
|
|
5471
|
+
console.log(`\n ${c.orange}⚠${c.reset} ${bold('Arweave upload propagates with a delay.')} ${dim('Turbo settles the bundle over minutes (sometimes longer); until then the gateway 404s, so the live view and a render will fail. A render run now correctly reports the content is not servable yet and stores NOTHING (not a garbage 404 thumbnail) — this is expected. Re-run the render once the content is live (the effects service will pick it up on its next sweep).')}`);
|
|
5472
|
+
}
|
|
4637
5473
|
}
|
|
4638
5474
|
/**
|
|
4639
5475
|
* Resolve `--721c` into the InitParams `transferValidator`. Absent → `zeroAddress`: a plain
|