@artblocks/abx-cli 0.1.0-alpha.40 → 0.1.0-alpha.41
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 +145 -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.map +1 -1
- package/dist/commands/deploy.js +731 -65
- 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 +333 -27
- 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 +150 -0
- package/dist/deploy-plan.d.ts.map +1 -0
- package/dist/deploy-plan.js +31 -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 +46 -7
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +69 -0
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +221 -17
- package/dist/ownerops.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 +17 -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 +43 -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, 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');
|
|
@@ -978,6 +998,10 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
978
998
|
};
|
|
979
999
|
let clone;
|
|
980
1000
|
let blockNumber;
|
|
1001
|
+
// The eventual owner/signer — set in whichever lane branch below learns it, so the SENT plan
|
|
1002
|
+
// object (further down) can report `roles` without re-deriving anything. `undefined` only while
|
|
1003
|
+
// no branch has run yet; every path that reaches the sent `emit()` has set it by then.
|
|
1004
|
+
let deployerAddr;
|
|
981
1005
|
if (dryRun) {
|
|
982
1006
|
let deployer;
|
|
983
1007
|
if (flags.for)
|
|
@@ -1035,7 +1059,39 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1035
1059
|
// A dry run DOES know the address when --salt pinned it (a pure function of factory+salt); without
|
|
1036
1060
|
// --salt it's null — reporting the freshly-reserved one would be a real-looking value a script
|
|
1037
1061
|
// could act on that the actual deploy will not land at. `saltPinned` still says why.
|
|
1038
|
-
emit(jsonSafe({
|
|
1062
|
+
emit(jsonSafe({
|
|
1063
|
+
command: 'deploy', dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol,
|
|
1064
|
+
// The structured plan (issue #123) — built from the SAME locals the readout above just
|
|
1065
|
+
// printed; nothing here is re-derived or newly computed. `deployer` is a real wallet address
|
|
1066
|
+
// regardless of salt pinning (only the PREDICTED CONTRACT address carries that caveat, and
|
|
1067
|
+
// this plan doesn't repeat it — see `address`, above), so roles are safe to report unconditionally.
|
|
1068
|
+
plan: {
|
|
1069
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1070
|
+
family: '1of1',
|
|
1071
|
+
lane,
|
|
1072
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging'] : null },
|
|
1073
|
+
// The 1/1 lane's InitParams has no minter/primaryPayee field at all — `null` here means
|
|
1074
|
+
// "no concept on this lane", not "unset".
|
|
1075
|
+
roles: { signer: deployer, owner: deployer, royaltyReceiver: deployer, primaryPayee: null, minter: null },
|
|
1076
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1077
|
+
custody: {
|
|
1078
|
+
onChainUri,
|
|
1079
|
+
imageOnChain: onchainImage,
|
|
1080
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1081
|
+
// `''` (on-chain lane, no explicit --public-base-url) is reported as `null` here — the
|
|
1082
|
+
// human line already spells out WHY (renderer-authoritative); a JSON consumer just needs
|
|
1083
|
+
// "nothing baked", not an empty string to interpret.
|
|
1084
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
1085
|
+
contractUriBase: params.contractURIBase || null,
|
|
1086
|
+
},
|
|
1087
|
+
mint: { deferred: noMint, count: noMint ? 0 : 1, amountPerId: null, recipient: noMint ? null : deployer },
|
|
1088
|
+
estimate: { ethApprox: null, gasApprox: null }, // the 1/1 lane computes no cost estimate
|
|
1089
|
+
warnings: planWarnings.slice(),
|
|
1090
|
+
surfaces: null, // the code lanes' per-surface disposition has no equivalent here
|
|
1091
|
+
dependencies: null, // the 1/1 lane has no dependency-registry concept
|
|
1092
|
+
resume: null,
|
|
1093
|
+
},
|
|
1094
|
+
}));
|
|
1039
1095
|
return;
|
|
1040
1096
|
}
|
|
1041
1097
|
// opt-in --confirm: a final y/N before the real send (no-op without --confirm; never blocks scripts)
|
|
@@ -1044,6 +1100,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1044
1100
|
if (lane === 'send') {
|
|
1045
1101
|
// hot lane: the env key is the deployer, known up front.
|
|
1046
1102
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
1103
|
+
deployerAddr = account.address;
|
|
1047
1104
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(account.address);
|
|
1048
1105
|
info(`deterministic address: ${predicted}`);
|
|
1049
1106
|
info(describeDeployField('description', flags.description, onChainUri || !!flags['description-onchain']));
|
|
@@ -1081,6 +1138,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1081
1138
|
let r;
|
|
1082
1139
|
try {
|
|
1083
1140
|
const signer = await session.connect();
|
|
1141
|
+
deployerAddr = signer;
|
|
1084
1142
|
// Stage on-chain through the session (sets bakedImage, which buildForDeployer reads below).
|
|
1085
1143
|
const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sessionStagingSender(session));
|
|
1086
1144
|
bakedImage = field;
|
|
@@ -1113,6 +1171,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1113
1171
|
let r;
|
|
1114
1172
|
try {
|
|
1115
1173
|
const signer = await session.connect();
|
|
1174
|
+
deployerAddr = signer;
|
|
1116
1175
|
// Route Turbo uploads' signatures to this session before building content (which uploads).
|
|
1117
1176
|
uploadRemoteEth = { address: signer, signMessage: (m) => session.signMessage(m, 'Sign Arweave upload (paid from your Turbo credits)') };
|
|
1118
1177
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(signer);
|
|
@@ -1137,6 +1196,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1137
1196
|
? 'a wallet will become the owner; the token resolves from chain — no URI base is baked in.'
|
|
1138
1197
|
: `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
1139
1198
|
const result = await signTx(async (signer) => {
|
|
1199
|
+
deployerAddr = signer;
|
|
1140
1200
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
1141
1201
|
return prepareDeployOneOfOne({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
1142
1202
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -1150,7 +1210,32 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1150
1210
|
}
|
|
1151
1211
|
// Emitted the moment the address is known, and again below once minting is settled — so a crash in
|
|
1152
1212
|
// the indexing steps that follow still yields the address of a contract that really does exist.
|
|
1153
|
-
emit(jsonSafe({
|
|
1213
|
+
emit(jsonSafe({
|
|
1214
|
+
command: 'deploy', address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
1215
|
+
plan: {
|
|
1216
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1217
|
+
family: '1of1',
|
|
1218
|
+
lane,
|
|
1219
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging'] : null },
|
|
1220
|
+
roles: { signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null, primaryPayee: null, minter: null },
|
|
1221
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1222
|
+
custody: {
|
|
1223
|
+
onChainUri,
|
|
1224
|
+
imageOnChain: onchainImage,
|
|
1225
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1226
|
+
// Recomputed from the same top-level locals `buildForDeployer`'s `params` used (no branch
|
|
1227
|
+
// carries `params` out to this scope) — never a new decision, the identical ternary.
|
|
1228
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
1229
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
1230
|
+
},
|
|
1231
|
+
mint: { deferred: noMint, count: noMint ? 0 : 1, amountPerId: null, recipient: noMint ? null : deployerAddr ?? null },
|
|
1232
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
1233
|
+
warnings: planWarnings.slice(),
|
|
1234
|
+
surfaces: null,
|
|
1235
|
+
dependencies: null,
|
|
1236
|
+
resume: null,
|
|
1237
|
+
},
|
|
1238
|
+
}));
|
|
1154
1239
|
step(serveAfter ? 'What the chain knows now' : 'Index it — replay the event spine from chain');
|
|
1155
1240
|
const indexer = localIndexer();
|
|
1156
1241
|
// Off-chain traits ride in the registration (on-chain ones are already in the contract fields).
|
|
@@ -1284,6 +1369,7 @@ export async function cmdDeployBody(flags, serveAfter, emit) {
|
|
|
1284
1369
|
// spreads `DEPLOY_FLAGS`, so it must come AFTER that declaration at module scope, same reason
|
|
1285
1370
|
// `DEPLOY_SERIES_EDITION_FLAGS` sits next to it rather than beside `cmdDeployEditionImageBody`.)
|
|
1286
1371
|
export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
1372
|
+
beginPlanWarnings();
|
|
1287
1373
|
refuseStrayFlags(flags, DEPLOY_EDITION_FLAGS, 'deploy');
|
|
1288
1374
|
warnSignWithoutFor(flags);
|
|
1289
1375
|
// `--type` is the 721 lane's OWN dimension seam (cmdDeployBody redirects `--type series` to
|
|
@@ -1487,6 +1573,8 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1487
1573
|
};
|
|
1488
1574
|
let clone;
|
|
1489
1575
|
let blockNumber;
|
|
1576
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`).
|
|
1577
|
+
let deployerAddr;
|
|
1490
1578
|
if (dryRun) {
|
|
1491
1579
|
let deployer;
|
|
1492
1580
|
if (flags.for)
|
|
@@ -1531,13 +1619,41 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1531
1619
|
}
|
|
1532
1620
|
info(dim('the values above are exactly what a real deploy writes — nothing else is added.'));
|
|
1533
1621
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
|
|
1534
|
-
emit(jsonSafe({
|
|
1622
|
+
emit(jsonSafe({
|
|
1623
|
+
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,
|
|
1624
|
+
plan: {
|
|
1625
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1626
|
+
family: '1of1-edition',
|
|
1627
|
+
lane,
|
|
1628
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging'] : null },
|
|
1629
|
+
roles: {
|
|
1630
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
1631
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
1632
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
1633
|
+
},
|
|
1634
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1635
|
+
custody: {
|
|
1636
|
+
onChainUri,
|
|
1637
|
+
imageOnChain: onchainImage,
|
|
1638
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1639
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
1640
|
+
contractUriBase: params.contractURIBase || null,
|
|
1641
|
+
},
|
|
1642
|
+
mint: { deferred: mintAmount === 0n, count: mintAmount > 0n ? 1 : 0, amountPerId: mintAmount.toString(), recipient: mintAmount > 0n ? deployer : null },
|
|
1643
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
1644
|
+
warnings: planWarnings.slice(),
|
|
1645
|
+
surfaces: null,
|
|
1646
|
+
dependencies: null,
|
|
1647
|
+
resume: null,
|
|
1648
|
+
},
|
|
1649
|
+
}));
|
|
1535
1650
|
return;
|
|
1536
1651
|
}
|
|
1537
1652
|
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
1653
|
(transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
1539
1654
|
if (lane === 'send') {
|
|
1540
1655
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
1656
|
+
deployerAddr = account.address;
|
|
1541
1657
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(account.address);
|
|
1542
1658
|
info(`deterministic address: ${predicted}`);
|
|
1543
1659
|
info(describeDeployField('description', flags.description, onChainUri || !!flags['description-onchain']));
|
|
@@ -1568,6 +1684,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1568
1684
|
let r;
|
|
1569
1685
|
try {
|
|
1570
1686
|
const signer = await session.connect();
|
|
1687
|
+
deployerAddr = signer;
|
|
1571
1688
|
await stageOnChainImage(sessionStagingSender(session)); // sets bakedImage, read by buildForDeployer
|
|
1572
1689
|
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(signer);
|
|
1573
1690
|
info(contentNote);
|
|
@@ -1587,6 +1704,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1587
1704
|
// wallet lane without staging, or the cold lane: a single deploy tx.
|
|
1588
1705
|
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
1706
|
const result = await signTx(async (signer) => {
|
|
1707
|
+
deployerAddr = signer;
|
|
1590
1708
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
1591
1709
|
return prepareDeployOneOfOneEdition({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
1592
1710
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -1598,7 +1716,34 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1598
1716
|
blockNumber = result.blockNumber;
|
|
1599
1717
|
ok(`deployed ${clone}`);
|
|
1600
1718
|
}
|
|
1601
|
-
emit(jsonSafe({
|
|
1719
|
+
emit(jsonSafe({
|
|
1720
|
+
command: 'deploy', kind: '1of1-edition', copies: editionSize.toString(), address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
1721
|
+
plan: {
|
|
1722
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
1723
|
+
family: '1of1-edition',
|
|
1724
|
+
lane,
|
|
1725
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging'] : null },
|
|
1726
|
+
roles: {
|
|
1727
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
1728
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
1729
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
1730
|
+
},
|
|
1731
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
1732
|
+
custody: {
|
|
1733
|
+
onChainUri,
|
|
1734
|
+
imageOnChain: onchainImage,
|
|
1735
|
+
backend: !onChainUri && !onchainImage ? backendResolution(storageOverrides(flags)).backend : null,
|
|
1736
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
1737
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
1738
|
+
},
|
|
1739
|
+
mint: { deferred: mintAmount === 0n, count: mintAmount > 0n ? 1 : 0, amountPerId: mintAmount.toString(), recipient: mintAmount > 0n ? deployerAddr ?? null : null },
|
|
1740
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
1741
|
+
warnings: planWarnings.slice(),
|
|
1742
|
+
surfaces: null,
|
|
1743
|
+
dependencies: null,
|
|
1744
|
+
resume: null,
|
|
1745
|
+
},
|
|
1746
|
+
}));
|
|
1602
1747
|
step('Index it — replay the event spine from chain');
|
|
1603
1748
|
const indexer = localIndexer();
|
|
1604
1749
|
const offChainTraits = !traitsOnchain && traits.length ? JSON.stringify(traits) : undefined;
|
|
@@ -1668,6 +1813,7 @@ export async function cmdDeploySeries(flags) {
|
|
|
1668
1813
|
return withJson(flags, async (emit) => cmdDeploySeriesBody(flags, emit));
|
|
1669
1814
|
}
|
|
1670
1815
|
export async function cmdDeploySeriesBody(flags, emit) {
|
|
1816
|
+
beginPlanWarnings();
|
|
1671
1817
|
refuseStrayFlags(flags, DEPLOY_SERIES_FLAGS, 'deploy-series');
|
|
1672
1818
|
warnSignWithoutFor(flags);
|
|
1673
1819
|
const name = flags.name ?? 'ABX Series';
|
|
@@ -2070,14 +2216,46 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2070
2216
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored.')}\n`);
|
|
2071
2217
|
// Without --salt, `address` would be a freshly-reserved value a real deploy will NOT land at —
|
|
2072
2218
|
// report null rather than a real-looking value a script could wrongly act on.
|
|
2073
|
-
emit(jsonSafe({
|
|
2219
|
+
emit(jsonSafe({
|
|
2220
|
+
command: 'deploy-series', dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol,
|
|
2221
|
+
plan: {
|
|
2222
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2223
|
+
family: 'series',
|
|
2224
|
+
lane,
|
|
2225
|
+
// A staged on-chain collection is many chunk-write txs, one per file — that per-token detail
|
|
2226
|
+
// is printed above (`previewImageStaging`), not re-derived here; `legs` just names the shape.
|
|
2227
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per token)'] : null },
|
|
2228
|
+
roles: {
|
|
2229
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
2230
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2231
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2232
|
+
},
|
|
2233
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2234
|
+
custody: {
|
|
2235
|
+
onChainUri,
|
|
2236
|
+
imageOnChain: onchainImage,
|
|
2237
|
+
backend: offchainImageOnchainJson || !onChainUri ? backendId : null,
|
|
2238
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
2239
|
+
contractUriBase: params.contractURIBase || null,
|
|
2240
|
+
},
|
|
2241
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployer : null },
|
|
2242
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2243
|
+
warnings: planWarnings.slice(),
|
|
2244
|
+
surfaces: null,
|
|
2245
|
+
dependencies: null,
|
|
2246
|
+
resume: null,
|
|
2247
|
+
},
|
|
2248
|
+
}));
|
|
2074
2249
|
return;
|
|
2075
2250
|
}
|
|
2076
2251
|
let clone;
|
|
2077
2252
|
let blockNumber;
|
|
2253
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`).
|
|
2254
|
+
let deployerAddr;
|
|
2078
2255
|
if (lane === 'send') {
|
|
2079
2256
|
// hot lane: the env key stages every token up front (deployer-independent), then deploys.
|
|
2080
2257
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
2258
|
+
deployerAddr = account.address;
|
|
2081
2259
|
if (onchainImage)
|
|
2082
2260
|
await buildFields(envStagingSender());
|
|
2083
2261
|
const { clone: predicted, params, salt } = await buildForDeployer(account.address);
|
|
@@ -2104,6 +2282,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2104
2282
|
});
|
|
2105
2283
|
try {
|
|
2106
2284
|
const signer = await session.connect();
|
|
2285
|
+
deployerAddr = signer;
|
|
2107
2286
|
if (onchainImage)
|
|
2108
2287
|
await buildFields(sessionStagingSender(session));
|
|
2109
2288
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
@@ -2129,6 +2308,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2129
2308
|
});
|
|
2130
2309
|
try {
|
|
2131
2310
|
const signer = await session.connect();
|
|
2311
|
+
deployerAddr = signer;
|
|
2132
2312
|
// Route Turbo uploads through the wallet (mutate the SAME opts.arweave the backend captured).
|
|
2133
2313
|
if (opts.arweave)
|
|
2134
2314
|
opts.arweave.remoteEth = { address: signer, signMessage: (m) => session.signMessage(m, 'Sign Arweave upload (paid from your Turbo credits)') };
|
|
@@ -2151,6 +2331,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2151
2331
|
? 'a wallet will become the owner; tokens resolve from chain — no URI base is baked in.'
|
|
2152
2332
|
: `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
2153
2333
|
const result = await signTx(async (signer) => {
|
|
2334
|
+
deployerAddr = signer;
|
|
2154
2335
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2155
2336
|
return prepareDeploySeries({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
2156
2337
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -2168,7 +2349,34 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2168
2349
|
info(`permanent: size ${count} · royalty ${royaltyBps / 100}%${flags['royalty-bps'] === undefined ? ' ⚠ default' : ''} → the deploying wallet` +
|
|
2169
2350
|
` · cap ${maxRoyaltyBps / 100}%${burnable ? ' · burnable' : ''}` +
|
|
2170
2351
|
dim(' (rate is changeable with `abx set-royalty`; the CAP only ever goes down)'));
|
|
2171
|
-
emit(jsonSafe({
|
|
2352
|
+
emit(jsonSafe({
|
|
2353
|
+
command: 'deploy-series', address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
2354
|
+
plan: {
|
|
2355
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2356
|
+
family: 'series',
|
|
2357
|
+
lane,
|
|
2358
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per token)'] : null },
|
|
2359
|
+
roles: {
|
|
2360
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
2361
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2362
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2363
|
+
},
|
|
2364
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2365
|
+
custody: {
|
|
2366
|
+
onChainUri,
|
|
2367
|
+
imageOnChain: onchainImage,
|
|
2368
|
+
backend: offchainImageOnchainJson || !onChainUri ? backendId : null,
|
|
2369
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
2370
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
2371
|
+
},
|
|
2372
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployerAddr ?? null : null },
|
|
2373
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2374
|
+
warnings: planWarnings.slice(),
|
|
2375
|
+
surfaces: null,
|
|
2376
|
+
dependencies: null,
|
|
2377
|
+
resume: null,
|
|
2378
|
+
},
|
|
2379
|
+
}));
|
|
2172
2380
|
step('Index it — replay the event spine from chain');
|
|
2173
2381
|
const indexer = localIndexer();
|
|
2174
2382
|
const baseReg = {
|
|
@@ -2244,6 +2452,7 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
2244
2452
|
// (`DEPLOY_SERIES_EDITION_FLAGS` is declared further down, beside `DEPLOY_EDITION_FLAGS` — see that
|
|
2245
2453
|
// const's own note on why.)
|
|
2246
2454
|
export async function cmdDeployEditionImageBody(flags, emit) {
|
|
2455
|
+
beginPlanWarnings();
|
|
2247
2456
|
refuseStrayFlags(flags, DEPLOY_SERIES_EDITION_FLAGS, 'deploy-series');
|
|
2248
2457
|
warnSignWithoutFor(flags);
|
|
2249
2458
|
const editionSize = parseCopies(flags.copies);
|
|
@@ -2563,6 +2772,8 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2563
2772
|
};
|
|
2564
2773
|
let clone;
|
|
2565
2774
|
let blockNumber;
|
|
2775
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`).
|
|
2776
|
+
let deployerAddr;
|
|
2566
2777
|
if (dryRun) {
|
|
2567
2778
|
let deployer;
|
|
2568
2779
|
if (flags.for)
|
|
@@ -2602,11 +2813,44 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2602
2813
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
|
|
2603
2814
|
}
|
|
2604
2815
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
|
|
2605
|
-
emit(jsonSafe({
|
|
2816
|
+
emit(jsonSafe({
|
|
2817
|
+
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,
|
|
2818
|
+
plan: {
|
|
2819
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2820
|
+
family: 'series-edition',
|
|
2821
|
+
lane,
|
|
2822
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per id)'] : null },
|
|
2823
|
+
roles: {
|
|
2824
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
2825
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2826
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2827
|
+
},
|
|
2828
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2829
|
+
custody: {
|
|
2830
|
+
onChainUri,
|
|
2831
|
+
imageOnChain: onchainImage,
|
|
2832
|
+
backend: !onchainImage && (offchainImageOnchainJson || !onChainUri) ? backendId : null,
|
|
2833
|
+
tokenUriBase: params.tokenURIBase || null,
|
|
2834
|
+
contractUriBase: params.contractURIBase || null,
|
|
2835
|
+
},
|
|
2836
|
+
mint: {
|
|
2837
|
+
deferred: effectiveMintCount === 0,
|
|
2838
|
+
count: effectiveMintCount,
|
|
2839
|
+
amountPerId: mintAmount.toString(),
|
|
2840
|
+
recipient: effectiveMintCount > 0 ? deployer : null,
|
|
2841
|
+
},
|
|
2842
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2843
|
+
warnings: planWarnings.slice(),
|
|
2844
|
+
surfaces: null,
|
|
2845
|
+
dependencies: null,
|
|
2846
|
+
resume: null,
|
|
2847
|
+
},
|
|
2848
|
+
}));
|
|
2606
2849
|
return;
|
|
2607
2850
|
}
|
|
2608
2851
|
if (lane === 'send') {
|
|
2609
2852
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
2853
|
+
deployerAddr = account.address;
|
|
2610
2854
|
// hot lane: the env key stages every id up front (deployer-independent), then deploys.
|
|
2611
2855
|
if (onchainImage)
|
|
2612
2856
|
await buildFields(envStagingSender());
|
|
@@ -2635,6 +2879,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2635
2879
|
let r;
|
|
2636
2880
|
try {
|
|
2637
2881
|
const signer = await session.connect();
|
|
2882
|
+
deployerAddr = signer;
|
|
2638
2883
|
await buildFields(sessionStagingSender(session));
|
|
2639
2884
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2640
2885
|
info(`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${signer} at deploy` : 'deferred'}`);
|
|
@@ -2652,6 +2897,7 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2652
2897
|
else {
|
|
2653
2898
|
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
2899
|
const result = await signTx(async (signer) => {
|
|
2900
|
+
deployerAddr = signer;
|
|
2655
2901
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2656
2902
|
return prepareDeployEditionImage({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
2657
2903
|
}, { lane, chainKey: CHAIN, yes: !!flags.yes, expectedSigner: flags.for, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
@@ -2663,7 +2909,39 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2663
2909
|
blockNumber = result.blockNumber;
|
|
2664
2910
|
ok(`deployed ${clone}`);
|
|
2665
2911
|
}
|
|
2666
|
-
emit(jsonSafe({
|
|
2912
|
+
emit(jsonSafe({
|
|
2913
|
+
command: 'deploy-series', kind: 'edition', copies: editionSize.toString(), ids: count, address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol,
|
|
2914
|
+
plan: {
|
|
2915
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
2916
|
+
family: 'series-edition',
|
|
2917
|
+
lane,
|
|
2918
|
+
transactions: { approvals, legs: onchainImage ? ['onchain-image-staging (per id)'] : null },
|
|
2919
|
+
roles: {
|
|
2920
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
2921
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
2922
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
2923
|
+
},
|
|
2924
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
2925
|
+
custody: {
|
|
2926
|
+
onChainUri,
|
|
2927
|
+
imageOnChain: onchainImage,
|
|
2928
|
+
backend: !onchainImage && (offchainImageOnchainJson || !onChainUri) ? backendId : null,
|
|
2929
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
2930
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
2931
|
+
},
|
|
2932
|
+
mint: {
|
|
2933
|
+
deferred: effectiveMintCount === 0,
|
|
2934
|
+
count: effectiveMintCount,
|
|
2935
|
+
amountPerId: mintAmount.toString(),
|
|
2936
|
+
recipient: effectiveMintCount > 0 ? deployerAddr ?? null : null,
|
|
2937
|
+
},
|
|
2938
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
2939
|
+
warnings: planWarnings.slice(),
|
|
2940
|
+
surfaces: null,
|
|
2941
|
+
dependencies: null,
|
|
2942
|
+
resume: null,
|
|
2943
|
+
},
|
|
2944
|
+
}));
|
|
2667
2945
|
step('Index it — replay the event spine from chain');
|
|
2668
2946
|
const indexer = localIndexer();
|
|
2669
2947
|
const offChainTraits = !seriesTraitsOnchain && offChainTokenTraits ? offChainTokenTraits : undefined;
|
|
@@ -2919,6 +3197,10 @@ export const DEPLOY_CODE_FLAGS = new Set([
|
|
|
2919
3197
|
'dry-run', 'confirm', 'yes',
|
|
2920
3198
|
'json', // the deployed address as data, narration to stderr
|
|
2921
3199
|
'resume', // finish an existing contract whose setup transaction never landed (no deploy)
|
|
3200
|
+
// Meaningful ONLY on `--resume` against an EditionCode target (per-id copies for the premint
|
|
3201
|
+
// shortfall) — kept in the allowlist (not stray) so a misuse elsewhere gets this function's own
|
|
3202
|
+
// pointed refusal instead of the generic "unrecognized flag". See the --resume block.
|
|
3203
|
+
'mint-amount',
|
|
2922
3204
|
]);
|
|
2923
3205
|
// Storage + signing + preview flags shared by every deploy path (mirrors storageOverrides() + the lanes).
|
|
2924
3206
|
export const SHARED_DEPLOY_FLAGS = [
|
|
@@ -2981,6 +3263,7 @@ export async function cmdDeployCode(flags) {
|
|
|
2981
3263
|
return withJson(flags, async (emit) => cmdDeployCodeBody(flags, emit));
|
|
2982
3264
|
}
|
|
2983
3265
|
export async function cmdDeployCodeBody(flags, emit) {
|
|
3266
|
+
beginPlanWarnings();
|
|
2984
3267
|
const usage = 'abx deploy-code (--script <file> | --code-dir <dir>) --name "Title" --symbol SYM ' +
|
|
2985
3268
|
'(--public-base-url https://your.resolver.domain | --onchain-uri) ' +
|
|
2986
3269
|
'[--description "<s>"] [--external-url <url>] [--image-base <url> | --image-renderer 0x..] [--attributes-renderer 0x..] ' +
|
|
@@ -3004,6 +3287,16 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3004
3287
|
// Shared path so the "did you mean --max / price is post-deploy" hints fire here too.
|
|
3005
3288
|
refuseStrayFlags(flags, DEPLOY_CODE_FLAGS, 'deploy-code');
|
|
3006
3289
|
warnSignWithoutFor(flags);
|
|
3290
|
+
// --mint-amount only means anything on a --resume against an EXISTING EditionCode target (a per-id
|
|
3291
|
+
// premint shortfall needs an amount; a fresh 721 deploy has no such field, and a 721 resume's mint
|
|
3292
|
+
// leg is a single whole-contract shortfall with no amount either). Checked HERE, before the dry-run
|
|
3293
|
+
// preview branch below returns early for a fresh deploy — a --resume against an actual EditionCode
|
|
3294
|
+
// (where the flag DOES apply) gets the more specific 721-vs-edition guard further down instead.
|
|
3295
|
+
if (flags['mint-amount'] !== undefined && flags.resume === undefined) {
|
|
3296
|
+
throw new Error('--mint-amount only applies to `abx deploy-code --resume <address>` against an EXISTING EditionCode contract — ' +
|
|
3297
|
+
'a fresh 721 deploy (this command, without --copies) has no per-id mint amount. For a NEW edition deploy, use ' +
|
|
3298
|
+
'`abx deploy-code --copies <n|open> --mint-amount <n>`.');
|
|
3299
|
+
}
|
|
3007
3300
|
const dryRun = isDryRun(flags);
|
|
3008
3301
|
const name = flags.name ?? 'ABX Code';
|
|
3009
3302
|
const symbol = flags.symbol ?? 'ABXC';
|
|
@@ -3458,6 +3751,10 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3458
3751
|
// PostParams it reads (unlike the JS lane's static paramHints), so nudge — the palette-miss.
|
|
3459
3752
|
? `⚠ 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
3753
|
: 'none declared';
|
|
3754
|
+
// Hoisted here (rather than beside its one printed use, in the Surfaces block below) so BOTH the
|
|
3755
|
+
// dry-run's Surfaces block and the plan object's `surfaces.postParams.ok` (populated at every one
|
|
3756
|
+
// of this lane's emit sites, dry-run/sent/resume alike) read the exact same verdict.
|
|
3757
|
+
const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
|
|
3461
3758
|
// TOKENURI PUBLIC-READ GAS — a large --onchain-uri document assembles on-chain per call; an
|
|
3462
3759
|
// unauthenticated public read (Etherscan "Read Contract" with no wallet connected) can hit a client
|
|
3463
3760
|
// gas cap and appear to REVERT. Expected, not a bug — a real session mis-diagnosed it as indexing lag.
|
|
@@ -3494,6 +3791,18 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3494
3791
|
const depLegs = deps.length + (depRegistry ? 1 : 0);
|
|
3495
3792
|
const setupLen = scriptChunks.length + schemas.length + depLegs + (onchainUriLegs?.length ?? 0) + setupMintCount;
|
|
3496
3793
|
const approvals = 1 + (setupLen > 0 ? 1 : 0); // deploy tx + (a setup multicall only if something rides it)
|
|
3794
|
+
// Ordered legs riding that setup multicall (if any), for the plan object's `transactions.legs` —
|
|
3795
|
+
// the SAME booleans `setupLen`/`setupBits` (the dry-run's own `transactions:` line, below) already
|
|
3796
|
+
// use, so this can't drift from the tx the wallet actually signs.
|
|
3797
|
+
const planSetupLegs = setupLen > 0
|
|
3798
|
+
? [
|
|
3799
|
+
scriptChunks.length ? 'chunks' : null,
|
|
3800
|
+
schemas.length ? 'param-schemas' : null,
|
|
3801
|
+
depLegs ? 'dependencies' : null,
|
|
3802
|
+
onchainUriLegs ? 'onchain-uri' : null,
|
|
3803
|
+
setupMintCount ? 'mints' : null,
|
|
3804
|
+
].filter((leg) => leg !== null)
|
|
3805
|
+
: null;
|
|
3497
3806
|
// opt-in --confirm: one y/N before ANY upload or send (no-op without --confirm; never blocks scripts).
|
|
3498
3807
|
if (!dryRun) {
|
|
3499
3808
|
const renderHome = process.env.ABX_STORAGE_BACKEND || 'fs'; // empty string counts as unset (=fs)
|
|
@@ -3616,6 +3925,9 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3616
3925
|
// RPC can't price gas; see cli-ux-decisions). On-chain byte storage dominates a template drop,
|
|
3617
3926
|
// so apply the skill's own ~200 gas/byte heuristic to the stored bytes + coarse per-op costs,
|
|
3618
3927
|
// priced at the live gas price. Clearly hedged — it's an order-of-magnitude figure, not a quote.
|
|
3928
|
+
// `planEstimate` carries the SAME figures into the plan object below — `null` (not computed)
|
|
3929
|
+
// when the RPC can't price gas, exactly mirroring the human line's own silent skip.
|
|
3930
|
+
let planEstimate = null;
|
|
3619
3931
|
try {
|
|
3620
3932
|
const chunkBytes = scriptChunks.reduce((n, c) => n + (c.length - 2) / 2, 0);
|
|
3621
3933
|
const fieldBytes = contractFields.reduce((n, f) => n + (f.value.length - 2) / 2, 0);
|
|
@@ -3632,6 +3944,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3632
3944
|
const ethStr = eth >= 0.00001 ? eth.toFixed(5) : '<0.00001';
|
|
3633
3945
|
info(`est. on-chain cost: ${dim('~')}${ethStr} ETH ` +
|
|
3634
3946
|
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' : ''})`));
|
|
3947
|
+
planEstimate = { ethApprox: ethStr, gasApprox: roughGas.toString() };
|
|
3635
3948
|
}
|
|
3636
3949
|
catch { /* gas pricing unavailable — cost guidance is a bonus, skip silently */ }
|
|
3637
3950
|
// ── Surfaces — every marketplace-facing dimension, resolved NOW (none is backfillable) ──────
|
|
@@ -3675,8 +3988,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3675
3988
|
info(` ${dim('render storage home:')} ${bold(renderHome)} ${dim('— a public home the runner uploads to ✓')}`);
|
|
3676
3989
|
}
|
|
3677
3990
|
}
|
|
3678
|
-
(traitsBroken ? warn : info)(`traits: ${traitsDisposition}`);
|
|
3679
|
-
const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
|
|
3991
|
+
(traitsBroken ? warn : info)(`traits: ${traitsDisposition}`); // `paramsNudge` is hoisted above (with `paramsDisposition`) — shared with the plan object
|
|
3680
3992
|
((undeclaredParams.length || paramsNudge ? warn : info))(`postparams: ${paramsDisposition}`);
|
|
3681
3993
|
if (anySurfaceBroken) {
|
|
3682
3994
|
const remedies = [
|
|
@@ -3769,7 +4081,43 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3769
4081
|
// `address` is null without --salt pinning it or in the wallet lane with no --for (the
|
|
3770
4082
|
// signer decides the salt, so no address exists yet) — reporting null is the honest answer;
|
|
3771
4083
|
// inventing one would be a wrong reservation.
|
|
3772
|
-
emit(jsonSafe({
|
|
4084
|
+
emit(jsonSafe({
|
|
4085
|
+
command: 'deploy-code', dryRun: true, sent: false, address: shownAddr, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt: salt ?? null, saltPinned: !!explicitSalt, name, symbol, onChainUri,
|
|
4086
|
+
plan: {
|
|
4087
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
4088
|
+
family: 'code',
|
|
4089
|
+
// `lane` (the hoisted const) isn't declared until AFTER this early-return block — reading it
|
|
4090
|
+
// here would hit its temporal dead zone. `laneFromFlags` is pure (flags only), so call it
|
|
4091
|
+
// directly; the sent-side emit, below `const lane = …`, uses the hoisted binding instead.
|
|
4092
|
+
lane: laneFromFlags(flags),
|
|
4093
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
4094
|
+
roles: {
|
|
4095
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
4096
|
+
primaryPayee: flags['primary-payee'] ?? null,
|
|
4097
|
+
minter: flags.minter ?? null,
|
|
4098
|
+
},
|
|
4099
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
4100
|
+
custody: {
|
|
4101
|
+
onChainUri,
|
|
4102
|
+
// The code lane's image is one of several independently-disposed surfaces, not a single
|
|
4103
|
+
// boolean — see `surfaces.thumbnail` below.
|
|
4104
|
+
imageOnChain: null,
|
|
4105
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
4106
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
4107
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
4108
|
+
},
|
|
4109
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployer : null },
|
|
4110
|
+
estimate: planEstimate ?? { ethApprox: null, gasApprox: null },
|
|
4111
|
+
warnings: planWarnings.slice(),
|
|
4112
|
+
surfaces: {
|
|
4113
|
+
thumbnail: { ok: !imageOrphaned, detail: stripAnsiForPlan(imageDisposition) },
|
|
4114
|
+
traits: { ok: !traitsBroken, detail: stripAnsiForPlan(traitsDisposition) },
|
|
4115
|
+
postParams: { ok: !(undeclaredParams.length > 0 || paramsNudge), detail: stripAnsiForPlan(paramsDisposition) },
|
|
4116
|
+
},
|
|
4117
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
4118
|
+
resume: null,
|
|
4119
|
+
},
|
|
4120
|
+
}));
|
|
3773
4121
|
return;
|
|
3774
4122
|
}
|
|
3775
4123
|
// Real deploy: perform the deferred directory upload now (past the confirm gate).
|
|
@@ -3894,36 +4242,39 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3894
4242
|
if (flags[flag] !== undefined)
|
|
3895
4243
|
throw new Error(`--resume cannot be combined with --${flag}: ${why}.`);
|
|
3896
4244
|
}
|
|
3897
|
-
//
|
|
3898
|
-
//
|
|
3899
|
-
// `totalSupply()
|
|
3900
|
-
// no whole-contract total — see tokens.ts's
|
|
3901
|
-
//
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
`
|
|
3907
|
-
|
|
3908
|
-
|
|
4245
|
+
// 721 SeriesCode vs EditionCode: same setup shape (script chunks, schemas, deps, on-chain URI
|
|
4246
|
+
// legs), different mint shape — a 721 mints ONE sequential token against a whole-contract
|
|
4247
|
+
// `totalSupply()`; an edition mints AMOUNT copies of a specific id against that id's own
|
|
4248
|
+
// `totalSupply(id)` (there is no whole-contract total on an edition — see tokens.ts's
|
|
4249
|
+
// `TokenListing.totalSupply` doc). `resume.ts` models both (`planResume`/`planEditionResume`,
|
|
4250
|
+
// sharing the four non-mint legs via `planCoreLegs`); this is the one place that decides which.
|
|
4251
|
+
const isEdition = await isEditionContract(publicClient, target);
|
|
4252
|
+
if (!isEdition && flags['mint-amount'] !== undefined) {
|
|
4253
|
+
throw new Error(`--resume ${target}: this is a 721 SeriesCode contract, so --mint-amount has nothing to address — its mint leg ` +
|
|
4254
|
+
`is a single whole-contract shortfall with no per-id amount. Drop --mint-amount (use --mint-count).`);
|
|
4255
|
+
}
|
|
4256
|
+
const abi = isEdition ? editionCodeAbi : seriesCodeAbi;
|
|
3909
4257
|
const owner = await publicClient
|
|
3910
|
-
.readContract({ address: target, abi
|
|
4258
|
+
.readContract({ address: target, abi, functionName: 'owner' })
|
|
3911
4259
|
.catch(() => undefined);
|
|
3912
4260
|
if (!owner)
|
|
3913
|
-
throw new Error(`--resume ${target}: could not read owner() — is this an ABX SeriesCode contract?`);
|
|
4261
|
+
throw new Error(`--resume ${target}: could not read owner() — is this an ABX SeriesCode/EditionCode contract?`);
|
|
3914
4262
|
step('Resume — read what is missing, send only that');
|
|
3915
|
-
info(`target ${bold(target)} ${dim(`· owner ${owner}`)}`);
|
|
4263
|
+
info(`target ${bold(target)} ${dim(`· owner ${owner}${isEdition ? ' · EditionCode (ERC-1155)' : ''}`)}`);
|
|
3916
4264
|
// 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.
|
|
4265
|
+
// uses, so what gets sent is by construction what the deploy would have written. Only the four
|
|
4266
|
+
// non-mint groups are used on the edition branch below — `legs.mints` is the 721 shape and is
|
|
4267
|
+
// rebuilt per-id there instead (see resume.ts's class doc for why the split).
|
|
3918
4268
|
const legs = setupLegGroups(owner);
|
|
3919
|
-
//
|
|
3920
|
-
//
|
|
4269
|
+
// The contract is `T | null` (the planner distinguishes "read failed" from a legitimately-falsy
|
|
4270
|
+
// result); tryReadContract's is `T | undefined` — adapt at the boundary.
|
|
3921
4271
|
const readAt = async (functionName, args = []) => {
|
|
3922
|
-
const result = await tryReadContract(publicClient, { address: target, abi
|
|
4272
|
+
const result = await tryReadContract(publicClient, { address: target, abi, functionName, args });
|
|
3923
4273
|
return result === undefined ? null : result;
|
|
3924
4274
|
};
|
|
3925
4275
|
const nonZero = (a) => (a && a !== zeroAddress ? a : null);
|
|
3926
|
-
|
|
4276
|
+
// The four id-agnostic reads, shared verbatim by both standards (see resume.ts's `ResumeReaderCore`).
|
|
4277
|
+
const coreReads = {
|
|
3927
4278
|
scriptChunkCount: async () => Number((await readAt('scriptChunkCount')) ?? 0n),
|
|
3928
4279
|
scriptChunk: (index) => readAt('scriptChunk', [BigInt(index)]),
|
|
3929
4280
|
schemaExists: async (key) => {
|
|
@@ -3938,16 +4289,78 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3938
4289
|
const r = await readAt('contractField', [encodeTag(field)]);
|
|
3939
4290
|
return !!r && r[0] !== `0x${'0'.repeat(64)}`;
|
|
3940
4291
|
},
|
|
3941
|
-
totalSupply: async () => Number((await readAt('totalSupply')) ?? 0n),
|
|
3942
4292
|
};
|
|
3943
|
-
const plan =
|
|
4293
|
+
const plan = isEdition
|
|
4294
|
+
? await (async () => {
|
|
4295
|
+
// `--mint-count`/`--mint-amount` are the SAME flags a fresh `--copies` deploy reads (see
|
|
4296
|
+
// cmdDeployEditionCodeBody) — so re-running the original command with `--copies` swapped
|
|
4297
|
+
// for `--resume <address>` reproduces the identical intended mint plan: ids 0..mintCount-1,
|
|
4298
|
+
// each an intended TOTAL of mintAmount copies.
|
|
4299
|
+
const mintAmount = flags['mint-amount'] !== undefined ? parseNonNegativeIntFlag(String(flags['mint-amount']), 'mint-amount') : 1n;
|
|
4300
|
+
if (mintCount > 0 && mintAmount === 0n) {
|
|
4301
|
+
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.');
|
|
4302
|
+
}
|
|
4303
|
+
const editionLegs = {
|
|
4304
|
+
chunks: legs.chunks,
|
|
4305
|
+
schemas: legs.schemas,
|
|
4306
|
+
deps: legs.deps,
|
|
4307
|
+
uri: legs.uri,
|
|
4308
|
+
mints: Array.from({ length: mintCount }, (_, id) => ({
|
|
4309
|
+
id: BigInt(id),
|
|
4310
|
+
intendedAmount: mintAmount,
|
|
4311
|
+
dataForAmount: (amount) => encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'mint', args: [owner, BigInt(id), amount] }),
|
|
4312
|
+
})),
|
|
4313
|
+
};
|
|
4314
|
+
const reader = {
|
|
4315
|
+
...coreReads,
|
|
4316
|
+
totalSupplyForId: async (id) => Number((await readAt('totalSupply', [id])) ?? 0n),
|
|
4317
|
+
};
|
|
4318
|
+
return planEditionResume(reader, editionLegs);
|
|
4319
|
+
})()
|
|
4320
|
+
: await planResume({ ...coreReads, totalSupply: async () => Number((await readAt('totalSupply')) ?? 0n) }, legs);
|
|
3944
4321
|
for (const line of plan.done)
|
|
3945
4322
|
ok(line);
|
|
3946
4323
|
for (const line of plan.todo)
|
|
3947
4324
|
info(`will send — ${line}`);
|
|
4325
|
+
// A resume plan object (below) reports its OWN family/roles/custody, deliberately lighter than a
|
|
4326
|
+
// fresh deploy's: royalty, mint, and byte custody are InitParams fields fixed at the ORIGINAL
|
|
4327
|
+
// deploy — a resume only ever finishes MISSING setup legs, so those fields are explicit `null`
|
|
4328
|
+
// (see deploy-plan.ts's own doc comments) rather than restating what this invocation's flags say,
|
|
4329
|
+
// which a resume does not actually write.
|
|
4330
|
+
// ANNOTATED, not inferred: this object is spread into four `satisfies DeployPlan` emits below, and
|
|
4331
|
+
// an un-annotated literal WIDENS its own properties (`schemaVersion` 1 -> number, `family`
|
|
4332
|
+
// 'code'|'code-edition' -> string), so every one of those spreads would fail to satisfy the plan.
|
|
4333
|
+
// The Omit names exactly the three keys each call site supplies for itself.
|
|
4334
|
+
const resumePlanBase = {
|
|
4335
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
4336
|
+
family: isEdition ? 'code-edition' : 'code',
|
|
4337
|
+
lane,
|
|
4338
|
+
roles: { signer: owner, owner, royaltyReceiver: null, primaryPayee: null, minter: null },
|
|
4339
|
+
royalty: null,
|
|
4340
|
+
custody: { onChainUri, imageOnChain: null, backend: null, tokenUriBase: null, contractUriBase: null },
|
|
4341
|
+
mint: null,
|
|
4342
|
+
warnings: planWarnings.slice(),
|
|
4343
|
+
// Still reflects what THIS invocation's flags say about every marketplace-facing surface —
|
|
4344
|
+
// computed once, unconditionally, at the top of the function (see `imageDisposition` et al.),
|
|
4345
|
+
// so it's available whether this run turns out to be a fresh deploy or a `--resume`.
|
|
4346
|
+
surfaces: {
|
|
4347
|
+
thumbnail: { ok: !imageOrphaned, detail: stripAnsiForPlan(imageDisposition) },
|
|
4348
|
+
traits: { ok: !traitsBroken, detail: stripAnsiForPlan(traitsDisposition) },
|
|
4349
|
+
postParams: { ok: !(undeclaredParams.length > 0 || paramsNudge), detail: stripAnsiForPlan(paramsDisposition) },
|
|
4350
|
+
},
|
|
4351
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
4352
|
+
};
|
|
3948
4353
|
if (!plan.calls.length) {
|
|
3949
4354
|
ok(`nothing missing — this contract's setup is already complete. ${dim('Confirm with `abx verify ' + target + '`.')}`);
|
|
3950
|
-
emit(jsonSafe({
|
|
4355
|
+
emit(jsonSafe({
|
|
4356
|
+
command: 'deploy-code', kind: isEdition ? 'edition-code' : 'code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, sent: false, complete: true, sentLegs: 0,
|
|
4357
|
+
plan: {
|
|
4358
|
+
...resumePlanBase,
|
|
4359
|
+
transactions: { approvals: 0, legs: null },
|
|
4360
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4361
|
+
resume: { target, sentLegs: 0, complete: true },
|
|
4362
|
+
},
|
|
4363
|
+
}));
|
|
3951
4364
|
console.log('');
|
|
3952
4365
|
return;
|
|
3953
4366
|
}
|
|
@@ -3961,6 +4374,15 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3961
4374
|
deps: plan.sending.deps ? deps.map((d) => d.display) : [],
|
|
3962
4375
|
onchainUri: plan.sending.uri,
|
|
3963
4376
|
});
|
|
4377
|
+
// A resume always sends its missing legs as ONE setup-multicall tx — one wallet approval — so
|
|
4378
|
+
// `transactions.legs` names WHICH groups ride it, from the same `plan.sending` flags that build
|
|
4379
|
+
// the tx above (never re-derived).
|
|
4380
|
+
const resumeLegs = [
|
|
4381
|
+
plan.sending.chunkIndices.length ? 'chunks' : null,
|
|
4382
|
+
plan.sending.schemaKeys.length ? 'param-schemas' : null,
|
|
4383
|
+
plan.sending.deps ? 'dependencies' : null,
|
|
4384
|
+
plan.sending.uri ? 'onchain-uri' : null,
|
|
4385
|
+
].filter((leg) => leg !== null);
|
|
3964
4386
|
// Routed through the shared risk gate — the same choke point ownerops.ts's `runWrite` uses for
|
|
3965
4387
|
// every owner-op: --dry-run preview / --confirm prompt / lane selection / signing, all one way.
|
|
3966
4388
|
// This used to be its own hand-rolled copy of that exact shape (a dry-run print then confirmSend
|
|
@@ -3968,14 +4390,30 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3968
4390
|
// any other, so it takes the identical gate (the preview also gains the `to`/`owner` lines
|
|
3969
4391
|
// `runWrite`'s preview always printed).
|
|
3970
4392
|
if (isDryRun(flags)) {
|
|
3971
|
-
emit(jsonSafe({
|
|
4393
|
+
emit(jsonSafe({
|
|
4394
|
+
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,
|
|
4395
|
+
plan: {
|
|
4396
|
+
...resumePlanBase,
|
|
4397
|
+
transactions: { approvals: 1, legs: resumeLegs },
|
|
4398
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4399
|
+
resume: { target, sentLegs: plan.calls.length, complete: false },
|
|
4400
|
+
},
|
|
4401
|
+
}));
|
|
3972
4402
|
}
|
|
3973
4403
|
const result = await gatedSend(() => tx, flags, { chainKey: CHAIN, expectedSigner: owner });
|
|
3974
4404
|
if (!result) {
|
|
3975
4405
|
if (isDryRun(flags))
|
|
3976
4406
|
return;
|
|
3977
4407
|
console.log(`\n${dim(' unsigned — broadcast it, then re-run `abx verify ' + target + '` to confirm the setup completed.')}\n`);
|
|
3978
|
-
emit(jsonSafe({
|
|
4408
|
+
emit(jsonSafe({
|
|
4409
|
+
command: 'deploy-code', kind: isEdition ? 'edition-code' : 'code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, sent: false, complete: false, sentLegs: plan.calls.length,
|
|
4410
|
+
plan: {
|
|
4411
|
+
...resumePlanBase,
|
|
4412
|
+
transactions: { approvals: 1, legs: resumeLegs },
|
|
4413
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4414
|
+
resume: { target, sentLegs: plan.calls.length, complete: false },
|
|
4415
|
+
},
|
|
4416
|
+
}));
|
|
3979
4417
|
return;
|
|
3980
4418
|
}
|
|
3981
4419
|
ok(`${tx.summary}`);
|
|
@@ -3985,6 +4423,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3985
4423
|
const resumedFloor = (await discoverDeployBlock(publicClient, target)) ?? result.blockNumber;
|
|
3986
4424
|
emit(jsonSafe({
|
|
3987
4425
|
command: 'deploy-code',
|
|
4426
|
+
kind: isEdition ? 'edition-code' : 'code',
|
|
3988
4427
|
resumed: target,
|
|
3989
4428
|
address: target,
|
|
3990
4429
|
chain: CHAIN,
|
|
@@ -3994,15 +4433,33 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
3994
4433
|
complete: true,
|
|
3995
4434
|
sentLegs: plan.calls.length,
|
|
3996
4435
|
txHash: result.txHash,
|
|
4436
|
+
plan: {
|
|
4437
|
+
...resumePlanBase,
|
|
4438
|
+
transactions: { approvals: 1, legs: resumeLegs },
|
|
4439
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4440
|
+
resume: { target, sentLegs: plan.calls.length, complete: true },
|
|
4441
|
+
},
|
|
3997
4442
|
}));
|
|
3998
4443
|
step('Index');
|
|
3999
|
-
|
|
4444
|
+
// The factory HINT registerAndIndexLocally passes through to detectCanonicalFactory is taken as
|
|
4445
|
+
// an UNVERIFIED override when present (see anchors.ts) — so it must name the RIGHT factory family
|
|
4446
|
+
// for this target. `factory` (outer scope) is the 721 SeriesCode factory this function resolved at
|
|
4447
|
+
// its own "Trust anchor" step; an EditionCode target needs its own factory's address instead (a
|
|
4448
|
+
// read-only manifest lookup — never bootstraps one, unlike a real edition deploy).
|
|
4449
|
+
await registerAndIndexLocally(target, {
|
|
4450
|
+
'from-block': resumedFloor.toString(),
|
|
4451
|
+
factory: isEdition ? editionCodeFactoryAddress(flags.factory) : factory,
|
|
4452
|
+
label: name,
|
|
4453
|
+
});
|
|
4000
4454
|
console.log(`\n ${g('\u2713 setup finished')} \u2014 ${dim('confirm it resolves:')} ${bold(`abx verify ${target}`)}` +
|
|
4001
4455
|
`${onChainUri ? dim(' (chain-complete + the on-chain tokenURI)') : ''}\n`);
|
|
4002
4456
|
return;
|
|
4003
4457
|
}
|
|
4004
4458
|
let clone;
|
|
4005
4459
|
let deployBlock;
|
|
4460
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`). The
|
|
4461
|
+
// 'unsigned' branch below returns before the sent `emit()`, so it never needs to set this.
|
|
4462
|
+
let deployerAddr;
|
|
4006
4463
|
if (lane === 'unsigned') {
|
|
4007
4464
|
// cold lane: deterministic deploy makes the whole sequence pre-computable — the clone
|
|
4008
4465
|
// address is a pure function of (factory, salt), so setup + mints target it up front.
|
|
@@ -4027,6 +4484,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
4027
4484
|
const session = await openWalletSession({ chainKey: CHAIN, expectedSigner: flags.for, total: approvals, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
4028
4485
|
try {
|
|
4029
4486
|
const signer = await session.connect();
|
|
4487
|
+
deployerAddr = signer;
|
|
4030
4488
|
const { clone: predicted, txs } = await preparedFor(signer);
|
|
4031
4489
|
// The scan floor is the clone-CREATION block (txs[0]) — NOT the last tx. A wallet session can
|
|
4032
4490
|
// span blocks (deploy at N, setup+mint at N+2), and the `code` field is written in the deploy
|
|
@@ -4052,6 +4510,7 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
4052
4510
|
// `signHotSequence` (signer.ts) / `makeHotSender` (sdk execute.ts) for the read-after-write-lag
|
|
4053
4511
|
// reasoning this used to be a hand-rolled loop for.
|
|
4054
4512
|
const { account } = makeWalletClient({ chainKey: CHAIN });
|
|
4513
|
+
deployerAddr = account.address;
|
|
4055
4514
|
const { clone: predicted, txs } = await preparedFor(account.address);
|
|
4056
4515
|
const results = await signHotSequence(txs, { chainKey: CHAIN, yes: !!flags.yes });
|
|
4057
4516
|
clone = predicted;
|
|
@@ -4087,6 +4546,37 @@ export async function cmdDeployCodeBody(flags, emit) {
|
|
|
4087
4546
|
name,
|
|
4088
4547
|
symbol,
|
|
4089
4548
|
onChainUri,
|
|
4549
|
+
plan: {
|
|
4550
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
4551
|
+
family: 'code',
|
|
4552
|
+
lane,
|
|
4553
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
4554
|
+
roles: {
|
|
4555
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
4556
|
+
primaryPayee: flags['primary-payee'] ?? null,
|
|
4557
|
+
minter: flags.minter ?? null,
|
|
4558
|
+
},
|
|
4559
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
4560
|
+
custody: {
|
|
4561
|
+
onChainUri,
|
|
4562
|
+
imageOnChain: null, // see `surfaces.thumbnail`
|
|
4563
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
4564
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
4565
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
4566
|
+
},
|
|
4567
|
+
mint: { deferred: mintCount === 0, count: mintCount, amountPerId: null, recipient: mintCount > 0 ? deployerAddr ?? null : null },
|
|
4568
|
+
// Unlike the dry run, the sent path computes no cost estimate (the gas-guidance try/catch is
|
|
4569
|
+
// dry-run-only — a real send just pays whatever `estimateGas` returns at signing time).
|
|
4570
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
4571
|
+
warnings: planWarnings.slice(),
|
|
4572
|
+
surfaces: {
|
|
4573
|
+
thumbnail: { ok: !imageOrphaned, detail: stripAnsiForPlan(imageDisposition) },
|
|
4574
|
+
traits: { ok: !traitsBroken, detail: stripAnsiForPlan(traitsDisposition) },
|
|
4575
|
+
postParams: { ok: !(undeclaredParams.length > 0 || paramsNudge), detail: stripAnsiForPlan(paramsDisposition) },
|
|
4576
|
+
},
|
|
4577
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
4578
|
+
resume: null,
|
|
4579
|
+
},
|
|
4090
4580
|
}));
|
|
4091
4581
|
step('Index');
|
|
4092
4582
|
await registerAndIndexLocally(clone, { 'from-block': deployBlock.toString(), factory, label: name });
|
|
@@ -4175,43 +4665,51 @@ export const DEPLOY_CODE_EDITION_FLAGS = new Set([
|
|
|
4175
4665
|
'public-base-url', 'onchain-uri', 'generator', 'renderer', 'port',
|
|
4176
4666
|
'sign', 'unsigned', 'for', 'salt', 'factory', 'bootstrap-factory', 'sign-url-file',
|
|
4177
4667
|
'dry-run', 'confirm', 'yes', 'json', 'copies',
|
|
4178
|
-
//
|
|
4179
|
-
//
|
|
4180
|
-
|
|
4668
|
+
// storage (directory mode + --image-base's off-chain still) — mirrors storageOverrides(), same
|
|
4669
|
+
// allowlist entries as the 721 twin's DEPLOY_CODE_FLAGS.
|
|
4670
|
+
'code-dir', 'backend', 'endpoint', 'bucket', 'region', 'prefix', 'public-base', 'gateway', 'mode', 'api-url', 'upload-url', 'provider', 'storage-signer',
|
|
4671
|
+
'ipfs-gateway', 'arweave-gateway',
|
|
4672
|
+
// NOT scope cuts: --image-renderer/--attributes-renderer (on-chain field renderers) and
|
|
4673
|
+
// --image-base (an off-chain deterministic-per-id still) are all fully wired on EditionCode —
|
|
4674
|
+
// `abx help deploy-code` documents them as working. --dep/--dep-registry likewise: EditionCode
|
|
4675
|
+
// inherits the same Dependencies extension, and the legs ride the same setup multicall.
|
|
4676
|
+
'image-renderer', 'attributes-renderer', 'image-base', 'dep', 'dep-registry',
|
|
4677
|
+
// v1-scope-cut flag: kept in the ALLOWLIST (not stray) so it reaches this function's own pointed
|
|
4678
|
+
// refusal below, rather than the generic "unrecognized flag" `refuseStrayFlags` would give it
|
|
4679
|
+
// otherwise — a real but less helpful refusal (it doesn't say WHY, or the way out).
|
|
4181
4680
|
// (`resume` is NOT here: --resume + --copies is refused earlier, at cmdDeployCode's dispatch —
|
|
4182
4681
|
// 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',
|
|
4682
|
+
'no-delegation',
|
|
4188
4683
|
]);
|
|
4189
4684
|
export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
4190
|
-
|
|
4685
|
+
beginPlanWarnings();
|
|
4686
|
+
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
4687
|
'[--description "<s>"] [--external-url <url>] [--max N] [--mint-count N | --mint-all] [--mint-amount N] ' +
|
|
4192
4688
|
'[--schema key:Type:Auth,…] [--no-seed | --seed-source 0x..] [--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..]';
|
|
4193
4689
|
refuseStrayFlags(flags, DEPLOY_CODE_EDITION_FLAGS, 'deploy-code');
|
|
4194
4690
|
warnSignWithoutFor(flags);
|
|
4195
4691
|
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
|
-
}
|
|
4692
|
+
const scriptPath = flags.script;
|
|
4693
|
+
const codeDir = flags['code-dir'];
|
|
4694
|
+
if (scriptPath && codeDir)
|
|
4695
|
+
throw new Error(usage);
|
|
4208
4696
|
// The in-chain Solidity renderer lane, same as the 721 twin: EditionCode composes the same
|
|
4209
4697
|
// OnChainMetadata extension and the same URI-renderer slots, so `image`/`attributes` at a
|
|
4210
4698
|
// `renderer` representation resolve through the canonical metadata renderer with no server in the
|
|
4211
4699
|
// graph. Proven end-to-end in contracts/test/EditionOnChainRender.t.sol.
|
|
4212
4700
|
const imageRendererFlag = flags['image-renderer'] ? String(flags['image-renderer']) : undefined;
|
|
4213
4701
|
const attributesRendererFlag = flags['attributes-renderer'] ? String(flags['attributes-renderer']) : undefined;
|
|
4214
|
-
|
|
4702
|
+
// --image-renderer and --image-base both set the `image` field — pick ONE, same rule as the 721
|
|
4703
|
+
// twin. Checked before either is resolved so the refusal is immediate rather than after a renderer
|
|
4704
|
+
// code-presence probe.
|
|
4705
|
+
if (imageRendererFlag && flags['image-base']) {
|
|
4706
|
+
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).');
|
|
4707
|
+
}
|
|
4708
|
+
// A JS program either way (template chunks or a hosted build) — same "hasProgram" test as the 721
|
|
4709
|
+
// twin (see its own `const hasProgram = !!(scriptPath || codeDir);`), so every downstream
|
|
4710
|
+
// conditional gated on it (generator resolution, animation_url, the on-chain-URI legs) already
|
|
4711
|
+
// does the right thing for a directory build without a SEPARATE flag to thread through.
|
|
4712
|
+
const hasProgram = !!(scriptPath || codeDir);
|
|
4215
4713
|
const hasFieldRenderers = !!(imageRendererFlag || attributesRendererFlag);
|
|
4216
4714
|
if (!hasProgram && !hasFieldRenderers) {
|
|
4217
4715
|
throw new Error(`${usage}\n (or, for a FULLY on-chain Solidity render with no program: --copies <n|open> --image-renderer 0x.. ` +
|
|
@@ -4373,11 +4871,30 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4373
4871
|
}
|
|
4374
4872
|
}
|
|
4375
4873
|
}
|
|
4874
|
+
// Content: directory mode uploads the build (its root locator → the `code` collection field);
|
|
4875
|
+
// template mode chunks the script for on-chain storage. The directory UPLOAD is deferred until
|
|
4876
|
+
// after the --confirm gate (and skipped entirely on --dry-run) so a preview never pins bytes —
|
|
4877
|
+
// same shape as the 721 twin's identical block (see its own comment for the full reasoning).
|
|
4376
4878
|
step('Content');
|
|
4377
4879
|
let scriptChunks = [];
|
|
4880
|
+
let dirUpload = null;
|
|
4378
4881
|
let contentSummary;
|
|
4379
|
-
if (
|
|
4380
|
-
const
|
|
4882
|
+
if (codeDir) {
|
|
4883
|
+
const dirPath = resolvePath(codeDir);
|
|
4884
|
+
const files = readdirSync(dirPath).filter((f) => !f.startsWith('.') && statSync(joinPath(dirPath, f)).isFile());
|
|
4885
|
+
if (!files.includes('index.html'))
|
|
4886
|
+
throw new Error(`${dirPath} has no index.html — the directory entry, by convention`);
|
|
4887
|
+
const backend = resolveBackend(storageOptions(storageOverrides(flags)));
|
|
4888
|
+
if (!backend.putDirectory)
|
|
4889
|
+
throw new Error(`storage backend '${backend.id}' has no directory upload — use --backend ipfs (pinata) or arweave`);
|
|
4890
|
+
const entries = files.map((f) => ({ name: f, bytes: new Uint8Array(readFileSync(joinPath(dirPath, f))), contentType: contentTypeFromPath(joinPath(dirPath, f)) }));
|
|
4891
|
+
dirUpload = { backend, entries, sizes: entries.map((e) => e.bytes.length) };
|
|
4892
|
+
contentSummary = `${entries.length} file(s) → ${backend.id} directory (code field)`;
|
|
4893
|
+
info(`${entries.length} file(s) from ${basename(dirPath)}/ → ${backend.id} directory (code field)`);
|
|
4894
|
+
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).'));
|
|
4895
|
+
}
|
|
4896
|
+
else if (scriptPath) {
|
|
4897
|
+
const source = readFileSync(resolvePath(scriptPath), 'utf8');
|
|
4381
4898
|
scriptChunks = planOnChainScript(source);
|
|
4382
4899
|
contentSummary = `script ${Buffer.byteLength(source, 'utf8')} bytes → ${scriptChunks.length} on-chain chunk(s)`;
|
|
4383
4900
|
ok(contentSummary);
|
|
@@ -4431,6 +4948,56 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4431
4948
|
if (hasFieldRenderers && !onChainUri) {
|
|
4432
4949
|
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
4950
|
}
|
|
4951
|
+
// --image-base: bake the on-chain `image` as a url-template (`{base}/{id}.png`) — same field write
|
|
4952
|
+
// as the 721 twin's identical block (see its own comment for the full reasoning), one id space
|
|
4953
|
+
// finer: `{id}` substitutes the EditionCode id, not a sequential 721 token number, and the effect
|
|
4954
|
+
// runner's render sweep already excludes an id with no live copies (harness.ts) — an id with no
|
|
4955
|
+
// mint-time seed drawn yet has nothing to render FROM, the same reason a burned 721 token is
|
|
4956
|
+
// excluded. There is no per-copy addressing: every copy of an id shares one image, same as every
|
|
4957
|
+
// other field on an id.
|
|
4958
|
+
if (flags['image-base']) {
|
|
4959
|
+
const base = String(flags['image-base']);
|
|
4960
|
+
// --image-base needs a mutable, path-addressed URL (the effect runner overwrites the SAME key in
|
|
4961
|
+
// place) — a content-addressed gateway URL (ipfs/arweave) can't back a FIXED per-id address, no
|
|
4962
|
+
// matter what THIS deploy's own --backend happens to be. One shared validator decides this
|
|
4963
|
+
// (packages/storage/content-plan.ts), so this can never disagree with the 721 lane's own refusal.
|
|
4964
|
+
const imageBaseCombo = validateRenderStorageCombo({ imageBaseUrl: base });
|
|
4965
|
+
if (!imageBaseCombo.ok) {
|
|
4966
|
+
if (dryRun)
|
|
4967
|
+
warn(`would REFUSE — ${imageBaseCombo.reason}`);
|
|
4968
|
+
else
|
|
4969
|
+
throw new Error(imageBaseCombo.reason);
|
|
4970
|
+
}
|
|
4971
|
+
// NEVER bake a localhost/loopback image host on-chain — the token's `image` would resolve for no
|
|
4972
|
+
// one. This is the marketplace still's PERMANENT address.
|
|
4973
|
+
if (loopbackBaseUrl(base)) {
|
|
4974
|
+
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.`;
|
|
4975
|
+
if (dryRun)
|
|
4976
|
+
warn(`would REFUSE — ${m}`);
|
|
4977
|
+
else
|
|
4978
|
+
throw new Error(m);
|
|
4979
|
+
}
|
|
4980
|
+
// The exact trap a real agent hit on the 721 lane: it baked the S3 API endpoint (auth-only) as
|
|
4981
|
+
// --image-base. Refuse the R2 API form (never public); warn on the S3 API form.
|
|
4982
|
+
if (/\.r2\.cloudflarestorage\.com/i.test(base)) {
|
|
4983
|
+
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.`;
|
|
4984
|
+
if (dryRun)
|
|
4985
|
+
warn(`would REFUSE — ${m}`);
|
|
4986
|
+
else
|
|
4987
|
+
throw new Error(m);
|
|
4988
|
+
}
|
|
4989
|
+
else if (/(^|\/\/)s3[.-][^/]*amazonaws\.com/i.test(base) || /\.s3[.-][^/]*amazonaws\.com/i.test(base)) {
|
|
4990
|
+
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.`);
|
|
4991
|
+
}
|
|
4992
|
+
const imageTemplate = base.includes('{id}') ? base : `${base.replace(/\/+$/, '')}/{id}.png`;
|
|
4993
|
+
contractFields.push({ field: encodeTag(F.image), representation: encodeTag(R.urlTemplate), value: toHex(imageTemplate) });
|
|
4994
|
+
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)')}`);
|
|
4995
|
+
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)')}`);
|
|
4996
|
+
const s3Base = process.env.ABX_S3_PUBLIC_BASE?.replace(/\/+$/, '');
|
|
4997
|
+
if (s3Base && !imageTemplate.startsWith(`${s3Base}/`)) {
|
|
4998
|
+
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.`);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
4434
5001
|
const onchainUriLegs = onChainUri ? onchainUriSetupCalls({ generator, metadataRenderer }) : [];
|
|
4435
5002
|
step('Deploy');
|
|
4436
5003
|
const lane = laneFromFlags(flags);
|
|
@@ -4513,6 +5080,17 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4513
5080
|
// (a script with no schemas and no premints) would report 1 approval and send 2 transactions.
|
|
4514
5081
|
const depLegCount = dependencySetupCalls(deps, depRegistry).length;
|
|
4515
5082
|
const approvals = 1 + (scriptChunks.length + schemas.length + depLegCount + onchainUriLegs.length + mintCount > 0 ? 1 : 0);
|
|
5083
|
+
// Ordered legs riding the setup multicall (if any) — same booleans `approvals` above uses, so the
|
|
5084
|
+
// plan object's `transactions.legs` can never drift from the tx the wallet actually signs.
|
|
5085
|
+
const planSetupLegs = approvals > 1
|
|
5086
|
+
? [
|
|
5087
|
+
scriptChunks.length ? 'chunks' : null,
|
|
5088
|
+
schemas.length ? 'param-schemas' : null,
|
|
5089
|
+
depLegCount ? 'dependencies' : null,
|
|
5090
|
+
onchainUriLegs.length ? 'onchain-uri' : null,
|
|
5091
|
+
mintCount ? 'mints' : null,
|
|
5092
|
+
].filter((leg) => leg !== null)
|
|
5093
|
+
: null;
|
|
4516
5094
|
if (!dryRun) {
|
|
4517
5095
|
await confirmSend(
|
|
4518
5096
|
// Second site of the same missing binding as the plan line above — and the worse one: this is
|
|
@@ -4558,11 +5136,60 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4558
5136
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
|
|
4559
5137
|
}
|
|
4560
5138
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
|
|
4561
|
-
emit(jsonSafe({
|
|
5139
|
+
emit(jsonSafe({
|
|
5140
|
+
command: 'deploy-code', kind: 'edition-code', copies: editionSize.toString(), dryRun: true, sent: false, address: shownAddr, chain: CHAIN, chainId, factory, name, symbol,
|
|
5141
|
+
plan: {
|
|
5142
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
5143
|
+
family: 'code-edition',
|
|
5144
|
+
lane,
|
|
5145
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
5146
|
+
roles: {
|
|
5147
|
+
signer: deployer, owner: deployer, royaltyReceiver: deployer,
|
|
5148
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
5149
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
5150
|
+
},
|
|
5151
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
5152
|
+
custody: {
|
|
5153
|
+
onChainUri,
|
|
5154
|
+
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
|
|
5155
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
5156
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
5157
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
5158
|
+
},
|
|
5159
|
+
mint: {
|
|
5160
|
+
deferred: mintCount === 0,
|
|
5161
|
+
count: mintCount,
|
|
5162
|
+
amountPerId: mintAmount.toString(),
|
|
5163
|
+
recipient: mintCount > 0 ? deployer : null,
|
|
5164
|
+
},
|
|
5165
|
+
estimate: { ethApprox: null, gasApprox: null }, // this lane computes no cost estimate
|
|
5166
|
+
warnings: planWarnings.slice(),
|
|
5167
|
+
surfaces: null, // only the fresh 721 code lane computes the per-surface disposition today
|
|
5168
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
5169
|
+
resume: null,
|
|
5170
|
+
},
|
|
5171
|
+
}));
|
|
4562
5172
|
return;
|
|
4563
5173
|
}
|
|
5174
|
+
// Real deploy: perform the deferred directory upload now (past the confirm gate). `contractFields`
|
|
5175
|
+
// is captured by reference in `initParamsFor` above — pushing to it here still lands in the CREATE
|
|
5176
|
+
// tx's init params, since that closure isn't actually CALLED until the signing lane below runs.
|
|
5177
|
+
if (dirUpload) {
|
|
5178
|
+
const { base } = await dirUpload.backend.putDirectory(dirUpload.entries);
|
|
5179
|
+
// Store the BARE locator (CID / arweave txid), never a full gateway URL — the resolver applies
|
|
5180
|
+
// the gateway at serve time per `representation`.
|
|
5181
|
+
const ipfsCid = base.match(/\/ipfs\/([^/]+)/)?.[1];
|
|
5182
|
+
const arTxid = base.match(/arweave\.net\/([^/?#]+)/i)?.[1];
|
|
5183
|
+
const rep = ipfsCid ? 'ipfs' : dirUpload.backend.id === 'arweave' ? 'arweave' : 'url';
|
|
5184
|
+
const value = ipfsCid ?? (rep === 'arweave' ? (arTxid ?? base) : base);
|
|
5185
|
+
contractFields.push({ field: encodeTag('code'), representation: encodeTag(rep), value: toHex(value) });
|
|
5186
|
+
ok(`uploaded ${dirUpload.entries.length} file(s) → code field (${rep}): ${value}`);
|
|
5187
|
+
}
|
|
4564
5188
|
let clone;
|
|
4565
5189
|
let deployBlock;
|
|
5190
|
+
// See the 1/1 lane's identical local for why this is hoisted (feeds the sent plan's `roles`). The
|
|
5191
|
+
// 'unsigned' branch below returns before the sent `emit()`, so it never needs to set this.
|
|
5192
|
+
let deployerAddr;
|
|
4566
5193
|
if (lane === 'unsigned') {
|
|
4567
5194
|
const signer = flags.for;
|
|
4568
5195
|
if (!signer)
|
|
@@ -4577,6 +5204,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4577
5204
|
const session = await openWalletSession({ chainKey: CHAIN, expectedSigner: flags.for, total: approvals, port: flags.port ? Number(flags.port) : undefined, signUrlFile: flags['sign-url-file'] });
|
|
4578
5205
|
try {
|
|
4579
5206
|
const signer = await session.connect();
|
|
5207
|
+
deployerAddr = signer;
|
|
4580
5208
|
const { clone: predicted, txs } = await preparedFor(signer);
|
|
4581
5209
|
let first;
|
|
4582
5210
|
for (const tx of txs) {
|
|
@@ -4593,6 +5221,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4593
5221
|
}
|
|
4594
5222
|
else {
|
|
4595
5223
|
const { account } = makeWalletClient({ chainKey: CHAIN });
|
|
5224
|
+
deployerAddr = account.address;
|
|
4596
5225
|
const { clone: predicted, txs } = await preparedFor(account.address);
|
|
4597
5226
|
const results = await signHotSequence(txs, { chainKey: CHAIN, yes: !!flags.yes });
|
|
4598
5227
|
clone = predicted;
|
|
@@ -4606,7 +5235,39 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4606
5235
|
info(`permanent: max ${max} id(s) · royalty ${royaltyBps / 100}%${flags['royalty-bps'] === undefined ? ' ⚠ default' : ''} → the deploying wallet` +
|
|
4607
5236
|
` · cap ${maxRoyaltyBps / 100}%${burnable ? ' · burnable' : ''}` +
|
|
4608
5237
|
dim(' (rate is changeable with `abx set-royalty`; the CAP only ever goes down)'));
|
|
4609
|
-
emit(jsonSafe({
|
|
5238
|
+
emit(jsonSafe({
|
|
5239
|
+
command: 'deploy-code', kind: 'edition-code', copies: editionSize.toString(), address: clone, chain: CHAIN, chainId, factory, deployBlock, name, symbol, onChainUri,
|
|
5240
|
+
plan: {
|
|
5241
|
+
schemaVersion: DEPLOY_PLAN_SCHEMA_VERSION,
|
|
5242
|
+
family: 'code-edition',
|
|
5243
|
+
lane,
|
|
5244
|
+
transactions: { approvals, legs: planSetupLegs },
|
|
5245
|
+
roles: {
|
|
5246
|
+
signer: deployerAddr ?? null, owner: deployerAddr ?? null, royaltyReceiver: deployerAddr ?? null,
|
|
5247
|
+
primaryPayee: primaryPayee !== zeroAddress ? primaryPayee : null,
|
|
5248
|
+
minter: minter !== zeroAddress ? minter : null,
|
|
5249
|
+
},
|
|
5250
|
+
royalty: { bps: royaltyBps, capBps: maxRoyaltyBps, burnable },
|
|
5251
|
+
custody: {
|
|
5252
|
+
onChainUri,
|
|
5253
|
+
imageOnChain: null,
|
|
5254
|
+
backend: dirUpload ? dirUpload.backend.id : null,
|
|
5255
|
+
tokenUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/t`,
|
|
5256
|
+
contractUriBase: onChainUri && !hasPublicUrl ? null : `${baseUrl}/c`,
|
|
5257
|
+
},
|
|
5258
|
+
mint: {
|
|
5259
|
+
deferred: mintCount === 0,
|
|
5260
|
+
count: mintCount,
|
|
5261
|
+
amountPerId: mintAmount.toString(),
|
|
5262
|
+
recipient: mintCount > 0 ? deployerAddr ?? null : null,
|
|
5263
|
+
},
|
|
5264
|
+
estimate: { ethApprox: null, gasApprox: null },
|
|
5265
|
+
warnings: planWarnings.slice(),
|
|
5266
|
+
surfaces: null,
|
|
5267
|
+
dependencies: { count: deps.length, registry: depRegistry ?? null },
|
|
5268
|
+
resume: null,
|
|
5269
|
+
},
|
|
5270
|
+
}));
|
|
4610
5271
|
step('Index');
|
|
4611
5272
|
await registerAndIndexLocally(clone, { 'from-block': deployBlock.toString(), factory, label: name });
|
|
4612
5273
|
if (onChainUri) {
|
|
@@ -4634,6 +5295,11 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
4634
5295
|
console.log(` ${bold(`abx mint ${clone} --token-id <id> --amount <n>`)} ${dim('# mint more copies of an id')}`);
|
|
4635
5296
|
console.log(` ${bold(`abx minter configure ${clone} --token-id <id> --price <eth> --allocation <n>`)} ${dim('# set up a priced sale for one id')}`);
|
|
4636
5297
|
console.log(` ${bold(`abx refresh ${clone}`)} ${dim('# nudge marketplaces once metadata is live')}\n`);
|
|
5298
|
+
// Same Arweave-propagation caveat as the 721 twin (see its own comment): the gateway 404s until
|
|
5299
|
+
// Turbo settles the bundle, so a render run right after deploy correctly finds nothing servable yet.
|
|
5300
|
+
if (dirUpload?.backend.id === 'arweave') {
|
|
5301
|
+
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).')}`);
|
|
5302
|
+
}
|
|
4637
5303
|
}
|
|
4638
5304
|
/**
|
|
4639
5305
|
* Resolve `--721c` into the InitParams `transferValidator`. Absent → `zeroAddress`: a plain
|