@artblocks/abx-cli 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +1243 -0
- package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +17 -0
- package/assets/renderer-scaffold/test/MyRenderer.t.sol +51 -2
- package/dist/flags.d.ts +7 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +19 -0
- package/dist/flags.js.map +1 -1
- package/dist/main.js +267 -35
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +53 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +181 -13
- package/dist/ownerops.js.map +1 -1
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +52 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +7 -6
- package/skill/SKILL.md +6 -2
- package/skill/reference/code-projects.md +4 -3
- package/skill/reference/creator-token.md +71 -0
- package/skill/reference/operating.md +2 -0
package/dist/main.js
CHANGED
|
@@ -58,11 +58,12 @@ import { encodeFunctionData } from 'viem';
|
|
|
58
58
|
import { DEP_RESOLUTION, deploySeedSource, deploySeriesCodeFactory, prepareCodeSetup, prepareDeploySeriesCode, resolveGenerator, resolveSeedSource, resolveSeriesCodeFactory, seriesCodeAbi, readParamSchema, seriesCodeFactoryAbi, } from '@artblocks/abx-sdk';
|
|
59
59
|
import { checkRegistryDeps, dependencySetupCalls, parseDepFlag, resolveDepRegistryPointer } from './deps.js';
|
|
60
60
|
import { expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport, readLegacyParamsKeys, readParamSchemaKeys, readSetParamKeys, } from './onchain-uri.js';
|
|
61
|
-
import { parseFlags, unknownFlags } from './flags.js';
|
|
61
|
+
import { parseFlags, positionalArgs, unknownFlags } from './flags.js';
|
|
62
62
|
import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, readSkillVersion, SKILL_DIR_NAME, } from './update-check.js';
|
|
63
63
|
import { analyzeScript, recommendLane } from './inspect.js';
|
|
64
64
|
import { previewConfigFromFlags, previewDepTags, parsePreviewParams, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
65
65
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
66
|
+
import { copyRendererScaffold } from './scaffold.js';
|
|
66
67
|
import { declinesSkillInstall } from './prompt.js';
|
|
67
68
|
import { pinGas, waitForCodeAt } from './gas.js';
|
|
68
69
|
import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
|
|
@@ -74,11 +75,11 @@ const SCHEMA_CATALOG = 'Types: Bool·Select·Uint256Range·Int256Range·DecimalR
|
|
|
74
75
|
'Select needs options — key:Select[A|B|C]:Auth; a Range takes bounds — key:Uint256Range[0..100]:Auth. ' +
|
|
75
76
|
'A palette collectors set = palette:HexColor:TokenOwner';
|
|
76
77
|
import { uploadAndLocate } from './upload.js';
|
|
77
|
-
import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, resolveRpcUrl, redactRpcUrl, explorerUrl, DEFAULT_CHAIN_KEY, KNOWN_CHAIN_KEYS, normalizeAttributes, parseTraitPairs, METADATA_FIELD as F, METADATA_REPRESENTATION as R, indexProgress, isAccepted, AbxIndexTimeoutError, AbxServiceError, } from '@artblocks/abx-sdk';
|
|
78
|
+
import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, resolveRpcUrl, redactRpcUrl, explorerUrl, DEFAULT_CHAIN_KEY, KNOWN_CHAIN_KEYS, normalizeAttributes, parseTraitPairs, resolveRecommendedTransferValidator, CREATOR_TOKEN_INTERFACE_ID, METADATA_FIELD as F, METADATA_REPRESENTATION as R, indexProgress, isAccepted, AbxIndexTimeoutError, AbxServiceError, } from '@artblocks/abx-sdk';
|
|
78
79
|
import { SelfHostIndexer, SqliteStore } from '@artblocks/abx-indexer';
|
|
79
80
|
import { artContentHash, currentRenderArtifact, generateArt, resolveBaseUrl, startChainWatcher, startTokenApiServer, verifyProject, watchIntervalMs, DEFAULT_PORT, } from '@artblocks/abx-token-api';
|
|
80
81
|
import { ARWEAVE_FREE_UPLOAD_LIMIT, arweaveAddress, arweaveFunding, contentTypeFromPath, hashContent, resolveBackend, turboBalanceForAddress, turboUploadCostUsd, turboUploadWinc } from '@artblocks/abx-storage';
|
|
81
|
-
import { cmdTransfer, cmdMint, cmdSetMinter, cmdSetMaxInvocations, cmdConfigureParam, cmdSetSchema, cmdRetireParam, cmdSetParamHooks, cmdSetDependency, cmdRemoveLastDependency, cmdSetDependencyRegistry, cmdLockDependencies, cmdSetPrimaryPayee, cmdPause, cmdUnpause, cmdRefresh, cmdSetTokenUri, cmdSetContractUri, cmdSetRoyalty, cmdSetField, cmdAttach, cmdLockField, cmdSetRenderer, cmdLockUri, cmdSetAdmin, cmdMinterConfigure, cmdMinterShow, cmdMinterBuy, computeContentPlan, envStagingSender, laneFromFlags, ONCHAIN_PROJECT_SOFT_LIMIT, parseCompress, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, authorshipContractFields, AUTHORSHIP_DEPLOY_FIELDS, } from './ownerops.js';
|
|
82
|
+
import { cmdTransfer, cmdMint, cmdSetMinter, cmdSetMaxInvocations, cmdConfigureParam, cmdSetSchema, cmdRetireParam, cmdSetParamHooks, cmdSetDependency, cmdRemoveLastDependency, cmdSetDependencyRegistry, cmdLockDependencies, cmdSetPrimaryPayee, cmdPause, cmdUnpause, cmdRefresh, cmdSetTokenUri, cmdSetContractUri, cmdSetRoyalty, cmdSetTransferValidator, cmdSetField, cmdAttach, parseTransferValidatorValue, cmdLockField, cmdSetRenderer, cmdLockUri, cmdSetAdmin, cmdMinterConfigure, cmdMinterShow, cmdMinterBuy, computeContentPlan, envStagingSender, laneFromFlags, ONCHAIN_PROJECT_SOFT_LIMIT, parseCompress, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, authorshipContractFields, AUTHORSHIP_DEPLOY_FIELDS, } from './ownerops.js';
|
|
82
83
|
import { openWalletSession, signTx } from './signer.js';
|
|
83
84
|
import { describeRemoteError, listConfiguredRemotes, misnamedRemoteVars, requireRemoteToken, resolveRemote, serviceClient, tokenSourceLabel } from './remote.js';
|
|
84
85
|
import { buildMigrationPlan, repinNodeCustody, verifyParity } from './migrate.js';
|
|
@@ -161,9 +162,16 @@ async function maybeNotifyUpdate(flags) {
|
|
|
161
162
|
}
|
|
162
163
|
const latest = await checkForCliUpdate(current);
|
|
163
164
|
if (latest) {
|
|
165
|
+
// The notes pointer must RESOLVE. This printed github.com/ArtBlocks/abx/releases, which 404s
|
|
166
|
+
// for everyone outside the org (the repo is private) — so a tester reconstructed the diff by
|
|
167
|
+
// running the same dry run on both versions, which is how they discovered the canonical
|
|
168
|
+
// singletons had moved and then had no way to tell whether that needed them to act. The
|
|
169
|
+
// packaged CHANGELOG.md ships with every install (see package.json `files`) and carries the
|
|
170
|
+
// real per-release notes, so it's readable offline and always matches the version you have.
|
|
164
171
|
console.error(`\n ${c.orange}⚠${c.reset} update available: ${bold('abx')} ${dim(current)} → ${g(latest)}\n` +
|
|
165
172
|
` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx @artblocks/abx-cli@latest <command>')}\n` +
|
|
166
|
-
` release notes:
|
|
173
|
+
` release notes: ${g('abx changelog')} ${dim('(ships with the CLI) · all versions: https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions')}\n` +
|
|
174
|
+
` ${dim('silence: ABX_NO_UPDATE_CHECK=1')}\n`);
|
|
167
175
|
}
|
|
168
176
|
}
|
|
169
177
|
catch {
|
|
@@ -212,13 +220,14 @@ async function main() {
|
|
|
212
220
|
case 'inspect': return cmdInspect(rest[0], flags);
|
|
213
221
|
case 'preview': return cmdPreview(flags);
|
|
214
222
|
case 'scaffold-renderer': return cmdScaffoldRenderer(rest, flags);
|
|
223
|
+
case 'changelog': return cmdChangelog(flags);
|
|
215
224
|
case 'predict': return cmdPredict(flags);
|
|
216
225
|
case 'add': return cmdAdd(rest[0], flags);
|
|
217
226
|
case 'index': return cmdIndex(rest[0], flags);
|
|
218
227
|
case 'verify': return cmdVerify(rest[0], flags);
|
|
219
228
|
case 'render': return cmdRender(rest[0], rest.slice(1), flags);
|
|
220
229
|
case 'effects': return cmdEffects(flags);
|
|
221
|
-
case 'tokenuri': return cmdTokenUri(rest[0], flags);
|
|
230
|
+
case 'tokenuri': return cmdTokenUri(rest[0], flags, rest.slice(1));
|
|
222
231
|
case 'contracturi': return cmdContractUri(rest[0], flags);
|
|
223
232
|
case 'serve': return cmdServe(flags);
|
|
224
233
|
// owner operations — write + sign (hot/wallet/cold lane), then re-index
|
|
@@ -245,6 +254,7 @@ async function main() {
|
|
|
245
254
|
case 'deploy-effects': return cmdDeployEffects(flags);
|
|
246
255
|
case 'mint-page': return cmdMintPage(rest[0], flags);
|
|
247
256
|
case 'set-royalty': return cmdSetRoyalty(rest[0], flags);
|
|
257
|
+
case 'set-transfer-validator': return cmdSetTransferValidator(rest[0], rest.slice(1), flags);
|
|
248
258
|
case 'set-field': return cmdSetField(rest[0], flags);
|
|
249
259
|
case 'attach': return cmdAttach(rest, flags);
|
|
250
260
|
case 'lock-field': return cmdLockField(rest[0], flags);
|
|
@@ -445,6 +455,7 @@ function deployCommandLine(flags, salt) {
|
|
|
445
455
|
str('compress', flags.compress);
|
|
446
456
|
str('royalty-bps', flags['royalty-bps']);
|
|
447
457
|
bool('no-mint');
|
|
458
|
+
str('721c', flags['721c'] === 'true' ? 'recommended' : flags['721c']); // bare --721c ≡ recommended
|
|
448
459
|
str('backend', flags.backend);
|
|
449
460
|
str('gateway', flags.gateway);
|
|
450
461
|
str('bucket', flags.bucket);
|
|
@@ -488,6 +499,7 @@ function deploySeriesCommandLine(flags, salt) {
|
|
|
488
499
|
str('minter', flags.minter);
|
|
489
500
|
str('primary-payee', flags['primary-payee']);
|
|
490
501
|
str('royalty-bps', flags['royalty-bps']);
|
|
502
|
+
str('721c', flags['721c'] === 'true' ? 'recommended' : flags['721c']); // bare --721c ≡ recommended
|
|
491
503
|
if (flags.sign !== undefined)
|
|
492
504
|
parts.push('--sign');
|
|
493
505
|
if (flags.unsigned !== undefined)
|
|
@@ -528,6 +540,7 @@ function deployCodeCommandLine(flags, salt) {
|
|
|
528
540
|
str('minter', flags.minter);
|
|
529
541
|
str('primary-payee', flags['primary-payee']);
|
|
530
542
|
str('royalty-bps', flags['royalty-bps']);
|
|
543
|
+
str('721c', flags['721c'] === 'true' ? 'recommended' : flags['721c']); // bare --721c ≡ recommended
|
|
531
544
|
str('backend', flags.backend);
|
|
532
545
|
str('gateway', flags.gateway);
|
|
533
546
|
str('public-base-url', flags['public-base-url']);
|
|
@@ -1198,6 +1211,8 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1198
1211
|
` Nothing was deployed. Stop that one (Ctrl-C), or run this on another port: \`abx demo --port ${wanted + 1}\`.`);
|
|
1199
1212
|
}
|
|
1200
1213
|
}
|
|
1214
|
+
// (demo keeps its own message: "Nothing was deployed" is the load-bearing part here, and it must
|
|
1215
|
+
// be true — this check runs before anything irreversible. assertPortFree covers serve/preview.)
|
|
1201
1216
|
const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
|
|
1202
1217
|
// A keyless preview needs `--for` (the address is a pure function of factory+salt+deployer). Check
|
|
1203
1218
|
// it HERE, before the trust-anchor/content/plan steps print — hitting this after a wall of output
|
|
@@ -1210,6 +1225,8 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1210
1225
|
// a wrong-network RPC must still be caught with the clear mismatch message rather than failing
|
|
1211
1226
|
// opaquely inside predict; `allowUnreachable` keeps a genuinely offline dry-run previewable.
|
|
1212
1227
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
1228
|
+
// --721c (opt-in ERC-721C): absent → zeroAddress, a plain ERC-721 exactly as before.
|
|
1229
|
+
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun);
|
|
1213
1230
|
// `demo` is exempt: its whole job is a zero-argument first token.
|
|
1214
1231
|
if (!serveAfter)
|
|
1215
1232
|
assertRealIdentity(flags, { name, symbol, dryRun });
|
|
@@ -1458,6 +1475,7 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1458
1475
|
contractURIRenderer: renderer,
|
|
1459
1476
|
royaltyReceiver: deployer,
|
|
1460
1477
|
royaltyBps,
|
|
1478
|
+
transferValidator,
|
|
1461
1479
|
tokenFields,
|
|
1462
1480
|
// authorship + rights (artist / license / …) are collection-scope on-chain fields.
|
|
1463
1481
|
contractFields: authorshipContractFields(flags),
|
|
@@ -1512,7 +1530,8 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1512
1530
|
return;
|
|
1513
1531
|
}
|
|
1514
1532
|
// opt-in --confirm: a final y/N before the real send (no-op without --confirm; never blocks scripts)
|
|
1515
|
-
await confirmSend(`About to deploy 1/1 "${name}" (${symbol})${flags.image ? ' with your image' : ' (generative demo art)'}; mint: ${noMint ? 'deferred' : 'token #0 at deploy'}; owner+royalty: your wallet @ ${royaltyBps / 100}
|
|
1533
|
+
await confirmSend(`About to deploy 1/1 "${name}" (${symbol})${flags.image ? ' with your image' : ' (generative demo art)'}; mint: ${noMint ? 'deferred' : 'token #0 at deploy'}; owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
1534
|
+
(transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
1516
1535
|
if (lane === 'send') {
|
|
1517
1536
|
// hot lane: the env key is the deployer, known up front.
|
|
1518
1537
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
@@ -1767,7 +1786,7 @@ async function cmdDeploySeries(flags) {
|
|
|
1767
1786
|
if (!flags.dir) {
|
|
1768
1787
|
throw new Error('abx deploy-series --dir <media-dir> [--count N] [--mint-all | --mint-count N | --no-mint] ' +
|
|
1769
1788
|
'[--onchain-image [--compress fastlz] | --onchain-uri --backend ipfs|arweave|cloud (image off-chain, JSON on-chain; cloud needs --public-base) | --onchain-uri (inline SVG) | --public-base-url https://… (hosted resolver)] ' +
|
|
1770
|
-
'[--minter 0x..] [--primary-payee 0x..] [--unpaused] --name "Title" --symbol SYM');
|
|
1789
|
+
'[--minter 0x..] [--primary-payee 0x..] [--unpaused] [--721c recommended|0x..] --name "Title" --symbol SYM');
|
|
1771
1790
|
}
|
|
1772
1791
|
const dirPath = resolvePath(flags.dir);
|
|
1773
1792
|
const files = readdirSync(dirPath)
|
|
@@ -1790,6 +1809,8 @@ async function cmdDeploySeries(flags) {
|
|
|
1790
1809
|
// Catch a wrong-network RPC with the clear mismatch message even on dry-run (which still reads
|
|
1791
1810
|
// the chain to predict the address); tolerate an unreachable RPC so an offline preview still works.
|
|
1792
1811
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
1812
|
+
// --721c (opt-in ERC-721C): absent → zeroAddress, a plain ERC-721 exactly as before.
|
|
1813
|
+
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun);
|
|
1793
1814
|
// (identity guard already ran above, via the shared assertRealIdentity — before any RPC)
|
|
1794
1815
|
// Mint timing: mint-all → the whole series; mint-count N → the first N; else deferred.
|
|
1795
1816
|
const mintCount = flags['mint-all'] !== undefined ? count : flags['mint-count'] ? Number(flags['mint-count']) : 0;
|
|
@@ -2004,7 +2025,8 @@ async function cmdDeploySeries(flags) {
|
|
|
2004
2025
|
: onChainUri
|
|
2005
2026
|
? 'inline SVG on-chain'
|
|
2006
2027
|
: `off-chain custody → ${baseUrl}`;
|
|
2007
|
-
await confirmSend(`About to deploy Series "${name}" (${symbol}) — ${count} token(s); ${custody}; mint: ${mintCount > 0 ? `${mintCount} at deploy` : 'deferred'}; owner+royalty: your wallet @ ${royaltyBps / 100}
|
|
2028
|
+
await confirmSend(`About to deploy Series "${name}" (${symbol}) — ${count} token(s); ${custody}; mint: ${mintCount > 0 ? `${mintCount} at deploy` : 'deferred'}; owner+royalty: your wallet @ ${royaltyBps / 100}%.` +
|
|
2029
|
+
(transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
2008
2030
|
}
|
|
2009
2031
|
if (!onchainImage && !remoteEthUpload)
|
|
2010
2032
|
await buildFields();
|
|
@@ -2023,6 +2045,7 @@ async function cmdDeploySeries(flags) {
|
|
|
2023
2045
|
contractURIRenderer: renderer,
|
|
2024
2046
|
royaltyReceiver: deployer,
|
|
2025
2047
|
royaltyBps,
|
|
2048
|
+
transferValidator,
|
|
2026
2049
|
maxInvocations: count,
|
|
2027
2050
|
primaryPayee,
|
|
2028
2051
|
minter,
|
|
@@ -2869,6 +2892,21 @@ async function walkthroughReadBack(state, baseUrl, onChainUri) {
|
|
|
2869
2892
|
}
|
|
2870
2893
|
console.log(` ${dim('prove the bytes match the chain:')} ${bold(`abx verify ${state.address}`)}`);
|
|
2871
2894
|
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Refuse a port that's already bound, with a formatted one-liner naming the port and the fix.
|
|
2897
|
+
*
|
|
2898
|
+
* Without this, `listen()` has no `'error'` handler and EADDRINUSE reaches Node's default handler:
|
|
2899
|
+
* the creator gets a raw stack trace through `node:net` and our own `dist/` paths, which reads as a
|
|
2900
|
+
* crash inside abx rather than "something else is on this port" — and it's the one unformatted error
|
|
2901
|
+
* surface in a CLI where every other error is formatted. `preview`'s default port colliding with a
|
|
2902
|
+
* studio left running in another terminal was reported as especially hard to diagnose.
|
|
2903
|
+
*/
|
|
2904
|
+
async function assertPortFree(port, cmd) {
|
|
2905
|
+
if (!(await portInUse(port)))
|
|
2906
|
+
return;
|
|
2907
|
+
throw new Error(`port ${port} is already in use — most likely an \`abx ${cmd}\`/\`abx serve\`/\`abx preview\` still running in another terminal.\n` +
|
|
2908
|
+
` Stop that one (Ctrl-C), or run this on a different port: \`abx ${cmd} --port ${port + 1}\`.`);
|
|
2909
|
+
}
|
|
2872
2910
|
/** Is a TCP port already bound on localhost? Used to preflight a serve BEFORE spending a tx. */
|
|
2873
2911
|
async function portInUse(port) {
|
|
2874
2912
|
const { createServer } = await import('node:net');
|
|
@@ -2945,7 +2983,11 @@ async function cmdPreview(flags) {
|
|
|
2945
2983
|
if (cfg.deps.some((d) => d.display.startsWith('0x'))) {
|
|
2946
2984
|
warn('an on-chain data-contract dep is NOT loaded in preview — the sketch will run without it here. Use a name@version ref to preview against the CDN copy.');
|
|
2947
2985
|
}
|
|
2948
|
-
|
|
2986
|
+
// `--shoot` takes an ephemeral port (0), so only the studio lane can collide.
|
|
2987
|
+
const previewPort = shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT);
|
|
2988
|
+
if (previewPort !== 0)
|
|
2989
|
+
await assertPortFree(previewPort, 'preview');
|
|
2990
|
+
const server = await startPreviewServer(cfg, previewPort);
|
|
2949
2991
|
if (shootDir) {
|
|
2950
2992
|
step(`Render ${count} seeds headlessly`);
|
|
2951
2993
|
try {
|
|
@@ -3092,7 +3134,7 @@ const DEPLOY_CODE_FLAGS = new Set([
|
|
|
3092
3134
|
...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag), // artist · display-notes · artist-links · license
|
|
3093
3135
|
'image-base', 'attributes-renderer', 'image-renderer',
|
|
3094
3136
|
// supply + mint + economics
|
|
3095
|
-
'max', 'mint-count', 'mint-all', 'no-mint', 'unpaused', 'minter', 'primary-payee', 'royalty-bps',
|
|
3137
|
+
'max', 'mint-count', 'mint-all', 'no-mint', 'unpaused', 'minter', 'primary-payee', 'royalty-bps', '721c',
|
|
3096
3138
|
// params + dependencies
|
|
3097
3139
|
'schema', 'no-seed', 'no-delegation', 'dep', 'dep-registry',
|
|
3098
3140
|
// serving lane
|
|
@@ -3108,6 +3150,7 @@ const DEPLOY_CODE_FLAGS = new Set([
|
|
|
3108
3150
|
const SHARED_DEPLOY_FLAGS = [
|
|
3109
3151
|
'backend', 'endpoint', 'bucket', 'region', 'prefix', 'public-base', 'gateway', 'mode', 'api-url', 'upload-url', 'provider', 'storage-signer',
|
|
3110
3152
|
'sign', 'unsigned', 'for', 'sign-url-file', 'dry-run', 'confirm', 'yes', 'salt', 'factory', 'bootstrap-factory', 'port', 'renderer', 'public-base-url',
|
|
3153
|
+
'721c', // opt-in ERC-721C enrollment (recommended | 0x…) — every deploy path takes it
|
|
3111
3154
|
];
|
|
3112
3155
|
// A 1/1 `abx deploy` accepts these (see `abx help deploy`). Anything else warns (typo'd/unsupported).
|
|
3113
3156
|
const DEPLOY_FLAGS = new Set([
|
|
@@ -3188,7 +3231,7 @@ async function cmdDeployCode(flags) {
|
|
|
3188
3231
|
'[--description "<s>"] [--external-url <url>] [--image-base <url> | --image-renderer 0x..] [--attributes-renderer 0x..] ' +
|
|
3189
3232
|
'[--max N] [--mint-count N | --mint-all] [--schema key:Type:Auth,…] [--no-seed] ' +
|
|
3190
3233
|
'[--dep <name@version|0x..>[,…]] [--dep-registry 0x..] ' +
|
|
3191
|
-
'[--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--backend ipfs|arweave] [--dry-run] [--confirm] [--bootstrap-factory]';
|
|
3234
|
+
'[--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..] [--backend ipfs|arweave] [--dry-run] [--confirm] [--bootstrap-factory]';
|
|
3192
3235
|
const scriptPath = flags.script;
|
|
3193
3236
|
const codeDir = flags['code-dir'];
|
|
3194
3237
|
const hasProgram = !!(scriptPath || codeDir); // a JS program (script or built dir)
|
|
@@ -3261,6 +3304,8 @@ async function cmdDeployCode(flags) {
|
|
|
3261
3304
|
// chain to resolve the factory/renderer and predict the address); tolerate an offline preview.
|
|
3262
3305
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
3263
3306
|
const chainId = dryRun ? resolveChain(CHAIN).id : (await publicClient.getChainId());
|
|
3307
|
+
// --721c (opt-in ERC-721C): absent → zeroAddress, a plain ERC-721 exactly as before.
|
|
3308
|
+
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun);
|
|
3264
3309
|
step('Trust anchor');
|
|
3265
3310
|
let factory;
|
|
3266
3311
|
if (dryRun) {
|
|
@@ -3666,7 +3711,8 @@ async function cmdDeployCode(flags) {
|
|
|
3666
3711
|
`dependencies: ${deps.length ? deps.map((d) => d.display).join(', ') + (depRegistry ? ` (registry ${depRegistry})` : '') : 'none'}; ` +
|
|
3667
3712
|
`mint: ${mintCount > 0 ? `${mintCount} at deploy${foldIntoInit ? ' (in the deploy tx)' : ''}` : 'deferred'}; ` +
|
|
3668
3713
|
`tx count: ${foldIntoInit ? (schemas.length ? '2 (deploy + enable your params)' : '1 (everything in the deploy tx)') : '2 (deploy + setup)'}; ` +
|
|
3669
|
-
`owner+royalty: ${flags.for ? `pinned to ${String(flags.for)}` : flags.sign !== undefined ? '⚠ the wallet you connect (NOT pinned — pass --for to enforce)' : 'your wallet'} @ ${royaltyBps / 100}
|
|
3714
|
+
`owner+royalty: ${flags.for ? `pinned to ${String(flags.for)}` : flags.sign !== undefined ? '⚠ the wallet you connect (NOT pinned — pass --for to enforce)' : 'your wallet'} @ ${royaltyBps / 100}%.` +
|
|
3715
|
+
(transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : '') + `\n` +
|
|
3670
3716
|
` Surfaces — thumbnail: ${imageDisposition}. traits: ${traitsDisposition}. postparams: ${paramsDisposition}.` +
|
|
3671
3717
|
((imageOrphaned || traitsBroken || undeclaredParams.length)
|
|
3672
3718
|
? `\n ⚠ One or more surfaces resolve to NOTHING marketplaces can see — these are DEPLOY-TIME decisions, not backfillable. Re-run --dry-run after fixing, or pass --yes to ship as-is.`
|
|
@@ -3784,7 +3830,13 @@ async function cmdDeployCode(flags) {
|
|
|
3784
3830
|
catch { /* gas pricing unavailable — cost guidance is a bonus, skip silently */ }
|
|
3785
3831
|
// ── Surfaces — every marketplace-facing dimension, resolved NOW (none is backfillable) ──────
|
|
3786
3832
|
const renderHome = process.env.ABX_STORAGE_BACKEND || 'fs'; // empty string counts as unset (=fs)
|
|
3787
|
-
|
|
3833
|
+
// A surface is BROKEN only when it resolves to nothing a marketplace can see. A dropped param is
|
|
3834
|
+
// NOT that: the token still renders, the param just takes its default. Folding it in here made
|
|
3835
|
+
// the block contradict itself two lines apart — "thumbnail: ON-CHAIN ✓ / traits: on-chain ✓"
|
|
3836
|
+
// followed by "one or more surfaces resolve to NOTHING", re-recommending the very flags that
|
|
3837
|
+
// were already set. The alarm now covers only the surfaces it can honestly speak for, and names
|
|
3838
|
+
// only the remedies for what is actually broken.
|
|
3839
|
+
const anySurfaceBroken = imageOrphaned || traitsBroken;
|
|
3788
3840
|
step('Surfaces — what marketplaces will see (all DEPLOY-TIME; not backfillable)');
|
|
3789
3841
|
(imageOrphaned ? warn : info)(`thumbnail: ${imageDisposition}`);
|
|
3790
3842
|
// Render-mode / render-home guidance is for the RENDERED-still lanes only. The in-chain
|
|
@@ -3802,12 +3854,25 @@ async function cmdDeployCode(flags) {
|
|
|
3802
3854
|
(traitsBroken ? warn : info)(`traits: ${traitsDisposition}`);
|
|
3803
3855
|
const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
|
|
3804
3856
|
((undeclaredParams.length || paramsNudge ? warn : info))(`postparams: ${paramsDisposition}`);
|
|
3805
|
-
if (anySurfaceBroken)
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3857
|
+
if (anySurfaceBroken) {
|
|
3858
|
+
const remedies = [
|
|
3859
|
+
imageOrphaned && `thumbnail ⇒ ${bold('--image-renderer <Solidity SVG>')} / ${bold('--image-base <public bucket>')} / a resolver`,
|
|
3860
|
+
traitsBroken && `traits ⇒ ${bold('--attributes-renderer')} or a resolver`,
|
|
3861
|
+
].filter(Boolean).join(' · ');
|
|
3862
|
+
const which = imageOrphaned && traitsBroken ? 'the thumbnail and traits surfaces resolve' : `the ${imageOrphaned ? 'thumbnail' : 'traits'} surface resolves`;
|
|
3863
|
+
warn(`${bold(`${which} to NOTHING a marketplace can see`)} — fix before deploy (a deploy-time decision you can't add later without a re-point tx): ${remedies}.`);
|
|
3864
|
+
}
|
|
3865
|
+
// A dropped param is its own, milder problem: the piece renders, that input just takes its
|
|
3866
|
+
// default. Kept separate from the broken-surface alarm above (see the note there).
|
|
3867
|
+
if (undeclaredParams.length)
|
|
3868
|
+
info(` ${dim('dropped params render with their defaults — declare them to make them real:')} ${bold('--schema <key>:<Type>:<Auth>')}`);
|
|
3869
|
+
// The runner/verify line is for lanes that need an off-chain STILL. An on-chain image renderer has
|
|
3870
|
+
// no still to render, host, or refresh — whether or not there's also a program driving
|
|
3871
|
+
// animation_url — so pointing at a runner and a bucket backend there is simply wrong.
|
|
3872
|
+
if (hasImageRenderer) {
|
|
3873
|
+
info(` ${g('nothing to render')} — the thumbnail is computed on-chain${hasProgram ? ' and the animation assembles on-chain from your script' : ''}; no runner, no bucket, no refresh. ` +
|
|
3874
|
+
`verify from chain: ${bold('abx tokenuri ' + (predicted ?? '<address>'))} ${dim('(decodes name + on-chain SVG + traits)')}`);
|
|
3875
|
+
}
|
|
3811
3876
|
else {
|
|
3812
3877
|
const remoteRender = !(onChainUri && hasImageBase);
|
|
3813
3878
|
info(` ${dim('stand up the runner:')} ${bold('abx deploy-effects --resolver-url ' + baseUrl)} · one-shot: ${bold('abx render ' + (predicted ?? '<address>') + (remoteRender ? ' --remote ' + baseUrl : ''))} · verify: ${bold('abx verify ' + (predicted ?? '<address>'))}`);
|
|
@@ -3863,6 +3928,7 @@ async function cmdDeployCode(flags) {
|
|
|
3863
3928
|
contractURIRenderer: foldIntoInit ? metadataRenderer : zeroAddress,
|
|
3864
3929
|
royaltyReceiver: owner,
|
|
3865
3930
|
royaltyBps,
|
|
3931
|
+
transferValidator,
|
|
3866
3932
|
maxInvocations: BigInt(max),
|
|
3867
3933
|
primaryPayee: flags['primary-payee'] ?? zeroAddress,
|
|
3868
3934
|
minter: flags.minter ?? zeroAddress,
|
|
@@ -4132,6 +4198,31 @@ async function cmdRender(address, tokenIds, flags) {
|
|
|
4132
4198
|
warn(`${summary}\n ${dim(stats.errors[0] ?? 'see error above')}`);
|
|
4133
4199
|
else
|
|
4134
4200
|
ok(summary);
|
|
4201
|
+
if (stats.ran)
|
|
4202
|
+
noteArweavePropagation(flags);
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
4205
|
+
* After an Arweave publish, say that a fresh 404 at the gateway is PROPAGATION, not a failed render.
|
|
4206
|
+
*
|
|
4207
|
+
* `arweave.net` lags Turbo uploads by minutes: a tester found 32/32 of their renders 404ing there
|
|
4208
|
+
* while Turbo reported CONFIRMED and 22/32 already served fine from other ar.io gateways. The bytes
|
|
4209
|
+
* were never in doubt — only the gateway was behind. Unexplained, a "broken" thumbnail on a fresh
|
|
4210
|
+
* drop reads as a failed render, and the natural next move is `abx render --force` on all of them:
|
|
4211
|
+
* a full re-upload that fixes nothing.
|
|
4212
|
+
*
|
|
4213
|
+
* Note this is advisory only, and deliberately so — the locator is baked into what the resolver
|
|
4214
|
+
* registers at publish time, so it cannot be repaired by a redirect later. Choosing the gateway is
|
|
4215
|
+
* the operator's call (`ABX_ARWEAVE_GATEWAY`), which is why this names it.
|
|
4216
|
+
*/
|
|
4217
|
+
function noteArweavePropagation(flags) {
|
|
4218
|
+
const opts = storageOptions(storageOverrides(flags));
|
|
4219
|
+
if (resolveBackend(opts).id !== 'arweave')
|
|
4220
|
+
return;
|
|
4221
|
+
const gateway = opts.arweave?.gateway ?? 'https://arweave.net';
|
|
4222
|
+
if (!/(^|\/\/)([^/]*\.)?arweave\.net/.test(gateway))
|
|
4223
|
+
return; // a gateway they chose — don't lecture
|
|
4224
|
+
info(`${dim('arweave: the locator points at')} ${gateway}${dim(', which can 404 for several minutes after upload while it catches up. That is PROPAGATION, not a failed render — the bytes are already confirmed. Do NOT re-run with')} ${bold('--force')}${dim('; the URL starts working on its own.')}`);
|
|
4225
|
+
info(` ${dim('to bake a different gateway into the locator instead (it is fixed at publish time):')} ${bold('ABX_ARWEAVE_GATEWAY=https://<gateway>')} ${dim('before you render.')}`);
|
|
4135
4226
|
}
|
|
4136
4227
|
/**
|
|
4137
4228
|
* The publish topology's one hard prerequisite, checked BEFORE any capture.
|
|
@@ -4344,14 +4435,27 @@ async function cmdVerify(address, flags) {
|
|
|
4344
4435
|
// `abx verify <addr> --remote <resolver>` probes what the resolver actually serves (the truthful check).
|
|
4345
4436
|
if (minted.length)
|
|
4346
4437
|
info(dim(`render check is against THIS node's store; for a HOSTED drop use \`abx verify ${address} --remote <resolver>\``));
|
|
4438
|
+
// ONE line per outcome, not per token. This printed the same full-sentence advisory 32 times on a
|
|
4439
|
+
// 32-token project (~4KB of identical text) and pushed the four lines that answer "did my deploy
|
|
4440
|
+
// work" off the top of the screen; at a 1000-token supply it is unreadable. The per-token detail
|
|
4441
|
+
// that survives is the token LIST, which is the only part that differs.
|
|
4442
|
+
const missing = [];
|
|
4443
|
+
let present = 0;
|
|
4347
4444
|
for (const token of minted) {
|
|
4348
4445
|
const { found } = await currentRenderArtifact(client, state, token, storageForRender, 'image');
|
|
4349
4446
|
if (found)
|
|
4350
|
-
|
|
4351
|
-
else
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4447
|
+
present++;
|
|
4448
|
+
else
|
|
4449
|
+
missing.push(String(token.tokenId));
|
|
4450
|
+
}
|
|
4451
|
+
if (present)
|
|
4452
|
+
ok(`${present}/${minted.length} minted token(s): real render present (in this node's store)`);
|
|
4453
|
+
if (missing.length) {
|
|
4454
|
+
renderGap = true;
|
|
4455
|
+
const ids = missing.length > 12 ? `${missing.slice(0, 12).join(', ')}, …+${missing.length - 12} more` : missing.join(', ');
|
|
4456
|
+
console.log(` ${c.orange}⚠${c.reset} ${missing.length}/${minted.length} token(s) have no render in THIS node's store ${dim(`(#${ids})`)}`);
|
|
4457
|
+
console.log(` ${dim('published to a hosted resolver? check there:')} ${bold(`abx verify ${address} --remote <resolver>`)}`);
|
|
4458
|
+
console.log(` ${dim('else render them:')} ${bold(`abx render ${address}`)} ${dim('(once) ·')} ${bold('abx effects')} ${dim('(continuous)')}`);
|
|
4355
4459
|
}
|
|
4356
4460
|
}
|
|
4357
4461
|
// The on-chain URI lane (a non-zero tokenURIRenderer, or an animation field pointing at the
|
|
@@ -4625,7 +4729,7 @@ function decodeOnChainJson(uri, verbatim = false) {
|
|
|
4625
4729
|
return verbatim ? raw : raw.slice(0, 600);
|
|
4626
4730
|
}
|
|
4627
4731
|
}
|
|
4628
|
-
async function cmdTokenUri(address, flags) {
|
|
4732
|
+
async function cmdTokenUri(address, flags, extra = []) {
|
|
4629
4733
|
if (!address || address.startsWith('--')) {
|
|
4630
4734
|
console.error('usage: abx tokenuri <address> [--token <id>] [--json]\n');
|
|
4631
4735
|
process.exit(1);
|
|
@@ -4634,6 +4738,19 @@ async function cmdTokenUri(address, flags) {
|
|
|
4634
4738
|
console.error(`abx tokenuri: '${address}' isn't a 0x contract address.\n`);
|
|
4635
4739
|
process.exit(1);
|
|
4636
4740
|
}
|
|
4741
|
+
// `abx tokenuri <addr> 0` silently ignored the `0` and printed token 0 — a COINCIDENTALLY correct
|
|
4742
|
+
// answer, which is the dangerous kind: `… <addr> 7` would have printed token 0 just as confidently
|
|
4743
|
+
// and exited 0. The token id is a flag here, so name it rather than guessing at intent.
|
|
4744
|
+
const strayPositionals = positionalArgs(extra);
|
|
4745
|
+
if (strayPositionals.length) {
|
|
4746
|
+
const first = strayPositionals[0];
|
|
4747
|
+
const looksLikeTokenId = /^\d+$/.test(first);
|
|
4748
|
+
console.error(`abx tokenuri: unexpected extra argument '${first}'.` +
|
|
4749
|
+
(looksLikeTokenId
|
|
4750
|
+
? ` The token id is a flag — did you mean:\n abx tokenuri ${address} --token ${first}\n`
|
|
4751
|
+
: `\n usage: abx tokenuri <address> [--token <id>] [--json]\n`));
|
|
4752
|
+
process.exit(1);
|
|
4753
|
+
}
|
|
4637
4754
|
const tokenId = BigInt(flags.token ?? '0');
|
|
4638
4755
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
4639
4756
|
// A creator verifying their work often runs this on a wrong / not-yet-mined address — turn viem's
|
|
@@ -4776,6 +4893,7 @@ async function cmdContractUri(address, _flags) {
|
|
|
4776
4893
|
// ── serve ──────────────────────────────────────────────────────────────────--
|
|
4777
4894
|
async function cmdServe(flags) {
|
|
4778
4895
|
const port = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
|
|
4896
|
+
await assertPortFree(port, 'serve');
|
|
4779
4897
|
const baseUrl = resolveBaseUrl(port);
|
|
4780
4898
|
const indexer = new SelfHostIndexer();
|
|
4781
4899
|
// Surface the most-recently-reconstructed project (what you just deployed), not
|
|
@@ -5045,6 +5163,9 @@ const STATE_ABI = [
|
|
|
5045
5163
|
{ type: 'function', name: 'primaryPayee', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
5046
5164
|
{ type: 'function', name: 'tokenURIRenderer', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
5047
5165
|
{ type: 'function', name: 'royaltyInfo', stateMutability: 'view', inputs: [{ type: 'uint256' }, { type: 'uint256' }], outputs: [{ type: 'address' }, { type: 'uint256' }] },
|
|
5166
|
+
// ERC-721C (creator token) — ERC-165 advertises ICreatorToken ONLY when enrolled at deploy.
|
|
5167
|
+
{ type: 'function', name: 'supportsInterface', stateMutability: 'view', inputs: [{ type: 'bytes4' }], outputs: [{ type: 'bool' }] },
|
|
5168
|
+
{ type: 'function', name: 'getTransferValidator', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
5048
5169
|
];
|
|
5049
5170
|
async function cmdState(address, flags) {
|
|
5050
5171
|
if (!address || address.startsWith('--')) {
|
|
@@ -5061,7 +5182,7 @@ async function cmdState(address, flags) {
|
|
|
5061
5182
|
return undefined; // getter absent (extension not composed) or contract has no code
|
|
5062
5183
|
}
|
|
5063
5184
|
};
|
|
5064
|
-
const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty] = await Promise.all([
|
|
5185
|
+
const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator] = await Promise.all([
|
|
5065
5186
|
read('owner'),
|
|
5066
5187
|
read('totalSupply'),
|
|
5067
5188
|
read('nextTokenId'),
|
|
@@ -5071,6 +5192,8 @@ async function cmdState(address, flags) {
|
|
|
5071
5192
|
read('primaryPayee'),
|
|
5072
5193
|
read('tokenURIRenderer'),
|
|
5073
5194
|
read('royaltyInfo', [0n, 10000n]),
|
|
5195
|
+
read('supportsInterface', [CREATOR_TOKEN_INTERFACE_ID]),
|
|
5196
|
+
read('getTransferValidator'),
|
|
5074
5197
|
]);
|
|
5075
5198
|
if (owner === undefined && totalSupply === undefined) {
|
|
5076
5199
|
throw new Error(`no ABX contract state at ${address} on ${CHAIN} (no code, or not an ABX token).`);
|
|
@@ -5095,6 +5218,11 @@ async function cmdState(address, flags) {
|
|
|
5095
5218
|
if (royalty)
|
|
5096
5219
|
info(`royalty ${Number(royalty[1]) / 100}% → ${royalty[0]}`);
|
|
5097
5220
|
info(`renderer ${renderer && renderer !== zeroAddress ? `on-chain (${renderer})` : dim('off-chain (stored URI base / override)')}`);
|
|
5221
|
+
// ERC-721C: shown ONLY for an enrolled collection — an unenrolled token is a plain ERC-721
|
|
5222
|
+
// (indistinguishable from a pre-721C token) and shows nothing new here.
|
|
5223
|
+
if (creatorToken) {
|
|
5224
|
+
info(`721C validator ${transferValidator && transferValidator !== zeroAddress ? transferValidator : `${c.orange}suspended${c.reset} ${dim('(zero — enforcement off; re-enable with `abx set-transfer-validator`)')}`}`);
|
|
5225
|
+
}
|
|
5098
5226
|
// The PostParam surface, read straight from chain: `paramSchemaKeys()` is the DECLARED (governed)
|
|
5099
5227
|
// set — append-only, and it includes keys declared but never yet written, which nothing off-chain
|
|
5100
5228
|
// could otherwise discover — and `contractParamKeys()` is the collection-scope set values. Both
|
|
@@ -6041,6 +6169,10 @@ const COMMAND_HELP = {
|
|
|
6041
6169
|
${g('--compress')} none|fastlz|gzip for --onchain-image (default none): ${g('fastlz')} = on-chain decode (stays renderable);
|
|
6042
6170
|
${g('gzip')} = smaller but off-chain decode only
|
|
6043
6171
|
--royalty-bps <0-10000> default 500 (5%)
|
|
6172
|
+
${g('--721c')} [recommended|0x..] opt-in ERC-721C: enroll at deploy, PERMANENTLY, with that transfer validator
|
|
6173
|
+
(${g('recommended')} = OpenSea's validator for creator-fee enforcement — only owner-initiated
|
|
6174
|
+
transfers + authorized sales move the token). Absent = plain ERC-721, forever (the default).
|
|
6175
|
+
Manage later with ${g('abx set-transfer-validator')} (re-point or suspend; never un-enroll).
|
|
6044
6176
|
${g('--no-mint')} deploy without minting (warm the resolver, then ${g('abx mint')})
|
|
6045
6177
|
--salt 0x..<64hex> deploy to a reserved / vanity address (see ${g('abx predict')})
|
|
6046
6178
|
--public-base-url <url> bake a public resolver URL on-chain (off-chain custody REQUIRES a public URL; localhost is refused)
|
|
@@ -6070,6 +6202,8 @@ const COMMAND_HELP = {
|
|
|
6070
6202
|
a huge collection should set traits post-deploy via ${g('abx set-field')} under a gas budget.
|
|
6071
6203
|
--minter 0x.. authorize a single minting contract --primary-payee 0x.. primary-sale payout
|
|
6072
6204
|
--unpaused open the mint at deploy (default: paused ⇒ owner-only until ${g('abx unpause')})
|
|
6205
|
+
${g('--721c')} [recommended|0x..] opt-in ERC-721C: enroll at deploy, PERMANENTLY, with that transfer validator (absent = plain
|
|
6206
|
+
ERC-721 forever). ${g('recommended')} = OpenSea's creator-fee-enforcement validator. Manage: ${g('abx set-transfer-validator')}
|
|
6073
6207
|
--salt 0x.. · --backend … (storage override) · ${g('--storage-signer')} arweave|eth (who pays Turbo uploads) · signing: ${g('--send')} · ${g('--sign')} · ${g('--unsigned')}
|
|
6074
6208
|
${g('--dry-run')} preview (address · per-token plan · custody · mint plan) WITHOUT sending or storing
|
|
6075
6209
|
${dim('(a dry-run needs a deployer to compute the address — pass --for 0x.. if no signing key is set)')}
|
|
@@ -6151,6 +6285,8 @@ const COMMAND_HELP = {
|
|
|
6151
6285
|
a CDN-served record is the normal production path, on-chain bytes are the durability floor).
|
|
6152
6286
|
--max N supply cap (default 16) mint timing: ${g('--mint-all')} · ${g('--mint-count')} N · (default: defer, then ${g('abx mint')})
|
|
6153
6287
|
--no-seed opt out of the canonical mint-time seed (seeds settle once assigned; curated pre-set seeds win)
|
|
6288
|
+
${g('--721c')} [recommended|0x..] opt-in ERC-721C: enroll at deploy, PERMANENTLY, with that transfer validator (absent = plain
|
|
6289
|
+
ERC-721 forever). ${g('recommended')} = OpenSea's creator-fee-enforcement validator. Manage: ${g('abx set-transfer-validator')}
|
|
6154
6290
|
--backend ipfs|arweave directory-mode custody for the build upload ${g('--unpaused')} · --minter 0x.. · --primary-payee 0x..
|
|
6155
6291
|
${dim('a fixed-price PRIMARY SALE is set up AFTER deploy, not by these flags: ')}${g('abx minter configure <addr> --price <eth>')}${dim(' → ')}${g('set-minter')}${dim(' → ')}${g('set-primary-payee')}${dim(' → ')}${g('unpause')}${dim(' (see `abx minter --help`). --minter/--primary-payee here only pre-authorize an already-known minter.')}
|
|
6156
6292
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} ${dim('(needs --for 0x..)')} ${g('--bootstrap-factory')} ${dim('(private chains/sandboxes only)')}
|
|
@@ -6220,6 +6356,9 @@ const COMMAND_HELP = {
|
|
|
6220
6356
|
like ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
|
|
6221
6357
|
A contract-scope param applies to every token, and enumerates on-chain like any other.
|
|
6222
6358
|
--file <path> read the value from a file (String / Bytes payloads)
|
|
6359
|
+
${dim('PAYLOAD TYPES:')} ${g('String')} ${dim('takes literal text (UTF-8).')} ${g('Bytes')} ${dim('takes')} ${bold('0x-prefixed hex')} ${dim(`or ${g('--file')} — a bare`)}
|
|
6360
|
+
${dim('string is refused, because there is no safe guess between "these characters" and "these bytes".')}
|
|
6361
|
+
${dim(`(The docs' "Bytes becomes base64" describes how your ${bold('program')} receives the value, not how you write it.)`)}
|
|
6223
6362
|
${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
|
|
6224
6363
|
the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
|
|
6225
6364
|
resolver running the chain watcher (the ${g('abx serve')} default) sees the change on its next poll (~12s)
|
|
@@ -6255,7 +6394,8 @@ const COMMAND_HELP = {
|
|
|
6255
6394
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')}`,
|
|
6256
6395
|
state: `
|
|
6257
6396
|
${bold('abx state')} <address> ${dim('— read-only, on-chain operational snapshot (no tx, no local index).')}
|
|
6258
|
-
Shows owner · supply (minted / max, nextTokenId) · paused · minter · primary payee · royalty · renderer
|
|
6397
|
+
Shows owner · supply (minted / max, nextTokenId) · paused · minter · primary payee · royalty · renderer
|
|
6398
|
+
(+ the 721C transfer validator, for a collection that enrolled at deploy — plain ERC-721s show nothing new).
|
|
6259
6399
|
Series-only fields are shown for a Series; a 1/1 shows just supply + royalty + renderer.
|
|
6260
6400
|
Handy before/after owner ops (mint · pause/unpause · set-minter · set-primary-payee).
|
|
6261
6401
|
${dim('state = what the CHAIN says. For who is SERVING it and how fresh that is, see `abx status`.')}`,
|
|
@@ -6310,6 +6450,14 @@ const COMMAND_HELP = {
|
|
|
6310
6450
|
deploy to Vercel). ${dim('Offer this after a sale is live (configure → set-minter → set-primary-payee → unpause).')}`,
|
|
6311
6451
|
'set-royalty': `
|
|
6312
6452
|
${bold('abx set-royalty')} <address> --bps <0-10000> [--receiver 0x..] ${dim('— change the royalty. Sends a tx.')}`,
|
|
6453
|
+
'set-transfer-validator': `
|
|
6454
|
+
${bold('abx set-transfer-validator')} <address> <0x..|none|recommended> ${dim('— manage an ERC-721C collection\'s transfer validator. Owner-only. Sends a tx.')}
|
|
6455
|
+
<0x..> re-point enforcement at that validator (must be a DEPLOYED contract — checked before any gas)
|
|
6456
|
+
${g('none')} suspend enforcement (validator → address(0); the collection STAYS enrolled)
|
|
6457
|
+
${g('recommended')} the chain's recommended validator (OpenSea's, for creator-fee enforcement)
|
|
6458
|
+
${dim('Only for a collection that enrolled AT DEPLOY (--721c on the deploy commands) — a plain ERC-721 is refused')}
|
|
6459
|
+
${dim('up front: enrollment is a deploy-time decision and can never be added (or fully removed) later.')}
|
|
6460
|
+
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} · ${g('--dry-run')} preview (encodes + shows the tx, sends nothing)`,
|
|
6313
6461
|
attach: `
|
|
6314
6462
|
${bold('abx attach')} <address> <key> <ipfs://… | ar://… | https://…> ${dim('— attach a named file to a token. Sends a tx.')}
|
|
6315
6463
|
${dim('The data-plane verb: the file joins the token\'s served')} ${bold('artifacts')} ${dim('manifest as {key, mimeType, uri}.')}
|
|
@@ -6339,8 +6487,12 @@ const COMMAND_HELP = {
|
|
|
6339
6487
|
${g('--dry-run')} preview the tx, send nothing
|
|
6340
6488
|
--collection target collection (ERC-7572) scope · else --token <id> (default 0)`,
|
|
6341
6489
|
'lock-field': `
|
|
6342
|
-
${bold('abx lock-field')} <address> --field <name> ${dim('— freeze a
|
|
6343
|
-
--collection | --token <id
|
|
6490
|
+
${bold('abx lock-field')} <address> --field <name> ${dim('— freeze a metadata FIELD forever (irreversible). Sends a tx.')}
|
|
6491
|
+
--collection | --token <id>
|
|
6492
|
+
--force-field proceed even if a PostParam shares this name (you mean the field)
|
|
6493
|
+
${dim('FIELDS AND PARAMS ARE DIFFERENT NAMESPACES and may share a name. This locks the field only —')}
|
|
6494
|
+
${dim(`to weld a PostParam use ${g('abx set-schema <addr> --schema <key>:<Type>:<Auth>:lock=now')}. Passing a`)}
|
|
6495
|
+
${dim('declared param key here is refused, because locking the field leaves the param writable.')}`,
|
|
6344
6496
|
'set-renderer': `
|
|
6345
6497
|
${bold('abx set-renderer')} <address> ${dim('— toggle URI resolution between off-chain and on-chain. Sends a tx.')}
|
|
6346
6498
|
(default) point at the chain's canonical renderer (deploys it if needed) → resolve ON-CHAIN
|
|
@@ -6470,6 +6622,11 @@ const COMMAND_HELP = {
|
|
|
6470
6622
|
project to the effects layer (${g('ABX_EFFECTS_URL')} + ${g('ABX_EFFECTS_TOKEN')}) → thumbnails auto-re-render.
|
|
6471
6623
|
${g('ABX_WATCH_INTERVAL_MS')} tunes the cadence; ${g('0')} disables (state then updates only on explicit add/index).
|
|
6472
6624
|
${dim('Reorg note: no lookback (post-PoS reorgs are rare); the repair is the deterministic full replay — `abx index <addr> --full`.')}`,
|
|
6625
|
+
changelog: `
|
|
6626
|
+
${bold('abx changelog')} ${dim('— what changed in this and recent releases. Reads nothing from the network. No tx.')}
|
|
6627
|
+
${g('--all')} the full history (default: the 3 most recent releases)
|
|
6628
|
+
${dim('The notes ship INSIDE the package, so they always match the version you have and work offline.')}
|
|
6629
|
+
${dim('Every published version is listed at https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions')}`,
|
|
6473
6630
|
};
|
|
6474
6631
|
function printCommandHelp(cmd) {
|
|
6475
6632
|
if (cmd && COMMAND_HELP[cmd]) {
|
|
@@ -6543,6 +6700,7 @@ function help() {
|
|
|
6543
6700
|
${g('abx set-contract-uri')} <addr> re-point the collection URI --uri <url>
|
|
6544
6701
|
${g('abx migrate')} <addr> move off-chain state to a NEW resolver (verifies parity; no cutover) --from <old> --to <new>
|
|
6545
6702
|
${g('abx set-royalty')} <addr> change royalty --bps <0-10000> [--receiver 0x..]
|
|
6703
|
+
${g('abx set-transfer-validator')} <addr> <0x..|none|recommended> manage an ERC-721C collection's validator (enrolled-at-deploy only)
|
|
6546
6704
|
${g('abx attach')} <addr> <key> <ipfs://…|ar://…|https://…> attach a named file → the token's ${bold('artifacts')} manifest (data plane)
|
|
6547
6705
|
${g('abx set-field')} <addr> set an on-chain metadata field (low-level) --field <name> (--text "…" | --value 0x..) [--collection]
|
|
6548
6706
|
${g('abx lock-field')} <addr> freeze a field forever --field <name> [--collection]
|
|
@@ -6571,11 +6729,54 @@ function help() {
|
|
|
6571
6729
|
${g('abx doctor')} check environment (key, RPC, balance, factory, storage)
|
|
6572
6730
|
${g('abx skill install')} install the version-locked abx skill into your agent(s) [--agent <name>] [--global] [--target <dir>] · ${g('abx skill path')} prints the bundled skill
|
|
6573
6731
|
${g('abx version')} print the installed CLI version
|
|
6732
|
+
${g('abx changelog')} what changed in this and recent releases (ships with the CLI; offline) [--all]
|
|
6574
6733
|
|
|
6575
6734
|
${dim('Run')} ${g('abx <command> --help')} ${dim('for per-command usage. --help / -h never executes — it only prints usage.')}
|
|
6576
6735
|
${dim('abx checks npm for a newer release (every 6h, notify-only). Silence with')} ${g('ABX_NO_UPDATE_CHECK=1')} ${dim('or')} ${g('--no-update-check')}${dim('.')}
|
|
6577
6736
|
`);
|
|
6578
6737
|
}
|
|
6738
|
+
/**
|
|
6739
|
+
* Resolve `--721c` into the InitParams `transferValidator`. Absent → `zeroAddress`: a plain
|
|
6740
|
+
* ERC-721, byte-for-byte the pre-721C deploy — no prompts, no output, no mention of 721C.
|
|
6741
|
+
* `--721c` / `--721c recommended` → the per-chain recommended validator (refused, naming the
|
|
6742
|
+
* chains that have one, when the manifest has no entry); `--721c 0x…` → checksum-validated AND
|
|
6743
|
+
* pre-checked for code on this chain (the factory would revert `InvalidTransferValidator()` —
|
|
6744
|
+
* surface it before any upload/staging/gas). When enrolling, prints the one plain statement of
|
|
6745
|
+
* what enforcement means — once, adapted to the chosen validator.
|
|
6746
|
+
*/
|
|
6747
|
+
async function resolveTransferValidatorFlag(flags, publicClient, dryRun) {
|
|
6748
|
+
const raw = flags['721c'];
|
|
6749
|
+
if (raw === undefined)
|
|
6750
|
+
return zeroAddress;
|
|
6751
|
+
const chainId = resolveChain(CHAIN).id;
|
|
6752
|
+
const validator = parseTransferValidatorValue(raw, { chainId, chainLabel: CHAIN });
|
|
6753
|
+
// Has-code precheck — for the recommended constant too (presence on THIS chain is the fact that
|
|
6754
|
+
// matters; a sandbox/private chain won't have it). A dry run tolerates an unreachable RPC (it
|
|
6755
|
+
// sends nothing); a real deploy refuses to enroll blind.
|
|
6756
|
+
let code;
|
|
6757
|
+
try {
|
|
6758
|
+
code = await publicClient.getCode({ address: validator });
|
|
6759
|
+
}
|
|
6760
|
+
catch (err) {
|
|
6761
|
+
if (!dryRun)
|
|
6762
|
+
throw new Error(`--721c: couldn't verify the validator has code at ${validator} (${err.message}) — refusing to enroll blind; retry when the RPC answers.`);
|
|
6763
|
+
code = null; // offline preview — note it below
|
|
6764
|
+
}
|
|
6765
|
+
if (code === null) {
|
|
6766
|
+
info(dim(`--721c: RPC unreachable, so the validator code check is deferred — a real deploy verifies ${validator} has code first.`));
|
|
6767
|
+
}
|
|
6768
|
+
else if (!code || code === '0x') {
|
|
6769
|
+
const rec = resolveRecommendedTransferValidator(chainId);
|
|
6770
|
+
throw new Error(`--721c: no contract code at ${validator} on ${CHAIN} — the deploy would revert InvalidTransferValidator(). ` +
|
|
6771
|
+
`A transfer validator must be a DEPLOYED contract on this chain` +
|
|
6772
|
+
(rec && rec !== validator ? ` (the known-good one: --721c recommended → ${rec})` : '') + `.`);
|
|
6773
|
+
}
|
|
6774
|
+
// The one plain statement — printed once, only when enrolling. Never appears un-enrolled.
|
|
6775
|
+
info(validator === resolveRecommendedTransferValidator(chainId)
|
|
6776
|
+
? `ERC-721C enforcement: only owner-initiated transfers and OpenSea-authorized sales will transfer; other marketplaces/operators are blocked. Manage with \`abx set-transfer-validator\`.`
|
|
6777
|
+
: `ERC-721C enforcement: every non-mint transfer is checked by validator ${validator} — its policy decides which operators may transfer. Manage with \`abx set-transfer-validator\`.`);
|
|
6778
|
+
return validator;
|
|
6779
|
+
}
|
|
6579
6780
|
/** Validate an explicit --salt (a 32-byte hex). Undefined when absent. */
|
|
6580
6781
|
function parseSaltFlag(raw) {
|
|
6581
6782
|
if (!raw || raw === 'true')
|
|
@@ -6622,6 +6823,40 @@ function resolveBundledSkill() {
|
|
|
6622
6823
|
return canonical;
|
|
6623
6824
|
return null;
|
|
6624
6825
|
}
|
|
6826
|
+
/**
|
|
6827
|
+
* `abx changelog [--all]` — print the release notes that ship with THIS install.
|
|
6828
|
+
*
|
|
6829
|
+
* The update banner used to point at `github.com/ArtBlocks/abx/releases`, which 404s for anyone
|
|
6830
|
+
* outside the org, and no changelog shipped in the package — so "what changed?" was unanswerable
|
|
6831
|
+
* without diffing two versions' dry-run output by hand. `CHANGELOG.md` now ships (package.json
|
|
6832
|
+
* `files`), which makes the notes offline, version-matched, and readable by an agent.
|
|
6833
|
+
*
|
|
6834
|
+
* Default: the most recent few entries (what an upgrade needs). `--all`: the whole file.
|
|
6835
|
+
*/
|
|
6836
|
+
function cmdChangelog(flags) {
|
|
6837
|
+
const pkgDir = resolvePath(fileURLToPath(import.meta.url), '..', '..');
|
|
6838
|
+
const candidates = [joinPath(pkgDir, 'CHANGELOG.md')];
|
|
6839
|
+
const root = findRepoRoot();
|
|
6840
|
+
if (root)
|
|
6841
|
+
candidates.push(joinPath(root, 'packages', 'cli', 'CHANGELOG.md')); // dev checkout
|
|
6842
|
+
const path = candidates.find((p) => existsSync(p));
|
|
6843
|
+
if (!path) {
|
|
6844
|
+
throw new Error(`no CHANGELOG.md found beside this install (looked in ${candidates.join(', ')}). ` +
|
|
6845
|
+
'Version history: https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions');
|
|
6846
|
+
}
|
|
6847
|
+
const text = readFileSync(path, 'utf8');
|
|
6848
|
+
if (flags.all !== undefined) {
|
|
6849
|
+
console.log(text);
|
|
6850
|
+
return;
|
|
6851
|
+
}
|
|
6852
|
+
// Entries are `## <version>` sections; show the newest few and say how to see the rest.
|
|
6853
|
+
const lines = text.split('\n');
|
|
6854
|
+
const heads = lines.map((l, i) => (/^## /.test(l) ? i : -1)).filter((i) => i >= 0);
|
|
6855
|
+
const end = heads.length > 3 ? heads[3] : lines.length;
|
|
6856
|
+
console.log(`\n${lines.slice(0, end).join('\n').trimEnd()}\n`);
|
|
6857
|
+
if (heads.length > 3)
|
|
6858
|
+
info(`showing the ${3} most recent releases — ${bold('abx changelog --all')} for the full history ${dim(`(${path})`)}`);
|
|
6859
|
+
}
|
|
6625
6860
|
/** Locate the in-chain renderer Foundry scaffold: bundled beside the CLI (published), else the
|
|
6626
6861
|
* repo copy (dev). Same layout in both — it lives under the CLI package's `assets/`. */
|
|
6627
6862
|
function resolveRendererScaffold() {
|
|
@@ -6650,12 +6885,9 @@ function cmdScaffoldRenderer(rest, flags) {
|
|
|
6650
6885
|
if (existsSync(dir) && readdirSync(dir).length > 0 && flags.force === undefined) {
|
|
6651
6886
|
throw new Error(`${dir} already exists and is not empty — pass a fresh path, or --force to write into it.`);
|
|
6652
6887
|
}
|
|
6653
|
-
|
|
6654
|
-
//
|
|
6655
|
-
|
|
6656
|
-
recursive: true,
|
|
6657
|
-
filter: (s) => !/(^|\/)(out|cache|dependencies|broadcast|node_modules)(\/|$)/.test(s),
|
|
6658
|
-
});
|
|
6888
|
+
// Copy + assert it landed. Throws rather than printing a success banner over an empty directory —
|
|
6889
|
+
// the alpha.9→alpha.14 failure mode. See src/scaffold.ts for why this is not inline.
|
|
6890
|
+
copyRendererScaffold(src, dir);
|
|
6659
6891
|
ok(`in-chain renderer scaffold → ${dir}`);
|
|
6660
6892
|
step('The in-chain Solidity art lane — nothing to run after deploy, tokenURI resolves from chain forever');
|
|
6661
6893
|
info(`${bold('src/MyRenderer.sol')} draws an SVG from the token ${bold('seed')} + a ${bold('palette')} HexColor PostParam; ${bold('src/MyTraits.sol')} reads the SAME seed for coherent on-chain traits. Fork the art; keep the invariants in ${bold('src/interfaces/IAbxFieldRenderer.sol')} (above all: render() must NEVER revert).`);
|