@artblocks/abx-cli 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +229 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +300 -31
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +21 -5
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +33 -3
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +17 -9
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +11 -3
- package/dist/commands/storage.js.map +1 -1
- package/dist/flag-allowlists.d.ts +53 -0
- package/dist/flag-allowlists.d.ts.map +1 -0
- package/dist/flag-allowlists.js +148 -0
- package/dist/flag-allowlists.js.map +1 -0
- package/dist/flags.d.ts +2 -2
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js.map +1 -1
- package/dist/kind.d.ts +8 -3
- package/dist/kind.d.ts.map +1 -1
- package/dist/kind.js +25 -0
- package/dist/kind.js.map +1 -1
- package/dist/main.js +41 -16
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +87 -5
- package/dist/ownerops.js.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +8 -2
- package/dist/remote.js.map +1 -1
- package/dist/update-check.d.ts +25 -0
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +40 -6
- package/dist/update-check.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +7 -5
- package/skill/reference/code-projects.md +22 -0
- package/skill/reference/decisions.md +26 -0
- package/skill/reference/operating.md +3 -0
package/dist/commands/deploy.js
CHANGED
|
@@ -1190,13 +1190,15 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1190
1190
|
const dryRun = isDryRun(flags);
|
|
1191
1191
|
const onchainImage = !!flags['onchain-image'];
|
|
1192
1192
|
// Pure flag-combo validation — checked BEFORE any chain read (predict/factory/renderer all touch
|
|
1193
|
-
// the network), same reasoning every other "this combo can never work" refusal in this file uses
|
|
1194
|
-
//
|
|
1195
|
-
// the
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1193
|
+
// the network), same reasoning every other "this combo can never work" refusal in this file uses.
|
|
1194
|
+
// Staging is a SEQUENCE (chunk write(s) → the deploy that references the manifest) where each tx's
|
|
1195
|
+
// receipt feeds the next, so it cannot be signed offline in one run — the cold lane is refused on
|
|
1196
|
+
// every lineage, 721 and edition alike. The wallet lane CAN do it (one session signs the chunk
|
|
1197
|
+
// writes and the deploy); this edition path was hot-lane-only purely because it lacked that
|
|
1198
|
+
// session branch, which it now has.
|
|
1199
|
+
if (onchainImage && lane === 'unsigned') {
|
|
1200
|
+
throw new Error('Staging an on-chain image (--onchain-image) needs interactive signing — each chunk tx feeds ' +
|
|
1201
|
+
"the next, so it can't run on the cold lane (--unsigned). Use the hot lane (a funded key) or --sign (browser wallet).");
|
|
1200
1202
|
}
|
|
1201
1203
|
if (dryRun)
|
|
1202
1204
|
assertPreviewDeployer(flags);
|
|
@@ -1273,18 +1275,23 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1273
1275
|
}
|
|
1274
1276
|
step(`Deploy an edition of "${name}" to ${CHAIN}`);
|
|
1275
1277
|
let bakedImage;
|
|
1278
|
+
// On the WALLET lane staging cannot happen here — it has to run inside the sign session, after the
|
|
1279
|
+
// connect, so the connecting wallet pays for and owns every chunk write. `stageNow` is the hot-lane
|
|
1280
|
+
// (and dry-run) path; the session branch below calls the same helper with a session-backed sender.
|
|
1281
|
+
const stageOnChainImage = async (sender) => {
|
|
1282
|
+
const { field, note } = await stageImageField(flags.image, parseCompress(flags.compress), sender);
|
|
1283
|
+
bakedImage = field;
|
|
1284
|
+
info(note);
|
|
1285
|
+
};
|
|
1276
1286
|
if (onchainImage) {
|
|
1277
1287
|
if (!flags.image)
|
|
1278
1288
|
throw new Error('--onchain-image needs --image <path> (the bytes to put on-chain)');
|
|
1279
1289
|
step('Stage on-chain image');
|
|
1280
|
-
if (dryRun)
|
|
1290
|
+
if (dryRun)
|
|
1281
1291
|
info(previewImageStaging(flags.image, parseCompress(flags.compress)));
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
bakedImage = field;
|
|
1286
|
-
info(note);
|
|
1287
|
-
}
|
|
1292
|
+
else if (lane === 'send')
|
|
1293
|
+
await stageOnChainImage(envStagingSender());
|
|
1294
|
+
// lane === 'sign' → staged inside the wallet session further down.
|
|
1288
1295
|
}
|
|
1289
1296
|
const explicitSalt = parseSaltFlag(flags.salt);
|
|
1290
1297
|
const noMint = flags['no-mint'] !== undefined;
|
|
@@ -1295,6 +1302,21 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1295
1302
|
const minter = flags.minter ?? zeroAddress;
|
|
1296
1303
|
const primaryPayee = flags['primary-payee'] ?? zeroAddress;
|
|
1297
1304
|
const paused = flags.unpaused === undefined;
|
|
1305
|
+
// Same wallet-signature math as the 721 twin (see `cmdDeployBody`): staging tx(s) + the deploy tx.
|
|
1306
|
+
// The edition paths shipped without this and without the readout line below, so `--copies` previews
|
|
1307
|
+
// silently dropped the approval count the skill promises "every preview" prints — leaving an agent
|
|
1308
|
+
// with nothing to tell the creator about how many wallet prompts to expect.
|
|
1309
|
+
const approvals = onchainImage
|
|
1310
|
+
? (() => {
|
|
1311
|
+
const plan = computeContentPlan(readFileSync(resolvePath(flags.image)), parseCompress(flags.compress)).plan;
|
|
1312
|
+
return (plan.mode === 'single' ? 1 : plan.txCount) + 1;
|
|
1313
|
+
})()
|
|
1314
|
+
: 1;
|
|
1315
|
+
if (onChainUri) {
|
|
1316
|
+
info(`uri()/contractURI() resolve ON-CHAIN via the renderer — no resolver, no server, no localhost.`);
|
|
1317
|
+
if (!hasPublicUrl)
|
|
1318
|
+
info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
|
|
1319
|
+
}
|
|
1298
1320
|
const buildForDeployer = async (deployer) => {
|
|
1299
1321
|
const salt = explicitSalt ?? saltFor(deployer);
|
|
1300
1322
|
const clone = await predictClone(publicClient, { factory, salt });
|
|
@@ -1367,6 +1389,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1367
1389
|
if (primaryPayee !== zeroAddress)
|
|
1368
1390
|
info(`primary payee ${primaryPayee}`);
|
|
1369
1391
|
info(`paused ${paused}${flags.unpaused ? '' : dim(' (default — pass --unpaused to open at deploy)')}`);
|
|
1392
|
+
info(`approvals ${approvals} wallet approval(s)`); // parity with the 721 twin — TX signatures only
|
|
1370
1393
|
if (!explicitSalt) {
|
|
1371
1394
|
console.log(`\n ${bold('salt')} ${g(salt)}`);
|
|
1372
1395
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address), or ${bold(`abx predict --salt ${salt} --for ${deployer} --copies ${flags.copies}`)}.`);
|
|
@@ -1376,7 +1399,7 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1376
1399
|
emit(jsonSafe({ command: 'deploy', kind: '1of1-edition', copies: editionSize.toString(), dryRun: true, sent: false, address: explicitSalt ? predicted : null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol }));
|
|
1377
1400
|
return;
|
|
1378
1401
|
}
|
|
1379
|
-
await confirmSend(`About to deploy an edition of "${name}" (${symbol}) — ${editionSize === 0n ? 'open' : editionSize.toString()} copies${flags.image ? ' with your image' : ' (generative demo art)'}; mint: ${mintAmount > 0n ? `${mintAmount} at deploy` : 'deferred'}; owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
1402
|
+
await confirmSend(`About to deploy an edition of "${name}" (${symbol}) — ${editionSize === 0n ? 'open' : editionSize.toString()} copies${flags.image ? ' with your image' : ' (generative demo art)'}; mint: ${mintAmount > 0n ? `${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
1380
1403
|
(transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
1381
1404
|
if (lane === 'send') {
|
|
1382
1405
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
@@ -1394,8 +1417,39 @@ export async function cmdDeployOneOfOneEditionBody(flags, emit) {
|
|
|
1394
1417
|
ok(`deployed ${clone}`);
|
|
1395
1418
|
info(`tx ${explorerBase()}/tx/${r.txHash} (block ${blockNumber})`);
|
|
1396
1419
|
}
|
|
1420
|
+
else if (lane === 'sign' && onchainImage) {
|
|
1421
|
+
// Wallet lane + on-chain staging: ONE sign session signs every chunk write AND the deploy, so the
|
|
1422
|
+
// connecting wallet pays for and owns all of it. Staging can't precede the connect, so it runs
|
|
1423
|
+
// inside the session and the deploy then bakes in the resulting manifest field.
|
|
1424
|
+
// `--onchain-image` implies on-chain resolution, so nothing points at baseUrl — mirrors the 721 1/1.
|
|
1425
|
+
info('a wallet will become the owner; the edition resolves from chain — no URI base is baked in.');
|
|
1426
|
+
const session = await openWalletSession({
|
|
1427
|
+
chainKey: CHAIN,
|
|
1428
|
+
expectedSigner: flags.for,
|
|
1429
|
+
total: approvals, // the SAME staging-tx math the preview's `approvals` line reports
|
|
1430
|
+
port: flags.port ? Number(flags.port) : undefined,
|
|
1431
|
+
signUrlFile: flags['sign-url-file'],
|
|
1432
|
+
});
|
|
1433
|
+
let r;
|
|
1434
|
+
try {
|
|
1435
|
+
const signer = await session.connect();
|
|
1436
|
+
await stageOnChainImage(sessionStagingSender(session)); // sets bakedImage, read by buildForDeployer
|
|
1437
|
+
const { clone: predicted, params, salt, contentNote } = await buildForDeployer(signer);
|
|
1438
|
+
info(contentNote);
|
|
1439
|
+
info(mintAmount > 0n ? `mint: ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} of #0 → ${signer} at deploy` : 'mint: deferred — mint later with `abx mint`');
|
|
1440
|
+
const sent = await session.send(prepareDeployOneOfOneEdition({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted }));
|
|
1441
|
+
r = { txHash: sent.txHash, blockNumber: sent.receipt.blockNumber };
|
|
1442
|
+
clone = predicted;
|
|
1443
|
+
}
|
|
1444
|
+
finally {
|
|
1445
|
+
session.close();
|
|
1446
|
+
}
|
|
1447
|
+
blockNumber = r.blockNumber;
|
|
1448
|
+
ok(`deployed ${clone}`);
|
|
1449
|
+
info(`tx ${explorerBase()}/tx/${r.txHash} (block ${blockNumber})`);
|
|
1450
|
+
}
|
|
1397
1451
|
else {
|
|
1398
|
-
// wallet lane
|
|
1452
|
+
// wallet lane without staging, or the cold lane: a single deploy tx.
|
|
1399
1453
|
info(onChainUri ? 'a wallet will become the owner; the token resolves from chain — no URI base is baked in.' : `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
1400
1454
|
const result = await signTx(async (signer) => {
|
|
1401
1455
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
@@ -1570,6 +1624,10 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
1570
1624
|
if (onChainUri) {
|
|
1571
1625
|
step('On-chain renderer');
|
|
1572
1626
|
renderer = dryRun ? (rendererAddress(flags.renderer) ?? zeroAddress) : await ensureRenderer(flags.renderer);
|
|
1627
|
+
// Say WHICH renderer, like the 1/1 lane does. Both series lanes printed the step header with
|
|
1628
|
+
// nothing under it, while the skill's confirm-readout template implies an address is quotable
|
|
1629
|
+
// there — so an agent relaying the readout had a blank line to explain.
|
|
1630
|
+
info(renderer === zeroAddress ? 'would deploy the canonical renderer first' : `${dryRun ? 'would use' : 'using'} renderer ${renderer}`);
|
|
1573
1631
|
}
|
|
1574
1632
|
// Off-chain custody bakes the resolver URL on-chain — a localhost URL resolves for no one.
|
|
1575
1633
|
// (Not a concern with --onchain-uri: the renderer is authoritative and resolves from chain.)
|
|
@@ -1607,6 +1665,16 @@ export async function cmdDeploySeriesBody(flags, emit) {
|
|
|
1607
1665
|
const offchainImageOnchainJson = onChainUri && !onchainImage && directUrlBackend;
|
|
1608
1666
|
const backend = (offchainImageOnchainJson || !onChainUri) && !dryRun ? resolveBackend(opts) : undefined;
|
|
1609
1667
|
const tokenPaths = slots.map((s, i) => ({ tokenId: i, name: s, path: joinPath(dirPath, s) }));
|
|
1668
|
+
// Say WHICH backend holds the bytes, exactly as the 1/1 lane does. Both Series lanes resolved a
|
|
1669
|
+
// backend and never named it, so a creator asking for "images on IPFS" had no way to confirm from
|
|
1670
|
+
// the preview that their collection would actually pin there — the flag was accepted in silence.
|
|
1671
|
+
if (!onchainImage && (offchainImageOnchainJson || !onChainUri)) {
|
|
1672
|
+
const sr = backendResolution(overrides);
|
|
1673
|
+
info(`storage: ${sr.backend} ${dim(`(${sr.source === 'flag' ? '--backend' : sr.source === 'env' ? 'env' : 'default'})`)} — byte custody for the image(s)`);
|
|
1674
|
+
const gwWarn = localGatewayWarning(flags);
|
|
1675
|
+
if (gwWarn)
|
|
1676
|
+
warn(gwWarn);
|
|
1677
|
+
}
|
|
1610
1678
|
// Wallet-signature count for THIS deploy — the same per-token chunk math that sizes the
|
|
1611
1679
|
// wallet-lane session `total` below (B2), computed ONCE so preview/confirm text and the real
|
|
1612
1680
|
// session can never disagree. TX signatures only — see the 1/1's `approvals` for the same note
|
|
@@ -2019,9 +2087,6 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2019
2087
|
: `EditionImage (ERC-1155) — N ids from the folder × ${editionSize} cop${editionSize === 1n ? 'y' : 'ies'} each`)}`));
|
|
2020
2088
|
if (editionSize === 1n)
|
|
2021
2089
|
info(copiesOneNote('deploy-series'));
|
|
2022
|
-
if (flags['onchain-image']) {
|
|
2023
|
-
throw new Error('--onchain-image is not yet supported for edition deploys (--copies) — use --onchain-uri (inline SVG) or off-chain custody (--backend ipfs|arweave|cloud) instead. Tracked as a follow-up.');
|
|
2024
|
-
}
|
|
2025
2090
|
if (!flags.dir) {
|
|
2026
2091
|
throw new Error('abx deploy-series --copies <n|open> --dir <media-dir> [--count N] [--mint-all | --mint-count N | --no-mint] [--mint-amount N] ' +
|
|
2027
2092
|
'[--onchain-uri (inline SVG) | --backend ipfs|arweave|cloud (off-chain, hosted resolver) | --public-base-url https://…] ' +
|
|
@@ -2044,6 +2109,15 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2044
2109
|
assertPreviewDeployer(flags);
|
|
2045
2110
|
assertRealIdentity(flags, { name, symbol, dryRun });
|
|
2046
2111
|
const lane = laneFromFlags(flags);
|
|
2112
|
+
const onchainImage = !!flags['onchain-image'];
|
|
2113
|
+
const compress = parseCompress(flags.compress);
|
|
2114
|
+
// Staging is a SEQUENCE (chunk writes → the deploy that references each manifest) where every tx's
|
|
2115
|
+
// receipt feeds the next, so it can't be signed offline in one run. Refused on the cold lane for the
|
|
2116
|
+
// same reason on every lineage, 721 and edition alike; hot + wallet both work.
|
|
2117
|
+
if (onchainImage && lane === 'unsigned') {
|
|
2118
|
+
throw new Error('Staging on-chain images (--onchain-image) needs interactive signing — each chunk tx feeds ' +
|
|
2119
|
+
"the next, so it can't run on the cold lane (--unsigned). Use the hot lane (a funded key) or --sign (browser wallet).");
|
|
2120
|
+
}
|
|
2047
2121
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
2048
2122
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
2049
2123
|
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun, '1155C');
|
|
@@ -2060,7 +2134,12 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2060
2134
|
if (effectiveMintCount > 0 && mintAmount === 0n) {
|
|
2061
2135
|
throw new Error('--mint-amount 0 with ids being pre-minted at deploy makes no sense — pass a positive --mint-amount, or drop --mint-all/--mint-count to defer minting entirely.');
|
|
2062
2136
|
}
|
|
2063
|
-
|
|
2137
|
+
// `--onchain-image` IMPLIES on-chain resolution — bytes on-chain behind a renderer that has no
|
|
2138
|
+
// reason to defer to a server. Every sibling lane already reads it that way (the 1/1, the
|
|
2139
|
+
// 1/1-edition, the 721 Series); this one didn't, which was harmless only while `--onchain-image`
|
|
2140
|
+
// was refused here. Now that it's wired, omitting it would demand a `--public-base-url` for a drop
|
|
2141
|
+
// whose bytes are already on-chain — incoherent, and it would bake a resolver URL nobody needs.
|
|
2142
|
+
const onChainUri = !!flags['onchain-uri'] || onchainImage;
|
|
2064
2143
|
const minter = flags.minter ?? zeroAddress;
|
|
2065
2144
|
const paused = flags.unpaused === undefined;
|
|
2066
2145
|
const primaryPayee = flags['primary-payee'] ?? zeroAddress;
|
|
@@ -2087,6 +2166,10 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2087
2166
|
if (onChainUri) {
|
|
2088
2167
|
step('On-chain renderer');
|
|
2089
2168
|
renderer = dryRun ? (rendererAddress(flags.renderer) ?? zeroAddress) : await ensureRenderer(flags.renderer);
|
|
2169
|
+
// Say WHICH renderer, like the 1/1 lane does. Both series lanes printed the step header with
|
|
2170
|
+
// nothing under it, while the skill's confirm-readout template implies an address is quotable
|
|
2171
|
+
// there — so an agent relaying the readout had a blank line to explain.
|
|
2172
|
+
info(renderer === zeroAddress ? 'would deploy the canonical renderer first' : `${dryRun ? 'would use' : 'using'} renderer ${renderer}`);
|
|
2090
2173
|
}
|
|
2091
2174
|
if (!onChainUri) {
|
|
2092
2175
|
const loopback = loopbackBaseUrl(baseUrl);
|
|
@@ -2109,22 +2192,118 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2109
2192
|
const overrides = storageOverrides(flags);
|
|
2110
2193
|
const opts = storageOptions(overrides);
|
|
2111
2194
|
const backendId = overrides.backend ?? process.env.ABX_STORAGE_BACKEND ?? 'fs';
|
|
2112
|
-
|
|
2195
|
+
// Pattern 2 — image off-chain on a durable backend, JSON rendered on-chain, NOTHING to run. Ported
|
|
2196
|
+
// from the 721 Series twin, which has had it all along; `EditionImage` shipped without it, so a
|
|
2197
|
+
// multi-artwork RASTER edition had no no-server option at all and `--onchain-uri` just refused the
|
|
2198
|
+
// art. That pushed a cold agent into deploying one contract PER artwork to get permanence (B28).
|
|
2199
|
+
// Nothing on-chain needed changing: `EditionImageInitParams` already takes `contractFields`, and the
|
|
2200
|
+
// renderer's `url-template` substitutes `{id}` from whatever id it is called with — `uri(id)` on a
|
|
2201
|
+
// 1155 reaches it exactly as `tokenURI(id)` does on a 721.
|
|
2202
|
+
const directUrlBackend = backendId === 'ipfs' || backendId === 'arweave' || backendId === 'cloud';
|
|
2203
|
+
const offchainImageOnchainJson = onChainUri && !onchainImage && directUrlBackend;
|
|
2204
|
+
const backend = (offchainImageOnchainJson || (!onChainUri && !onchainImage)) && !dryRun ? resolveBackend(opts) : undefined;
|
|
2113
2205
|
const tokenPaths = slots.map((s, i) => ({ tokenId: i, name: s, path: joinPath(dirPath, s) }));
|
|
2206
|
+
// Say WHICH backend holds the bytes, exactly as the 1/1 lane does. Both Series lanes resolved a
|
|
2207
|
+
// backend and never named it, so a creator asking for "images on IPFS" had no way to confirm from
|
|
2208
|
+
// the preview that their collection would actually pin there — the flag was accepted in silence.
|
|
2209
|
+
if (!onchainImage && (offchainImageOnchainJson || !onChainUri)) {
|
|
2210
|
+
const sr = backendResolution(overrides);
|
|
2211
|
+
info(`storage: ${sr.backend} ${dim(`(${sr.source === 'flag' ? '--backend' : sr.source === 'env' ? 'env' : 'default'})`)} — byte custody for the image(s)`);
|
|
2212
|
+
const gwWarn = localGatewayWarning(flags);
|
|
2213
|
+
if (gwWarn)
|
|
2214
|
+
warn(gwWarn);
|
|
2215
|
+
}
|
|
2114
2216
|
const seriesTraits = parseSeriesTraits(flags.attributes ? readFileSync(resolvePath(process.cwd(), flags.attributes), 'utf8') : undefined, slots);
|
|
2115
2217
|
const seriesTraitsOnchain = seriesTraits.size > 0 && (!!flags['traits-onchain'] || onChainUri);
|
|
2116
2218
|
const offChainTokenTraits = seriesTraits.size && !seriesTraitsOnchain
|
|
2117
2219
|
? JSON.stringify(Object.fromEntries([...seriesTraits].map(([id, a]) => [String(id), a])))
|
|
2118
2220
|
: undefined;
|
|
2221
|
+
// Wallet-signature count for THIS deploy — parity with the 721 Series twin, which prints it and
|
|
2222
|
+
// whose count the skill promises "every preview" shows. Same per-id chunk math as that twin, so the
|
|
2223
|
+
// preview, the confirm text, and the wallet session's `total` can never disagree.
|
|
2224
|
+
const approvals = onchainImage
|
|
2225
|
+
? tokenPaths.reduce((n, { path }) => {
|
|
2226
|
+
const plan = computeContentPlan(readFileSync(path), compress).plan;
|
|
2227
|
+
return n + (plan.mode === 'single' ? 1 : plan.txCount);
|
|
2228
|
+
}, 0) + 1 // + the deploy tx
|
|
2229
|
+
: 1;
|
|
2119
2230
|
const tokenFields = [];
|
|
2120
|
-
|
|
2231
|
+
// Collection-scope fields the image lane may add (the O(1) `url-template`). Kept separate from the
|
|
2232
|
+
// authorship fields and CONCATENATED at the params site — never assigned over them, or an
|
|
2233
|
+
// `--artist`/`--license` value would silently vanish whenever pattern 2 is in play.
|
|
2234
|
+
let imageContractFields = [];
|
|
2235
|
+
const buildFields = async (stage) => {
|
|
2236
|
+
// Pattern 1: the bytes themselves on-chain, staged into the shared chunk store per id, each id's
|
|
2237
|
+
// field pointing at its manifest. Ported from the 721 Series twin — the edition lane refused this
|
|
2238
|
+
// outright before (B28), though nothing on-chain prevented it.
|
|
2239
|
+
if (onchainImage) {
|
|
2240
|
+
const { fields } = await stageImageFieldsBatch(tokenPaths.map((s) => s.path), compress, stage, flags['chunk-store']);
|
|
2241
|
+
tokenPaths.forEach(({ tokenId }, i) => tokenFields.push(tokenFieldOf(tokenId, fields[i])));
|
|
2242
|
+
if (seriesTraitsOnchain)
|
|
2243
|
+
for (const [tokenId, attrs] of seriesTraits)
|
|
2244
|
+
tokenFields.push(tokenFieldOf(tokenId, attributesInlineField(attrs)));
|
|
2245
|
+
info(`uri()/contractURI() resolve ON-CHAIN via the renderer — the image BYTES are on-chain too, nothing off-chain at all.`);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
// Pattern 2: one durable upload per id, then a locator on-chain instead of the bytes.
|
|
2249
|
+
if (offchainImageOnchainJson) {
|
|
2250
|
+
const exts = tokenPaths.map(({ path }) => extname(path).toLowerCase());
|
|
2251
|
+
const uniform = exts[0] !== '' && exts.every((e) => e === exts[0]);
|
|
2252
|
+
if (dryRun || !backend) {
|
|
2253
|
+
if (uniform) {
|
|
2254
|
+
imageContractFields = [imageUrlTemplateField(`<${backendId}-dir>/{id}${exts[0]}`)];
|
|
2255
|
+
info(`would upload ${tokenPaths.length} file(s) as ONE ${backendId} directory → collection image url-template <${backendId}-dir>/{id}${exts[0]} (O(1))`);
|
|
2256
|
+
}
|
|
2257
|
+
else {
|
|
2258
|
+
for (const { tokenId } of tokenPaths)
|
|
2259
|
+
tokenFields.push(tokenFieldOf(tokenId, imageUrlField(`<${backendId}-url-${tokenId}>`)));
|
|
2260
|
+
info(`would upload ${tokenPaths.length} file(s) to ${backendId} → per-id url image fields (mixed extensions, O(N))`);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
else if (uniform && backend.putDirectory) {
|
|
2264
|
+
// O(1) directory-base: upload the folder renamed to {id}{ext}; ONE collection url-template.
|
|
2265
|
+
const entries = tokenPaths.map(({ tokenId, path }) => ({ name: `${tokenId}${exts[0]}`, bytes: new Uint8Array(readFileSync(path)), contentType: contentTypeFromPath(path) }));
|
|
2266
|
+
const { base } = await backend.putDirectory(entries);
|
|
2267
|
+
const template = `${base}/{id}${exts[0]}`;
|
|
2268
|
+
imageContractFields = [imageUrlTemplateField(template)];
|
|
2269
|
+
info(`uploaded ${entries.length} file(s) as one ${backendId} directory`);
|
|
2270
|
+
info(`collection image (url-template): ${template} ${dim('— one field covers every id (O(1))')}`);
|
|
2271
|
+
}
|
|
2272
|
+
else {
|
|
2273
|
+
// mixed extensions (or no directory support): per-id url fields (O(N)).
|
|
2274
|
+
for (const { tokenId, name: fname, path } of tokenPaths) {
|
|
2275
|
+
const bytes = new Uint8Array(readFileSync(path));
|
|
2276
|
+
const hash = hashContent(bytes);
|
|
2277
|
+
await backend.put(hash, { bytes, contentType: contentTypeFromPath(path) });
|
|
2278
|
+
const locator = (await backend.locator?.(hash)) ?? '';
|
|
2279
|
+
if (!locator)
|
|
2280
|
+
throw new Error(`backend ${backendId} returned no public locator for id ${tokenId} — a durable backend (ipfs/arweave/cloud) is required for --onchain-uri image hosting`);
|
|
2281
|
+
tokenFields.push(tokenFieldOf(tokenId, imageUrlField(locator)));
|
|
2282
|
+
info(`id ${tokenId} ← ${fname} → ${locator}`);
|
|
2283
|
+
}
|
|
2284
|
+
info(`mixed file extensions → per-id url fields (O(N)); a uniform extension enables the O(1) directory template`);
|
|
2285
|
+
}
|
|
2286
|
+
if (seriesTraitsOnchain)
|
|
2287
|
+
for (const [tokenId, attrs] of seriesTraits)
|
|
2288
|
+
tokenFields.push(tokenFieldOf(tokenId, attributesInlineField(attrs)));
|
|
2289
|
+
info(`uri()/contractURI() resolve ON-CHAIN via the renderer — the image bytes live on ${backendId}, so there is still no server to run.`);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2121
2292
|
for (const { tokenId, name: fname, path } of tokenPaths) {
|
|
2122
2293
|
const bytes = new Uint8Array(readFileSync(path));
|
|
2123
2294
|
const contentType = contentTypeFromPath(path);
|
|
2124
2295
|
if (onChainUri) {
|
|
2125
2296
|
const text = Buffer.from(bytes).toString('utf8');
|
|
2126
2297
|
if (!looksLikeSvg(text)) {
|
|
2127
|
-
|
|
2298
|
+
// Name the real alternatives, the way the 721 twin's version of this refusal does — a bare
|
|
2299
|
+
// "drop --onchain-uri" once sent a cold agent fanning a 3-artwork edition out into THREE
|
|
2300
|
+
// separate contracts to find permanence. The `--backend` route is now the first suggestion
|
|
2301
|
+
// because it IS available on this lane (see `offchainImageOnchainJson` above).
|
|
2302
|
+
throw new Error(`--onchain-uri inlines SVG only; id ${tokenId} "${fname}" is ${contentType}. For raster art in ONE edition contract:\n` +
|
|
2303
|
+
` • No server, images off-chain: add --backend arweave (or ipfs/cloud) — the image URL is baked into the on-chain JSON\n` +
|
|
2304
|
+
` • Host the metadata yourself: drop --onchain-uri and pass --public-base-url https://your.domain\n` +
|
|
2305
|
+
` • Keep it SVG: supply SVG art and --onchain-uri inlines every id on-chain\n` +
|
|
2306
|
+
` • Bytes fully on-chain: --onchain-image --compress fastlz (hot or --sign lane; --unsigned is refused)`);
|
|
2128
2307
|
}
|
|
2129
2308
|
tokenFields.push(tokenFieldOf(tokenId, imageInlineField(text)));
|
|
2130
2309
|
}
|
|
@@ -2139,14 +2318,24 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2139
2318
|
if (seriesTraitsOnchain)
|
|
2140
2319
|
for (const [tokenId, attrs] of seriesTraits)
|
|
2141
2320
|
tokenFields.push(tokenFieldOf(tokenId, attributesInlineField(attrs)));
|
|
2321
|
+
// AFTER the per-id content actually validated — stating "resolves ON-CHAIN, no server" and then
|
|
2322
|
+
// refusing the very art that was passed reads as the tool contradicting itself.
|
|
2323
|
+
if (onChainUri) {
|
|
2324
|
+
info(`uri()/contractURI() resolve ON-CHAIN via the renderer — no resolver, no server, no localhost.`);
|
|
2325
|
+
if (!hasPublicUrl)
|
|
2326
|
+
info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
|
|
2327
|
+
}
|
|
2142
2328
|
};
|
|
2143
2329
|
if (!dryRun) {
|
|
2144
2330
|
const custody = onChainUri ? 'inline SVG on-chain' : `off-chain custody (${backendId})`;
|
|
2145
2331
|
await confirmSend(`About to deploy an edition series "${name}" (${symbol}) — ${count} id(s) × ${editionSize === 0n ? 'open' : editionSize.toString()} cop${editionSize === 1n ? 'y' : 'ies'} each; ${custody}; ` +
|
|
2146
|
-
`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} at deploy` : 'deferred'}; owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
2332
|
+
`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} at deploy` : 'deferred'}; approvals: ${approvals} wallet approval(s); owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
2147
2333
|
(transferValidator !== zeroAddress ? ` ERC-1155C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
2148
2334
|
}
|
|
2149
|
-
|
|
2335
|
+
// `--onchain-image` defers field-building to a lane branch below: the hot lane stages with the env
|
|
2336
|
+
// key, the wallet lane inside its sign session. Every other custody mode can build up front.
|
|
2337
|
+
if (!onchainImage)
|
|
2338
|
+
await buildFields();
|
|
2150
2339
|
const buildForDeployer = async (deployer) => {
|
|
2151
2340
|
const salt = explicitSalt ?? saltFor(deployer);
|
|
2152
2341
|
const clone = await predictClone(publicClient, { factory, salt });
|
|
@@ -2170,7 +2359,9 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2170
2359
|
mintCount: effectiveMintCount,
|
|
2171
2360
|
mintAmount,
|
|
2172
2361
|
tokenFields,
|
|
2173
|
-
|
|
2362
|
+
// Concatenate — the image lane's collection-scope url-template rides ALONGSIDE the authorship
|
|
2363
|
+
// fields. Assigning either over the other would silently drop the creator's --artist/--license.
|
|
2364
|
+
contractFields: [...authorshipContractFields(flags), ...imageContractFields],
|
|
2174
2365
|
};
|
|
2175
2366
|
return { clone, params, salt };
|
|
2176
2367
|
};
|
|
@@ -2204,6 +2395,10 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2204
2395
|
info(`minter ${minter}`);
|
|
2205
2396
|
if (primaryPayee !== zeroAddress)
|
|
2206
2397
|
info(`primary payee ${primaryPayee}`);
|
|
2398
|
+
// `paused` gates whether the public can mint at all — the 1/1-edition twin prints it and an
|
|
2399
|
+
// edition sale is the point of the lane, so it belongs in the readout, not just in the params.
|
|
2400
|
+
info(`paused ${paused}${flags.unpaused ? '' : dim(' (default — pass --unpaused to open at deploy)')}`);
|
|
2401
|
+
info(`approvals ${approvals} wallet approval(s)`); // parity with the 721 Series twin
|
|
2207
2402
|
if (!explicitSalt) {
|
|
2208
2403
|
console.log(`\n ${bold('salt')} ${g(salt)}`);
|
|
2209
2404
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
|
|
@@ -2214,6 +2409,9 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2214
2409
|
}
|
|
2215
2410
|
if (lane === 'send') {
|
|
2216
2411
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
2412
|
+
// hot lane: the env key stages every id up front (deployer-independent), then deploys.
|
|
2413
|
+
if (onchainImage)
|
|
2414
|
+
await buildFields(envStagingSender());
|
|
2217
2415
|
const { clone: predicted, params, salt } = await buildForDeployer(account.address);
|
|
2218
2416
|
info(`deterministic address: ${predicted}`);
|
|
2219
2417
|
info(`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${account.address} at deploy` : 'deferred'}`);
|
|
@@ -2224,6 +2422,35 @@ export async function cmdDeployEditionImageBody(flags, emit) {
|
|
|
2224
2422
|
ok(`deployed ${clone}`);
|
|
2225
2423
|
info(`tx ${explorerBase()}/tx/${r.txHash} (block ${blockNumber})`);
|
|
2226
2424
|
}
|
|
2425
|
+
else if (lane === 'sign' && onchainImage) {
|
|
2426
|
+
// Wallet lane + on-chain staging: ONE session signs every chunk write across all ids AND the
|
|
2427
|
+
// deploy. The connecting wallet pays for (and owns) it; staging can't precede the connect, so it
|
|
2428
|
+
// runs inside the session and the deploy then bakes in every id's manifest.
|
|
2429
|
+
info(`a wallet will become the owner; it will approve ${approvals - 1} staging tx(s) + the deploy in one session.`);
|
|
2430
|
+
const session = await openWalletSession({
|
|
2431
|
+
chainKey: CHAIN,
|
|
2432
|
+
expectedSigner: flags.for,
|
|
2433
|
+
total: approvals, // the SAME staging-tx math the preview's `approvals` line reports
|
|
2434
|
+
port: flags.port ? Number(flags.port) : undefined,
|
|
2435
|
+
signUrlFile: flags['sign-url-file'],
|
|
2436
|
+
});
|
|
2437
|
+
let r;
|
|
2438
|
+
try {
|
|
2439
|
+
const signer = await session.connect();
|
|
2440
|
+
await buildFields(sessionStagingSender(session));
|
|
2441
|
+
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
2442
|
+
info(`mint: ${effectiveMintCount > 0 ? `${effectiveMintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${signer} at deploy` : 'deferred'}`);
|
|
2443
|
+
const sent = await session.send(prepareDeployEditionImage({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted }));
|
|
2444
|
+
r = { txHash: sent.txHash, blockNumber: sent.receipt.blockNumber };
|
|
2445
|
+
clone = predicted;
|
|
2446
|
+
}
|
|
2447
|
+
finally {
|
|
2448
|
+
session.close();
|
|
2449
|
+
}
|
|
2450
|
+
blockNumber = r.blockNumber;
|
|
2451
|
+
ok(`deployed ${clone}`);
|
|
2452
|
+
info(`tx ${explorerBase()}/tx/${r.txHash} (block ${blockNumber})`);
|
|
2453
|
+
}
|
|
2227
2454
|
else {
|
|
2228
2455
|
info(onChainUri ? 'a wallet will become the owner; the tokens resolve from chain — no URI base is baked in.' : `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
2229
2456
|
const result = await signTx(async (signer) => {
|
|
@@ -3657,10 +3884,6 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
3657
3884
|
}
|
|
3658
3885
|
if (!flags.script)
|
|
3659
3886
|
throw new Error(usage);
|
|
3660
|
-
if (flags.dep || flags['dep-registry']) {
|
|
3661
|
-
throw new Error('--dep/--dep-registry are not yet supported for edition code deploys (--copies) — ship a self-contained script ' +
|
|
3662
|
-
'(no external dependency declarations) for now, or drop --copies for the 721 lane. Tracked as a follow-up.');
|
|
3663
|
-
}
|
|
3664
3887
|
if (flags['image-renderer'] || flags['attributes-renderer'] || flags['image-base']) {
|
|
3665
3888
|
throw new Error('--image-renderer/--attributes-renderer/--image-base are not yet supported for edition code deploys (--copies). Tracked as a follow-up.');
|
|
3666
3889
|
}
|
|
@@ -3763,6 +3986,43 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
3763
3986
|
info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
|
|
3764
3987
|
}
|
|
3765
3988
|
const schemas = parseSchemaSpecs(flags.schema);
|
|
3989
|
+
// Dependencies — same shape as the 721 SeriesCode twin (see its block for the full reasoning).
|
|
3990
|
+
// `EditionCode` already inherits the `Dependencies` extension and calls `_initDependencies()`, and
|
|
3991
|
+
// the legs are the same `setDependency`/`setDependencyRegistry` calls on the same ABI, so this was
|
|
3992
|
+
// never a contract gap: only the CLI refused it (B28). It matters because "an edition of my p5
|
|
3993
|
+
// sketch, with p5 coming from the chain" is a thing creators ask for directly, and the refusal sent
|
|
3994
|
+
// them to the 721 lane (unique tokens) or to shipping a sketch whose library never loads.
|
|
3995
|
+
const deps = parseDepFlag(flags.dep);
|
|
3996
|
+
const hasRegistryDeps = deps.some((d) => d.resolution === DEP_RESOLUTION.registry);
|
|
3997
|
+
const depPointer = hasRegistryDeps
|
|
3998
|
+
? resolveDepRegistryPointer(flags['dep-registry'], chainId)
|
|
3999
|
+
: { registry: null, source: 'none' };
|
|
4000
|
+
const depRegistry = depPointer.registry;
|
|
4001
|
+
if (deps.length) {
|
|
4002
|
+
step('Dependencies');
|
|
4003
|
+
deps.forEach((d, i) => info(`[${i}] ${bold(d.display)} ${dim(d.resolution === DEP_RESOLUTION.registry ? '(registry name@version)' : '(on-chain data contract — read directly)')}${i === 0 ? dim(' · index 0 = the runtime') : ''}`));
|
|
4004
|
+
if (hasRegistryDeps) {
|
|
4005
|
+
if (depRegistry) {
|
|
4006
|
+
info(`registry pointer → ${depRegistry} ${dim(depPointer.source === 'flag' ? '(--dep-registry)' : "(the chain's AB Dependency Registry — soft, non-validating)")}`);
|
|
4007
|
+
const { checks, rpcOk } = await checkRegistryDeps(publicClient, depRegistry, deps);
|
|
4008
|
+
for (const chk of checks) {
|
|
4009
|
+
if (chk.status === 'found') {
|
|
4010
|
+
ok(chk.details.availableOnChain
|
|
4011
|
+
? `${chk.dep} — on registry; ON-CHAIN bytes available (${chk.details.scriptCount} chunk(s)) — chain-complete capable`
|
|
4012
|
+
: `${chk.dep} — on registry; served from CDN ${chk.details.preferredCDN || '(none listed)'} ${dim('— the normal production path, not a degradation')}`);
|
|
4013
|
+
}
|
|
4014
|
+
else if (chk.status === 'not-found') {
|
|
4015
|
+
warn(`${bold(chk.dep)} NOT FOUND on registry ${depRegistry} — the resolver won't resolve it from there. Deploy proceeds (you may intend a custom registry / a pending addition); double-check the exact name@version spelling.`);
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
if (!rpcOk)
|
|
4019
|
+
info('registry check skipped (RPC unreachable) — the deploy does not depend on it.');
|
|
4020
|
+
}
|
|
4021
|
+
else {
|
|
4022
|
+
warn('no dependency registry known for this chain — skipping the setDependencyRegistry leg (the pointer is SOFT; the resolver falls back to its built-in CDN map). Pass --dep-registry 0x… or run `abx set-dependency-registry` later.');
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
3766
4026
|
step('Content');
|
|
3767
4027
|
const scriptPath = flags.script;
|
|
3768
4028
|
const source = readFileSync(resolvePath(scriptPath), 'utf8');
|
|
@@ -3823,6 +4083,10 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
3823
4083
|
args: [encodeTag(s.key), s.paramType, s.auth, s.authAddress, s.lockAfter, s.min, s.max, s.selectOptions],
|
|
3824
4084
|
}));
|
|
3825
4085
|
}
|
|
4086
|
+
// Dependency legs, ordered (index 0 = the runtime) + the SOFT registry pointer when one resolved.
|
|
4087
|
+
// Built by the SDK's shared `dependencySetupCalls` — the same function the 721 lane uses — so the
|
|
4088
|
+
// two lanes can never encode `setDependency` differently.
|
|
4089
|
+
calls.push(...dependencySetupCalls(deps, depRegistry));
|
|
3826
4090
|
calls.push(...onchainUriLegs);
|
|
3827
4091
|
// Reserve mints: one `mint(to, id, amount)` per premint id — the edition twin of the 721 lane's
|
|
3828
4092
|
// N identical `mint(owner)` calls, one id finer (a specific amount of copies, not just "one").
|
|
@@ -3849,7 +4113,11 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
3849
4113
|
}
|
|
3850
4114
|
return { clone, txs };
|
|
3851
4115
|
};
|
|
3852
|
-
|
|
4116
|
+
// deps ride the same single setup multicall as chunks/schemas/mints, so they don't add an approval —
|
|
4117
|
+
// but they must be COUNTED in the "is there a setup multicall at all" test, or a deps-only project
|
|
4118
|
+
// (a script with no schemas and no premints) would report 1 approval and send 2 transactions.
|
|
4119
|
+
const depLegCount = dependencySetupCalls(deps, depRegistry).length;
|
|
4120
|
+
const approvals = 1 + (scriptChunks.length + schemas.length + depLegCount + onchainUriLegs.length + mintCount > 0 ? 1 : 0);
|
|
3853
4121
|
if (!dryRun) {
|
|
3854
4122
|
await confirmSend(`About to deploy an edition code project "${name}" (${symbol}) — script ${bytes.length} bytes → ${scriptChunks.length} chunk(s); ` +
|
|
3855
4123
|
`up to ${max} id(s) × ${editionSize === 0n ? 'open' : editionSize.toString()} cop${editionSize === 1n ? 'y' : 'ies'} each; ` +
|
|
@@ -3885,6 +4153,7 @@ export async function cmdDeployEditionCodeBody(flags, emit) {
|
|
|
3885
4153
|
info(`PostParam schema(s): ${schemas.length ? schemas.map((s) => describeSchema(s)).join(', ') : 'none'}`);
|
|
3886
4154
|
info(`mint: ${mintCount > 0 ? `${mintCount} id(s) × ${mintAmount} cop${mintAmount === 1n ? 'y' : 'ies'} → ${deployer ?? 'your wallet'} at deploy` : 'deferred (mint later / external minter)'}`);
|
|
3887
4155
|
info(`transactions: 2 — deploy + setup multicall (script chunks${schemas.length ? ' + schemas' : ''}${onChainUri ? ' + on-chain URI wiring' : ''}${mintCount > 0 ? ' + reserve mints' : ''})`);
|
|
4156
|
+
info(`approvals ${approvals} wallet approval(s)`); // parity with the 721 code twin — it had this only in the --confirm sentence
|
|
3888
4157
|
if (!explicitSalt && deployer) {
|
|
3889
4158
|
console.log(`\n ${bold('salt')} ${g(salt)}`);
|
|
3890
4159
|
info(`address: pinned by salt — re-run with ${bold(`--salt ${salt}`)} (same address).`);
|