@artblocks/abx-cli 0.1.0-alpha.13 → 0.1.0-alpha.15
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/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -8
- package/dist/config.js.map +1 -1
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +1 -1
- package/dist/flags.js.map +1 -1
- package/dist/main.js +271 -78
- package/dist/main.js.map +1 -1
- package/dist/onchain-uri.d.ts +23 -39
- package/dist/onchain-uri.d.ts.map +1 -1
- package/dist/onchain-uri.js +94 -86
- package/dist/onchain-uri.js.map +1 -1
- package/dist/ownerops.d.ts +29 -13
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +270 -127
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +8 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +27 -2
- package/dist/preview.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +4 -2
- package/skill/reference/code-projects.md +5 -3
- package/skill/reference/creator-token.md +71 -0
- package/skill/reference/decisions.md +2 -2
- package/skill/reference/operating.md +1 -1
package/dist/main.js
CHANGED
|
@@ -57,11 +57,11 @@ import { formatEther, toHex, zeroAddress } from 'viem';
|
|
|
57
57
|
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
|
-
import {
|
|
60
|
+
import { expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport, readLegacyParamsKeys, readParamSchemaKeys, readSetParamKeys, } from './onchain-uri.js';
|
|
61
61
|
import { parseFlags, 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
|
-
import { previewConfigFromFlags, previewDepTags, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
64
|
+
import { previewConfigFromFlags, previewDepTags, parsePreviewParams, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
65
65
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
66
66
|
import { declinesSkillInstall } from './prompt.js';
|
|
67
67
|
import { pinGas, waitForCodeAt } from './gas.js';
|
|
@@ -74,11 +74,11 @@ const SCHEMA_CATALOG = 'Types: Bool·Select·Uint256Range·Int256Range·DecimalR
|
|
|
74
74
|
'Select needs options — key:Select[A|B|C]:Auth; a Range takes bounds — key:Uint256Range[0..100]:Auth. ' +
|
|
75
75
|
'A palette collectors set = palette:HexColor:TokenOwner';
|
|
76
76
|
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';
|
|
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, resolveRecommendedTransferValidator, CREATOR_TOKEN_INTERFACE_ID, METADATA_FIELD as F, METADATA_REPRESENTATION as R, indexProgress, isAccepted, AbxIndexTimeoutError, AbxServiceError, } from '@artblocks/abx-sdk';
|
|
78
78
|
import { SelfHostIndexer, SqliteStore } from '@artblocks/abx-indexer';
|
|
79
79
|
import { artContentHash, currentRenderArtifact, generateArt, resolveBaseUrl, startChainWatcher, startTokenApiServer, verifyProject, watchIntervalMs, DEFAULT_PORT, } from '@artblocks/abx-token-api';
|
|
80
80
|
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';
|
|
81
|
+
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
82
|
import { openWalletSession, signTx } from './signer.js';
|
|
83
83
|
import { describeRemoteError, listConfiguredRemotes, misnamedRemoteVars, requireRemoteToken, resolveRemote, serviceClient, tokenSourceLabel } from './remote.js';
|
|
84
84
|
import { buildMigrationPlan, repinNodeCustody, verifyParity } from './migrate.js';
|
|
@@ -174,6 +174,22 @@ async function main() {
|
|
|
174
174
|
loadDotEnv();
|
|
175
175
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
176
176
|
const flags = parseFlags(rest);
|
|
177
|
+
// `--chain` is the flag people reach for, and it does not exist: chain selection is the ABX_CHAIN
|
|
178
|
+
// env var. It used to be swallowed by the generic stray-flag WARNING and the command then acted on
|
|
179
|
+
// the DEFAULT chain — `abx deploy-code … --chain sepolia` deployed to base-sepolia. On a dry run
|
|
180
|
+
// that costs a confused minute; on a funded send it is a wrong-chain deploy with real artifacts at
|
|
181
|
+
// the wrong address. Prose that was ignored once will be ignored again, so this refuses instead,
|
|
182
|
+
// on EVERY command (a read-only command that quietly ignored it would still teach the wrong model).
|
|
183
|
+
if (flags.chain !== undefined) {
|
|
184
|
+
const asked = typeof flags.chain === 'string' && flags.chain !== 'true' ? flags.chain : '';
|
|
185
|
+
process.stderr.write(`\n${c.red}✗${c.reset} there is no ${bold('--chain')} flag — the chain is chosen by the ${bold('ABX_CHAIN')} environment variable.\n` +
|
|
186
|
+
` active now: ${bold(CHAIN)}${asked && asked !== CHAIN ? ` ${dim(`(you asked for '${asked}')`)}` : ''}\n` +
|
|
187
|
+
` known chains: ${KNOWN_CHAIN_KEYS.join(', ')}\n\n` +
|
|
188
|
+
` run it on ${asked && KNOWN_CHAIN_KEYS.includes(asked) ? asked : '<chain>'} like this:\n` +
|
|
189
|
+
` ${bold(`ABX_CHAIN=${asked && KNOWN_CHAIN_KEYS.includes(asked) ? asked : '<chain>'} abx ${cmd ?? '<command>'} …`)}\n` +
|
|
190
|
+
` ${dim('(the CLI is deliberately stateless — every run resolves its chain from the environment, so nothing a previous command set can leak into this one.)')}\n\n`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
177
193
|
// `version` / --version / -v is a crisp query — print and exit, no update nag around it.
|
|
178
194
|
if (cmd === 'version' || cmd === '--version' || cmd === '-v') {
|
|
179
195
|
console.log(readCliVersion());
|
|
@@ -229,6 +245,7 @@ async function main() {
|
|
|
229
245
|
case 'deploy-effects': return cmdDeployEffects(flags);
|
|
230
246
|
case 'mint-page': return cmdMintPage(rest[0], flags);
|
|
231
247
|
case 'set-royalty': return cmdSetRoyalty(rest[0], flags);
|
|
248
|
+
case 'set-transfer-validator': return cmdSetTransferValidator(rest[0], rest.slice(1), flags);
|
|
232
249
|
case 'set-field': return cmdSetField(rest[0], flags);
|
|
233
250
|
case 'attach': return cmdAttach(rest, flags);
|
|
234
251
|
case 'lock-field': return cmdLockField(rest[0], flags);
|
|
@@ -429,6 +446,7 @@ function deployCommandLine(flags, salt) {
|
|
|
429
446
|
str('compress', flags.compress);
|
|
430
447
|
str('royalty-bps', flags['royalty-bps']);
|
|
431
448
|
bool('no-mint');
|
|
449
|
+
str('721c', flags['721c'] === 'true' ? 'recommended' : flags['721c']); // bare --721c ≡ recommended
|
|
432
450
|
str('backend', flags.backend);
|
|
433
451
|
str('gateway', flags.gateway);
|
|
434
452
|
str('bucket', flags.bucket);
|
|
@@ -472,6 +490,7 @@ function deploySeriesCommandLine(flags, salt) {
|
|
|
472
490
|
str('minter', flags.minter);
|
|
473
491
|
str('primary-payee', flags['primary-payee']);
|
|
474
492
|
str('royalty-bps', flags['royalty-bps']);
|
|
493
|
+
str('721c', flags['721c'] === 'true' ? 'recommended' : flags['721c']); // bare --721c ≡ recommended
|
|
475
494
|
if (flags.sign !== undefined)
|
|
476
495
|
parts.push('--sign');
|
|
477
496
|
if (flags.unsigned !== undefined)
|
|
@@ -512,6 +531,7 @@ function deployCodeCommandLine(flags, salt) {
|
|
|
512
531
|
str('minter', flags.minter);
|
|
513
532
|
str('primary-payee', flags['primary-payee']);
|
|
514
533
|
str('royalty-bps', flags['royalty-bps']);
|
|
534
|
+
str('721c', flags['721c'] === 'true' ? 'recommended' : flags['721c']); // bare --721c ≡ recommended
|
|
515
535
|
str('backend', flags.backend);
|
|
516
536
|
str('gateway', flags.gateway);
|
|
517
537
|
str('public-base-url', flags['public-base-url']);
|
|
@@ -688,10 +708,12 @@ async function cmdSetRendererCli(address, flags) {
|
|
|
688
708
|
flags.renderer = await ensureRenderer();
|
|
689
709
|
return cmdSetRenderer(address, flags);
|
|
690
710
|
}
|
|
691
|
-
/** Is the configured renderer the spec version this build expects (
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
711
|
+
/** Is the configured renderer the spec version this build expects (v4 — the computed `abx_params`
|
|
712
|
+
* object, enumerated from chain, plus `animation_url` wrapped as `data:text/html;base64` for the
|
|
713
|
+
* inline/reader representations)? A behind-spec renderer is treated as stale so {ensureRenderer}
|
|
714
|
+
* deploys a fresh one (greenfield redeploy-freely: this number is what tells the CLI a deployed
|
|
715
|
+
* renderer predates a projection the current build emits). A v4 renderer is safe on ANY token —
|
|
716
|
+
* pointed at a legacy implementation it simply emits no params block. Bump in lockstep with
|
|
695
717
|
* `AbxMetadataRenderer.SPEC_VERSION`. */
|
|
696
718
|
async function isCurrentRenderer(publicClient, renderer) {
|
|
697
719
|
try {
|
|
@@ -700,7 +722,7 @@ async function isCurrentRenderer(publicClient, renderer) {
|
|
|
700
722
|
abi: abxMetadataRendererAbi,
|
|
701
723
|
functionName: 'specVersion',
|
|
702
724
|
}));
|
|
703
|
-
return v ===
|
|
725
|
+
return v === 4n;
|
|
704
726
|
}
|
|
705
727
|
catch {
|
|
706
728
|
return false;
|
|
@@ -1130,7 +1152,7 @@ async function remoteLocators(address, stored, flags) {
|
|
|
1130
1152
|
}
|
|
1131
1153
|
// ── deploy (+ optionally serve) ──────────────────────────────────────────────
|
|
1132
1154
|
async function cmdDeploy(flags, serveAfter) {
|
|
1133
|
-
|
|
1155
|
+
refuseStrayFlags(flags, DEPLOY_FLAGS, 'deploy');
|
|
1134
1156
|
warnSignWithoutFor(flags);
|
|
1135
1157
|
const dimension = resolveDimension(flags.type ?? '1of1');
|
|
1136
1158
|
// A multi-token collection is its OWN command; `abx deploy` is the single-token (1/1) path.
|
|
@@ -1192,6 +1214,8 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1192
1214
|
// a wrong-network RPC must still be caught with the clear mismatch message rather than failing
|
|
1193
1215
|
// opaquely inside predict; `allowUnreachable` keeps a genuinely offline dry-run previewable.
|
|
1194
1216
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
1217
|
+
// --721c (opt-in ERC-721C): absent → zeroAddress, a plain ERC-721 exactly as before.
|
|
1218
|
+
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun);
|
|
1195
1219
|
// `demo` is exempt: its whole job is a zero-argument first token.
|
|
1196
1220
|
if (!serveAfter)
|
|
1197
1221
|
assertRealIdentity(flags, { name, symbol, dryRun });
|
|
@@ -1440,6 +1464,7 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1440
1464
|
contractURIRenderer: renderer,
|
|
1441
1465
|
royaltyReceiver: deployer,
|
|
1442
1466
|
royaltyBps,
|
|
1467
|
+
transferValidator,
|
|
1443
1468
|
tokenFields,
|
|
1444
1469
|
// authorship + rights (artist / license / …) are collection-scope on-chain fields.
|
|
1445
1470
|
contractFields: authorshipContractFields(flags),
|
|
@@ -1494,7 +1519,8 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1494
1519
|
return;
|
|
1495
1520
|
}
|
|
1496
1521
|
// opt-in --confirm: a final y/N before the real send (no-op without --confirm; never blocks scripts)
|
|
1497
|
-
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}
|
|
1522
|
+
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}%.` +
|
|
1523
|
+
(transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
1498
1524
|
if (lane === 'send') {
|
|
1499
1525
|
// hot lane: the env key is the deployer, known up front.
|
|
1500
1526
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
@@ -1738,7 +1764,7 @@ const tokenFieldOf = (tokenId, f) => ({
|
|
|
1738
1764
|
value: f.value,
|
|
1739
1765
|
});
|
|
1740
1766
|
async function cmdDeploySeries(flags) {
|
|
1741
|
-
|
|
1767
|
+
refuseStrayFlags(flags, DEPLOY_SERIES_FLAGS, 'deploy-series');
|
|
1742
1768
|
warnSignWithoutFor(flags);
|
|
1743
1769
|
const name = flags.name ?? 'ABX Series';
|
|
1744
1770
|
const symbol = flags.symbol ?? 'ABXS';
|
|
@@ -1749,7 +1775,7 @@ async function cmdDeploySeries(flags) {
|
|
|
1749
1775
|
if (!flags.dir) {
|
|
1750
1776
|
throw new Error('abx deploy-series --dir <media-dir> [--count N] [--mint-all | --mint-count N | --no-mint] ' +
|
|
1751
1777
|
'[--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)] ' +
|
|
1752
|
-
'[--minter 0x..] [--primary-payee 0x..] [--unpaused] --name "Title" --symbol SYM');
|
|
1778
|
+
'[--minter 0x..] [--primary-payee 0x..] [--unpaused] [--721c recommended|0x..] --name "Title" --symbol SYM');
|
|
1753
1779
|
}
|
|
1754
1780
|
const dirPath = resolvePath(flags.dir);
|
|
1755
1781
|
const files = readdirSync(dirPath)
|
|
@@ -1772,6 +1798,8 @@ async function cmdDeploySeries(flags) {
|
|
|
1772
1798
|
// Catch a wrong-network RPC with the clear mismatch message even on dry-run (which still reads
|
|
1773
1799
|
// the chain to predict the address); tolerate an unreachable RPC so an offline preview still works.
|
|
1774
1800
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
1801
|
+
// --721c (opt-in ERC-721C): absent → zeroAddress, a plain ERC-721 exactly as before.
|
|
1802
|
+
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun);
|
|
1775
1803
|
// (identity guard already ran above, via the shared assertRealIdentity — before any RPC)
|
|
1776
1804
|
// Mint timing: mint-all → the whole series; mint-count N → the first N; else deferred.
|
|
1777
1805
|
const mintCount = flags['mint-all'] !== undefined ? count : flags['mint-count'] ? Number(flags['mint-count']) : 0;
|
|
@@ -1986,7 +2014,8 @@ async function cmdDeploySeries(flags) {
|
|
|
1986
2014
|
: onChainUri
|
|
1987
2015
|
? 'inline SVG on-chain'
|
|
1988
2016
|
: `off-chain custody → ${baseUrl}`;
|
|
1989
|
-
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}
|
|
2017
|
+
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}%.` +
|
|
2018
|
+
(transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : ''), flags);
|
|
1990
2019
|
}
|
|
1991
2020
|
if (!onchainImage && !remoteEthUpload)
|
|
1992
2021
|
await buildFields();
|
|
@@ -2005,6 +2034,7 @@ async function cmdDeploySeries(flags) {
|
|
|
2005
2034
|
contractURIRenderer: renderer,
|
|
2006
2035
|
royaltyReceiver: deployer,
|
|
2007
2036
|
royaltyBps,
|
|
2037
|
+
transferValidator,
|
|
2008
2038
|
maxInvocations: count,
|
|
2009
2039
|
primaryPayee,
|
|
2010
2040
|
minter,
|
|
@@ -2931,9 +2961,14 @@ async function cmdPreview(flags) {
|
|
|
2931
2961
|
if (shootDir) {
|
|
2932
2962
|
step(`Render ${count} seeds headlessly`);
|
|
2933
2963
|
try {
|
|
2964
|
+
const shotParams = parsePreviewParams(flags.param);
|
|
2965
|
+
if (Object.keys(shotParams).length) {
|
|
2966
|
+
info(`param overrides applied to every frame: ${Object.entries(shotParams).map(([k, v]) => `${k}=${v || dim('(unset)')}`).join(' · ')}`);
|
|
2967
|
+
}
|
|
2934
2968
|
const shots = await shootPreview(server.url, shootDir, count, {
|
|
2935
2969
|
width: Number(flags.width ?? 1000) || 1000,
|
|
2936
2970
|
timeoutMs: Number(flags['timeout-ms'] ?? 10_000) || 10_000,
|
|
2971
|
+
params: shotParams,
|
|
2937
2972
|
});
|
|
2938
2973
|
ok(`${shots.length} frames → ${shootDir}/ ${dim('(traits in traits.json)')}`);
|
|
2939
2974
|
for (const s of shots) {
|
|
@@ -3069,7 +3104,7 @@ const DEPLOY_CODE_FLAGS = new Set([
|
|
|
3069
3104
|
...AUTHORSHIP_DEPLOY_FIELDS.map(([flag]) => flag), // artist · display-notes · artist-links · license
|
|
3070
3105
|
'image-base', 'attributes-renderer', 'image-renderer',
|
|
3071
3106
|
// supply + mint + economics
|
|
3072
|
-
'max', 'mint-count', 'mint-all', 'no-mint', 'unpaused', 'minter', 'primary-payee', 'royalty-bps',
|
|
3107
|
+
'max', 'mint-count', 'mint-all', 'no-mint', 'unpaused', 'minter', 'primary-payee', 'royalty-bps', '721c',
|
|
3073
3108
|
// params + dependencies
|
|
3074
3109
|
'schema', 'no-seed', 'no-delegation', 'dep', 'dep-registry',
|
|
3075
3110
|
// serving lane
|
|
@@ -3085,6 +3120,7 @@ const DEPLOY_CODE_FLAGS = new Set([
|
|
|
3085
3120
|
const SHARED_DEPLOY_FLAGS = [
|
|
3086
3121
|
'backend', 'endpoint', 'bucket', 'region', 'prefix', 'public-base', 'gateway', 'mode', 'api-url', 'upload-url', 'provider', 'storage-signer',
|
|
3087
3122
|
'sign', 'unsigned', 'for', 'sign-url-file', 'dry-run', 'confirm', 'yes', 'salt', 'factory', 'bootstrap-factory', 'port', 'renderer', 'public-base-url',
|
|
3123
|
+
'721c', // opt-in ERC-721C enrollment (recommended | 0x…) — every deploy path takes it
|
|
3088
3124
|
];
|
|
3089
3125
|
// A 1/1 `abx deploy` accepts these (see `abx help deploy`). Anything else warns (typo'd/unsupported).
|
|
3090
3126
|
const DEPLOY_FLAGS = new Set([
|
|
@@ -3113,6 +3149,33 @@ const FLAG_HINTS = {
|
|
|
3113
3149
|
supply: () => 'did you mean --max <N> (the supply cap)?',
|
|
3114
3150
|
amount: () => 'a sale price/amount is set post-deploy via `abx minter configure`.',
|
|
3115
3151
|
};
|
|
3152
|
+
/**
|
|
3153
|
+
* Unknown flags on a command that can SEND: refuse, don't warn.
|
|
3154
|
+
*
|
|
3155
|
+
* This used to warn and carry on, and `--chain sepolia` proved why that isn't enough — the warning was
|
|
3156
|
+
* accurate, it scrolled past, and the deploy ran on the DEFAULT chain. On a dry run that costs a
|
|
3157
|
+
* confused minute; on a funded send it is a wrong-chain deploy. "Prose that was ignored once will be
|
|
3158
|
+
* ignored again" is the standing rule here, so the tx-sending commands stop instead.
|
|
3159
|
+
*
|
|
3160
|
+
* Safe to refuse rather than warn because a flag absent from the allowlist is, by construction, one
|
|
3161
|
+
* the command never reads — verified per command by comparing every `flags.x` read in its body against
|
|
3162
|
+
* its allowlist. So refusal cannot break a working flag; it only makes an already-ignored one loud.
|
|
3163
|
+
*
|
|
3164
|
+
* `--dry-run` refuses identically. A preview that accepts what the real send rejects is its own trap:
|
|
3165
|
+
* you'd validate a command in preview and have it fail at the moment it matters.
|
|
3166
|
+
*/
|
|
3167
|
+
function refuseStrayFlags(flags, allowed, cmd) {
|
|
3168
|
+
const stray = unknownFlags(flags, allowed);
|
|
3169
|
+
if (!stray.length)
|
|
3170
|
+
return;
|
|
3171
|
+
const hints = stray.map((f) => FLAG_HINTS[f]?.(cmd)).filter(Boolean);
|
|
3172
|
+
throw new Error(`unrecognized flag(s): ${stray.map((f) => '--' + f).join(', ')} — this command would have ignored them silently, so it is refusing instead.\n` +
|
|
3173
|
+
hints.map((h, i) => ` ↳ --${stray[i]}: ${h}`).join('\n') +
|
|
3174
|
+
(hints.length ? '\n' : '') +
|
|
3175
|
+
` see \`abx help ${cmd}\` for the flags it accepts.`);
|
|
3176
|
+
}
|
|
3177
|
+
/** Unknown flags on a READ-ONLY command: warn, don't refuse. Nothing can be mis-sent, and a stray
|
|
3178
|
+
* flag on `preview`/`inspect` shouldn't stop a creator mid-iteration. */
|
|
3116
3179
|
function warnStrayFlags(flags, allowed, cmd) {
|
|
3117
3180
|
const stray = unknownFlags(flags, allowed);
|
|
3118
3181
|
if (stray.length) {
|
|
@@ -3138,7 +3201,7 @@ async function cmdDeployCode(flags) {
|
|
|
3138
3201
|
'[--description "<s>"] [--external-url <url>] [--image-base <url> | --image-renderer 0x..] [--attributes-renderer 0x..] ' +
|
|
3139
3202
|
'[--max N] [--mint-count N | --mint-all] [--schema key:Type:Auth,…] [--no-seed] ' +
|
|
3140
3203
|
'[--dep <name@version|0x..>[,…]] [--dep-registry 0x..] ' +
|
|
3141
|
-
'[--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--backend ipfs|arweave] [--dry-run] [--confirm] [--bootstrap-factory]';
|
|
3204
|
+
'[--unpaused] [--minter 0x..] [--primary-payee 0x..] [--royalty-bps N] [--721c recommended|0x..] [--backend ipfs|arweave] [--dry-run] [--confirm] [--bootstrap-factory]';
|
|
3142
3205
|
const scriptPath = flags.script;
|
|
3143
3206
|
const codeDir = flags['code-dir'];
|
|
3144
3207
|
const hasProgram = !!(scriptPath || codeDir); // a JS program (script or built dir)
|
|
@@ -3154,7 +3217,7 @@ async function cmdDeployCode(flags) {
|
|
|
3154
3217
|
// Surface unsupported/typo'd flags BEFORE any work — a silent no-op on a write-adjacent value
|
|
3155
3218
|
// (e.g. --description landing nowhere) is the worst failure mode. Non-fatal (see unknownFlags).
|
|
3156
3219
|
// Shared path so the "did you mean --max / price is post-deploy" hints fire here too.
|
|
3157
|
-
|
|
3220
|
+
refuseStrayFlags(flags, DEPLOY_CODE_FLAGS, 'deploy-code');
|
|
3158
3221
|
warnSignWithoutFor(flags);
|
|
3159
3222
|
const dryRun = !!flags['dry-run'];
|
|
3160
3223
|
const name = flags.name ?? 'ABX Code';
|
|
@@ -3211,6 +3274,8 @@ async function cmdDeployCode(flags) {
|
|
|
3211
3274
|
// chain to resolve the factory/renderer and predict the address); tolerate an offline preview.
|
|
3212
3275
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
3213
3276
|
const chainId = dryRun ? resolveChain(CHAIN).id : (await publicClient.getChainId());
|
|
3277
|
+
// --721c (opt-in ERC-721C): absent → zeroAddress, a plain ERC-721 exactly as before.
|
|
3278
|
+
const transferValidator = await resolveTransferValidatorFlag(flags, publicClient, dryRun);
|
|
3214
3279
|
step('Trust anchor');
|
|
3215
3280
|
let factory;
|
|
3216
3281
|
if (dryRun) {
|
|
@@ -3589,27 +3654,21 @@ async function cmdDeployCode(flags) {
|
|
|
3589
3654
|
// gas cap and appear to REVERT. Expected, not a bug — a real session mis-diagnosed it as indexing lag.
|
|
3590
3655
|
const tokenUriGasRisk = onChainUri && !codeDir && (scriptAnalysis?.doc.estBytes ?? 0) > 30_000;
|
|
3591
3656
|
// --onchain-uri legs (ride the setup multicall, before the mints): the animation_url field
|
|
3592
|
-
// pointing at the generator, the
|
|
3593
|
-
//
|
|
3594
|
-
//
|
|
3595
|
-
//
|
|
3596
|
-
// the generator's on-chain tokenData stays byte-aligned with the resolver's (which carries
|
|
3597
|
-
// every evented param — inputsHash parity is load-bearing).
|
|
3657
|
+
// pointing at the generator, and the two URI renderers. The param surface needs NO leg — the
|
|
3658
|
+
// generator enumerates it from the token on-chain (`contractParamKeys`/`tokenParamKeys`, kept in
|
|
3659
|
+
// step by the write paths themselves), so there is nothing for a deploy to declare and nothing to
|
|
3660
|
+
// drift. (The retired `params.keys` CSV convention is gone; a legacy project keeps its old generator.)
|
|
3598
3661
|
// THE FOLD: on the renderer-only in-chain lane (no program), the on-chain-URI wiring (tokenURI/
|
|
3599
3662
|
// contractURI renderers) + the reserve mint move INTO the deploy tx's init params — the token is
|
|
3600
3663
|
// fully configured at deploy (the renderers already exist on-chain), so the setup multicall is
|
|
3601
3664
|
// needed ONLY for a PostParam schema (and is skipped entirely when there's none → a 1-tx drop).
|
|
3602
3665
|
// The script/dir lane is UNCHANGED: its chunks always need the multicall, and its mint must ride
|
|
3603
3666
|
// AFTER the chunks (a mint-before-chunks token would be transiently unresolvable), so its
|
|
3604
|
-
// URI-renderers + animation +
|
|
3667
|
+
// URI-renderers + animation + mint stay in the multicall exactly as before.
|
|
3605
3668
|
const foldIntoInit = onChainUri && !hasProgram;
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
// generator consumes params.keys; renderer-only has no generator and folds the renderers to init).
|
|
3610
|
-
const onchainUriLegs = onChainUri && hasProgram
|
|
3611
|
-
? onchainUriSetupCalls({ generator, metadataRenderer, paramsKeys: paramsKeysCsv })
|
|
3612
|
-
: null;
|
|
3669
|
+
// The URI-renderer/animation legs only ride the multicall for a PROGRAM (renderer-only has no
|
|
3670
|
+
// generator and folds the renderers into init).
|
|
3671
|
+
const onchainUriLegs = onChainUri && hasProgram ? onchainUriSetupCalls({ generator, metadataRenderer }) : null;
|
|
3613
3672
|
const setupMintCount = foldIntoInit ? 0 : mintCount; // folded mints ride init, not the multicall
|
|
3614
3673
|
// opt-in --confirm: one y/N before ANY upload or send (no-op without --confirm; never blocks scripts).
|
|
3615
3674
|
if (!dryRun) {
|
|
@@ -3617,12 +3676,13 @@ async function cmdDeployCode(flags) {
|
|
|
3617
3676
|
await confirmSend(`About to deploy code project "${name}" (${symbol}) — ${contentSummary}; ` +
|
|
3618
3677
|
(identityFields.length ? `${identityFields.join(' + ')} on-chain; ` : '') +
|
|
3619
3678
|
(onChainUri
|
|
3620
|
-
? `tokenURI ON-CHAIN via renderer ${metadataRenderer}${hasProgram ? ` (generator ${generator}
|
|
3679
|
+
? `tokenURI ON-CHAIN via renderer ${metadataRenderer}${hasProgram ? ` (generator ${generator})` : ' (renderer-only — image + traits from your Solidity renderers, no animation)'}; `
|
|
3621
3680
|
: `resolver base ${baseUrl}; `) +
|
|
3622
3681
|
`dependencies: ${deps.length ? deps.map((d) => d.display).join(', ') + (depRegistry ? ` (registry ${depRegistry})` : '') : 'none'}; ` +
|
|
3623
3682
|
`mint: ${mintCount > 0 ? `${mintCount} at deploy${foldIntoInit ? ' (in the deploy tx)' : ''}` : 'deferred'}; ` +
|
|
3624
3683
|
`tx count: ${foldIntoInit ? (schemas.length ? '2 (deploy + enable your params)' : '1 (everything in the deploy tx)') : '2 (deploy + setup)'}; ` +
|
|
3625
|
-
`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}
|
|
3684
|
+
`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}%.` +
|
|
3685
|
+
(transferValidator !== zeroAddress ? ` ERC-721C: enrolled at deploy, permanently (validator ${transferValidator}).` : '') + `\n` +
|
|
3626
3686
|
` Surfaces — thumbnail: ${imageDisposition}. traits: ${traitsDisposition}. postparams: ${paramsDisposition}.` +
|
|
3627
3687
|
((imageOrphaned || traitsBroken || undeclaredParams.length)
|
|
3628
3688
|
? `\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.`
|
|
@@ -3652,7 +3712,7 @@ async function cmdDeployCode(flags) {
|
|
|
3652
3712
|
? (await publicClient.readContract({ address: factory, abi: seriesCodeFactoryAbi, functionName: 'predictDeterministicAddress', args: [salt] }))
|
|
3653
3713
|
: null;
|
|
3654
3714
|
const depLegs = deps.length + (depRegistry ? 1 : 0);
|
|
3655
|
-
const setupLen = scriptChunks.length + schemas.length + depLegs + (onchainUriLegs?.
|
|
3715
|
+
const setupLen = scriptChunks.length + schemas.length + depLegs + (onchainUriLegs?.length ?? 0) + setupMintCount;
|
|
3656
3716
|
step(`Deploy plan — a ${max}-token code project to ${CHAIN}`);
|
|
3657
3717
|
if (deployer)
|
|
3658
3718
|
info(`deployer ${deployer}`);
|
|
@@ -3677,7 +3737,7 @@ async function cmdDeployCode(flags) {
|
|
|
3677
3737
|
: `; no animation_url (renderer-only — image + attributes computed on-chain by your Solidity renderer(s))`));
|
|
3678
3738
|
if (hasProgram) {
|
|
3679
3739
|
info(` on-chain URI legs (ride the setup multicall): collection animation_url field (renderer rep → ${generator}) · ` +
|
|
3680
|
-
`
|
|
3740
|
+
`setTokenURIRenderer · setContractURIRenderer ${dim('(the param surface enumerates on-chain — no leg, nothing to maintain)')}`);
|
|
3681
3741
|
}
|
|
3682
3742
|
else {
|
|
3683
3743
|
info(` on-chain URI wiring rides the ${bold('deploy tx')} itself (tokenURI/contractURI renderers${mintCount > 0 ? ' + the reserve mint' : ''} in init) — no separate setup tx for it.`);
|
|
@@ -3729,7 +3789,7 @@ async function cmdDeployCode(flags) {
|
|
|
3729
3789
|
BigInt(storedBytes) * 216n + // ~200 gas/byte code deposit + ~16 gas/byte calldata
|
|
3730
3790
|
BigInt(schemas.length) * 45000n +
|
|
3731
3791
|
BigInt(depLegs) * 55000n +
|
|
3732
|
-
BigInt(onchainUriLegs?.
|
|
3792
|
+
BigInt(onchainUriLegs?.length ?? 0) * 60000n +
|
|
3733
3793
|
BigInt(mintCount) * 65000n;
|
|
3734
3794
|
const gasPrice = await publicClient.getGasPrice();
|
|
3735
3795
|
const eth = Number(roughGas * gasPrice) / 1e18;
|
|
@@ -3819,6 +3879,7 @@ async function cmdDeployCode(flags) {
|
|
|
3819
3879
|
contractURIRenderer: foldIntoInit ? metadataRenderer : zeroAddress,
|
|
3820
3880
|
royaltyReceiver: owner,
|
|
3821
3881
|
royaltyBps,
|
|
3882
|
+
transferValidator,
|
|
3822
3883
|
maxInvocations: BigInt(max),
|
|
3823
3884
|
primaryPayee: flags['primary-payee'] ?? zeroAddress,
|
|
3824
3885
|
minter: flags.minter ?? zeroAddress,
|
|
@@ -3835,7 +3896,7 @@ async function cmdDeployCode(flags) {
|
|
|
3835
3896
|
...scriptChunks.map((chunk, i) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] })),
|
|
3836
3897
|
...schemas.map(({ key, paramType, auth, authAddress, lockAfter, min, max, selectOptions }) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth, authAddress, lockAfter, min, max, selectOptions] })),
|
|
3837
3898
|
...dependencySetupCalls(deps, depRegistry), // setDependency legs in index order (+ the soft registry pointer)
|
|
3838
|
-
...(onchainUriLegs
|
|
3899
|
+
...(onchainUriLegs ?? []), // program lane only: animation_url field · the URI renderers (before the mints)
|
|
3839
3900
|
].concat(Array.from({ length: setupMintCount }, () => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'mint', args: [owner] }))); // folded mints ride init, not here
|
|
3840
3901
|
const cid = resolveChain(CHAIN).id;
|
|
3841
3902
|
const preparedFor = async (owner) => {
|
|
@@ -3935,7 +3996,7 @@ async function cmdDeployCode(flags) {
|
|
|
3935
3996
|
step('On-chain URI');
|
|
3936
3997
|
ok(`tokenURI/contractURI resolve ON-CHAIN via the renderer ${metadataRenderer} — any RPC returns the metadata, no server in the graph`);
|
|
3937
3998
|
if (hasProgram)
|
|
3938
|
-
info(`animation_url computes on-chain via the generator ${generator}
|
|
3999
|
+
info(`animation_url computes on-chain via the generator ${generator} — it enumerates this token's params from chain, so there is no key list to maintain`);
|
|
3939
4000
|
else
|
|
3940
4001
|
info(`renderer-only — image + attributes computed on-chain by your Solidity renderer(s); no animation_url · ${g('fully on-chain')}, zero dependency outside the EVM`);
|
|
3941
4002
|
if (hasProgram && !codeDir) {
|
|
@@ -4365,6 +4426,18 @@ async function cmdVerify(address, flags) {
|
|
|
4365
4426
|
console.log(` ${c.orange}⚠${c.reset} no animation_url in the on-chain JSON — the generator field may be missing or unrenderable`);
|
|
4366
4427
|
}
|
|
4367
4428
|
}
|
|
4429
|
+
// The READ-side envelope. Params enumerate on-chain, so the write side is unbounded — but
|
|
4430
|
+
// tokenURI and tokenData assemble EVERY enumerated param per call, and that is what grows.
|
|
4431
|
+
// ~64 keys is the documented design envelope; hard failure only nears at several hundred.
|
|
4432
|
+
// Advisory (never a ✗) — a big surface is a legitimate choice, it just isn't free.
|
|
4433
|
+
const set = await readSetParamKeys(client, state.address, probe ? BigInt(probe.tokenId) : undefined);
|
|
4434
|
+
if (set) {
|
|
4435
|
+
const total = new Set([...set.contract, ...set.token]).size;
|
|
4436
|
+
if (total > 64) {
|
|
4437
|
+
console.log(` ${c.orange}⚠${c.reset} ${total} params enumerate on-chain${probe ? ` for token #${probe.tokenId}` : ''} — tokenURI and tokenData assemble every one of them per call, so both grow with this count. ` +
|
|
4438
|
+
dim('The documented design envelope is ~64; past a few hundred a public eth_call can hit its gas cap.'));
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4368
4441
|
}
|
|
4369
4442
|
catch (e) {
|
|
4370
4443
|
console.log(` ${c.orange}⚠${c.reset} on-chain URI check unavailable: ${e.message}`);
|
|
@@ -4543,29 +4616,35 @@ async function reportRemoteByteIntegrity(address, remote, base) {
|
|
|
4543
4616
|
// Read tokenURI(id) STRAIGHT FROM THE CONTRACT on-chain (no server, no node) and decode
|
|
4544
4617
|
// the data: URI. For a fully on-chain token (`--onchain-uri`), this is the proof it
|
|
4545
4618
|
// self-resolves: the renderer assembles the JSON on-chain; any RPC returns it.
|
|
4546
|
-
function decodeOnChainJson(uri) {
|
|
4619
|
+
function decodeOnChainJson(uri, verbatim = false) {
|
|
4547
4620
|
const m = /^data:application\/json(;[^,]*)?,([\s\S]*)$/.exec(uri);
|
|
4548
4621
|
if (!m)
|
|
4549
4622
|
return null;
|
|
4550
4623
|
const raw = (m[1] ?? '').includes('base64') ? Buffer.from(m[2], 'base64').toString('utf8') : decodeURIComponent(m[2]);
|
|
4551
4624
|
try {
|
|
4552
4625
|
const obj = JSON.parse(raw);
|
|
4626
|
+
// `verbatim` (--json) returns the document EXACTLY as the chain assembled it. Everything below is
|
|
4627
|
+
// a courtesy for human eyes, and abbreviating for eyes is fine — abbreviating for a PROGRAM is
|
|
4628
|
+
// not. An integrator scraped this output, got a `data:` URI cut to 96 chars that still looked
|
|
4629
|
+
// valid, and stored it; the only way to read their own token was to reimplement `eth_call`.
|
|
4630
|
+
if (verbatim)
|
|
4631
|
+
return JSON.stringify(obj, null, 2);
|
|
4553
4632
|
for (const k of Object.keys(obj)) {
|
|
4554
4633
|
const v = obj[k];
|
|
4555
4634
|
// Truncate only huge inline `data:` blobs (base64 SVGs/images); show plain locator URLs
|
|
4556
4635
|
// (http/ipfs/ar) IN FULL so the creator can verify where the image actually resolves.
|
|
4557
4636
|
if (typeof v === 'string' && v.startsWith('data:') && v.length > 96)
|
|
4558
|
-
obj[k] = `${v.slice(0, 96)}… (${v.length} chars)`;
|
|
4637
|
+
obj[k] = `${v.slice(0, 96)}… (${v.length} chars) [--json for the full value]`;
|
|
4559
4638
|
}
|
|
4560
4639
|
return JSON.stringify(obj, null, 2);
|
|
4561
4640
|
}
|
|
4562
4641
|
catch {
|
|
4563
|
-
return raw.slice(0, 600);
|
|
4642
|
+
return verbatim ? raw : raw.slice(0, 600);
|
|
4564
4643
|
}
|
|
4565
4644
|
}
|
|
4566
4645
|
async function cmdTokenUri(address, flags) {
|
|
4567
4646
|
if (!address || address.startsWith('--')) {
|
|
4568
|
-
console.error('usage: abx tokenuri <address> [--token <id>]\n');
|
|
4647
|
+
console.error('usage: abx tokenuri <address> [--token <id>] [--json]\n');
|
|
4569
4648
|
process.exit(1);
|
|
4570
4649
|
}
|
|
4571
4650
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
@@ -4600,6 +4679,12 @@ async function cmdTokenUri(address, flags) {
|
|
|
4600
4679
|
}
|
|
4601
4680
|
process.exit(1);
|
|
4602
4681
|
}
|
|
4682
|
+
// --json: the verbatim document, nothing else on stdout — no banner, no ANSI, no truncation, so
|
|
4683
|
+
// `abx tokenuri <addr> --json | jq` is a supported read path rather than something to regex.
|
|
4684
|
+
if (flags.json !== undefined) {
|
|
4685
|
+
process.stdout.write((decodeOnChainJson(uri, true) ?? uri) + '\n');
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4603
4688
|
console.log(`\n ${bold(`tokenURI(${tokenId})`)} ${dim(`— read directly from ${address} on ${CHAIN}, no server`)}`);
|
|
4604
4689
|
const json = decodeOnChainJson(uri);
|
|
4605
4690
|
if (json) {
|
|
@@ -4844,7 +4929,7 @@ async function cmdStorage(rest) {
|
|
|
4844
4929
|
*/
|
|
4845
4930
|
async function cmdStorageUpload(path, flags) {
|
|
4846
4931
|
if (!path || path.startsWith('--')) {
|
|
4847
|
-
console.error('usage: abx storage upload <path> [--backend ipfs|arweave|cloud] [--key <name>]\n uploads one file and prints its locator (the URI `abx attach` wants).\n');
|
|
4932
|
+
console.error('usage: abx storage upload <path> [--backend ipfs|arweave|cloud] [--key <name>] [--json]\n uploads one file and prints its locator (the URI `abx attach` wants); --json emits it as data.\n');
|
|
4848
4933
|
process.exit(1);
|
|
4849
4934
|
}
|
|
4850
4935
|
const abs = resolvePath(path);
|
|
@@ -4856,7 +4941,9 @@ async function cmdStorageUpload(path, flags) {
|
|
|
4856
4941
|
throw new Error("the 'fs' backend has no public URL — an attached file must be reachable off this machine. " +
|
|
4857
4942
|
'Upload with `--backend arweave` (pay-once permanent) or `--backend ipfs` (keep it pinned), or `--backend cloud` (your S3/R2).');
|
|
4858
4943
|
}
|
|
4859
|
-
|
|
4944
|
+
const jsonMode = flags.json !== undefined;
|
|
4945
|
+
if (!jsonMode)
|
|
4946
|
+
noteArweavePlan(opts, bytes.length); // free-vs-credit readout for the arweave lane
|
|
4860
4947
|
const name = (flags.key && flags.key !== 'true' ? flags.key : basename(abs)).replace(/^\/+/, '');
|
|
4861
4948
|
// --dry-run: an upload is IRREVERSIBLE and can happen even keyless (Arweave's free tier under
|
|
4862
4949
|
// 100 KB auto-creates an identity), so a "preview" that actually uploaded would surprise. Show
|
|
@@ -4866,17 +4953,37 @@ async function cmdStorageUpload(path, flags) {
|
|
|
4866
4953
|
console.log(`\n ${bold('◆ storage upload')} ${dim('(dry run — nothing uploaded)')}`);
|
|
4867
4954
|
console.log(` ${dim('file'.padEnd(10))} ${basename(abs)} ${dim(`(${bytes.length} bytes, ${contentType})`)}`);
|
|
4868
4955
|
console.log(` ${dim('backend'.padEnd(10))} ${backend.id}`);
|
|
4956
|
+
// Name the IPFS mode, and say what an absent credential actually means. There is no silent
|
|
4957
|
+
// fallback to local `fs` (an earlier skill line wrongly claimed one) — without PINATA_JWT the
|
|
4958
|
+
// backend resolves to kubo against a LOCAL node, so a dry run that looked fine would fail at
|
|
4959
|
+
// upload for anyone not running one. A preview should surface that, not defer it to the failure.
|
|
4960
|
+
if (backend.id === 'ipfs' && !process.env.PINATA_JWT && !process.env.ABX_IPFS_MODE) {
|
|
4961
|
+
warn(`no ${bold('PINATA_JWT')} set → this resolves to a LOCAL IPFS node (kubo at ${process.env.ABX_IPFS_API_URL ?? 'http://127.0.0.1:5001'}). The real upload fails unless that node is running — set PINATA_JWT for the managed lane, or ${bold('--backend arweave')} for no-setup permanent storage.`);
|
|
4962
|
+
}
|
|
4869
4963
|
console.log(` ${dim('locator'.padEnd(10))} ${shape} ${dim('(filename preserved → declared type survives)')}`);
|
|
4870
4964
|
console.log(dim(`\n Re-run without --dry-run to upload, then: `) + `${g(`abx attach <address> <key> ${shape}`)}\n`);
|
|
4871
4965
|
return;
|
|
4872
4966
|
}
|
|
4873
|
-
|
|
4967
|
+
// In --json mode nothing but the JSON may touch stdout; progress still goes out, on stderr.
|
|
4968
|
+
const progress = (line) => (jsonMode ? process.stderr.write(line + '\n') : console.log(line));
|
|
4969
|
+
progress(` uploading ${bold(basename(abs))} ${dim(`(${bytes.length} bytes, ${contentType})`)} to '${backend.id}' …`);
|
|
4874
4970
|
// The returned locator MUST carry the filename so the declared mimeType survives when attached
|
|
4875
4971
|
// (the on-chain field has no MIME slot — the URL extension IS the declaration). uploadAndLocate
|
|
4876
4972
|
// encapsulates the capability-based branch (cloud key / dir-wrap / bare fallback).
|
|
4877
4973
|
const { locator, filenamePreserved, fallbackReason } = await uploadAndLocate(backend, name, { bytes, contentType });
|
|
4878
4974
|
if (!filenamePreserved && fallbackReason) {
|
|
4879
|
-
|
|
4975
|
+
const msg = `couldn't wrap the file with its name (${fallbackReason}) — the locator has no extension, so its declared type will be application/octet-stream when attached.`;
|
|
4976
|
+
if (jsonMode)
|
|
4977
|
+
process.stderr.write(` ⚠ ${msg}\n`);
|
|
4978
|
+
else
|
|
4979
|
+
warn(msg);
|
|
4980
|
+
}
|
|
4981
|
+
// --json: the locator as data, not as prose. An integrator scraped this line, captured the ANSI
|
|
4982
|
+
// colour codes along with the URL, wrote the result into a STORED player URL, and only found out
|
|
4983
|
+
// when it 404'd in production. A value a program needs must be obtainable without parsing output.
|
|
4984
|
+
if (jsonMode) {
|
|
4985
|
+
process.stdout.write(JSON.stringify({ locator, backend: backend.id, name, bytes: bytes.length, contentType, filenamePreserved }) + '\n');
|
|
4986
|
+
return;
|
|
4880
4987
|
}
|
|
4881
4988
|
console.log(` ${g('✓')} uploaded → ${bold(locator)}`);
|
|
4882
4989
|
if (contentType === 'application/octet-stream') {
|
|
@@ -4955,6 +5062,9 @@ const STATE_ABI = [
|
|
|
4955
5062
|
{ type: 'function', name: 'primaryPayee', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
4956
5063
|
{ type: 'function', name: 'tokenURIRenderer', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
4957
5064
|
{ type: 'function', name: 'royaltyInfo', stateMutability: 'view', inputs: [{ type: 'uint256' }, { type: 'uint256' }], outputs: [{ type: 'address' }, { type: 'uint256' }] },
|
|
5065
|
+
// ERC-721C (creator token) — ERC-165 advertises ICreatorToken ONLY when enrolled at deploy.
|
|
5066
|
+
{ type: 'function', name: 'supportsInterface', stateMutability: 'view', inputs: [{ type: 'bytes4' }], outputs: [{ type: 'bool' }] },
|
|
5067
|
+
{ type: 'function', name: 'getTransferValidator', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
4958
5068
|
];
|
|
4959
5069
|
async function cmdState(address, flags) {
|
|
4960
5070
|
if (!address || address.startsWith('--')) {
|
|
@@ -4971,7 +5081,7 @@ async function cmdState(address, flags) {
|
|
|
4971
5081
|
return undefined; // getter absent (extension not composed) or contract has no code
|
|
4972
5082
|
}
|
|
4973
5083
|
};
|
|
4974
|
-
const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty] = await Promise.all([
|
|
5084
|
+
const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator] = await Promise.all([
|
|
4975
5085
|
read('owner'),
|
|
4976
5086
|
read('totalSupply'),
|
|
4977
5087
|
read('nextTokenId'),
|
|
@@ -4981,6 +5091,8 @@ async function cmdState(address, flags) {
|
|
|
4981
5091
|
read('primaryPayee'),
|
|
4982
5092
|
read('tokenURIRenderer'),
|
|
4983
5093
|
read('royaltyInfo', [0n, 10000n]),
|
|
5094
|
+
read('supportsInterface', [CREATOR_TOKEN_INTERFACE_ID]),
|
|
5095
|
+
read('getTransferValidator'),
|
|
4984
5096
|
]);
|
|
4985
5097
|
if (owner === undefined && totalSupply === undefined) {
|
|
4986
5098
|
throw new Error(`no ABX contract state at ${address} on ${CHAIN} (no code, or not an ABX token).`);
|
|
@@ -5005,31 +5117,51 @@ async function cmdState(address, flags) {
|
|
|
5005
5117
|
if (royalty)
|
|
5006
5118
|
info(`royalty ${Number(royalty[1]) / 100}% → ${royalty[0]}`);
|
|
5007
5119
|
info(`renderer ${renderer && renderer !== zeroAddress ? `on-chain (${renderer})` : dim('off-chain (stored URI base / override)')}`);
|
|
5008
|
-
//
|
|
5009
|
-
//
|
|
5010
|
-
|
|
5011
|
-
|
|
5120
|
+
// ERC-721C: shown ONLY for an enrolled collection — an unenrolled token is a plain ERC-721
|
|
5121
|
+
// (indistinguishable from a pre-721C token) and shows nothing new here.
|
|
5122
|
+
if (creatorToken) {
|
|
5123
|
+
info(`721C validator ${transferValidator && transferValidator !== zeroAddress ? transferValidator : `${c.orange}suspended${c.reset} ${dim('(zero — enforcement off; re-enable with `abx set-transfer-validator`)')}`}`);
|
|
5124
|
+
}
|
|
5125
|
+
// The PostParam surface, read straight from chain: `paramSchemaKeys()` is the DECLARED (governed)
|
|
5126
|
+
// set — append-only, and it includes keys declared but never yet written, which nothing off-chain
|
|
5127
|
+
// could otherwise discover — and `contractParamKeys()` is the collection-scope set values. Both
|
|
5128
|
+
// are maintained by the contract's own write paths, so there is no list to drift. Read-only,
|
|
5129
|
+
// best-effort, and skipped entirely for a project that has none (a 1/1, or a code project with
|
|
5130
|
+
// no params). LEGACY projects (deployed before enumeration) expose neither getter: they fall back
|
|
5131
|
+
// to the retired `params.keys` CSV so `abx state` still describes them.
|
|
5012
5132
|
try {
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
if (keys
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5133
|
+
let keys = await readParamSchemaKeys(publicClient, address);
|
|
5134
|
+
let legacy = false;
|
|
5135
|
+
if (keys === null) {
|
|
5136
|
+
// The pre-enumeration lane, READ-ONLY. Nothing writes this CSV any more.
|
|
5137
|
+
keys = (await readLegacyParamsKeys(publicClient, address).catch(() => null)) ?? [];
|
|
5138
|
+
legacy = keys.length > 0;
|
|
5139
|
+
}
|
|
5140
|
+
const rows = keys.length
|
|
5141
|
+
? await Promise.all(keys.map(async (key) => ({ key, s: await readParamSchema(publicClient, address, key) })))
|
|
5142
|
+
: [];
|
|
5143
|
+
const governed = rows.filter((r) => r.s.exists);
|
|
5144
|
+
const now = Math.floor(Date.now() / 1000);
|
|
5145
|
+
if (governed.length) {
|
|
5146
|
+
console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/artist set these; the generator injects them`)}` +
|
|
5147
|
+
(legacy ? dim(' (legacy project — read from its params.keys list)') : ''));
|
|
5148
|
+
for (const { key, s } of governed) {
|
|
5149
|
+
const retired = s.lockAfter !== 0 && s.lockAfter < now;
|
|
5150
|
+
const locks = s.lockAfter !== 0 && !retired ? dim(` locks ${new Date(s.lockAfter * 1000).toISOString().slice(0, 10)}`) : '';
|
|
5151
|
+
const tag = retired ? ` ${c.orange}retired${c.reset}${dim(' — writes revert')}` : locks;
|
|
5152
|
+
console.log(` ${describeSchema({ key, paramType: s.paramType, auth: s.auth, authAddress: s.authAddress, lockAfter: 0, min: s.min, max: s.max, selectOptions: s.selectOptions })}${tag}`);
|
|
5027
5153
|
}
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5154
|
+
}
|
|
5155
|
+
// Legacy only: a key the old CSV lists with no schema behind it. On an enumerating project the
|
|
5156
|
+
// class doesn't exist — schema keys come from the schema list, set values from the value lists.
|
|
5157
|
+
const ungoverned = legacy ? rows.filter((r) => !r.s.exists).map((r) => r.key) : [];
|
|
5158
|
+
if (ungoverned.length)
|
|
5159
|
+
info(`${dim('also in params.keys, no schema:')} ${ungoverned.join(', ')}`);
|
|
5160
|
+
// Collection-scope values (every token sees these). `seed` is per-token and never enumerated —
|
|
5161
|
+
// it is a tokenData coordinate, read directly.
|
|
5162
|
+
const set = legacy ? null : await readSetParamKeys(publicClient, address);
|
|
5163
|
+
if (set?.contract.length) {
|
|
5164
|
+
info(`${dim('contract-scope params set:')} ${[...set.contract].sort().join(', ')} ${dim('(apply to every token)')}`);
|
|
5033
5165
|
}
|
|
5034
5166
|
}
|
|
5035
5167
|
catch {
|
|
@@ -5936,6 +6068,10 @@ const COMMAND_HELP = {
|
|
|
5936
6068
|
${g('--compress')} none|fastlz|gzip for --onchain-image (default none): ${g('fastlz')} = on-chain decode (stays renderable);
|
|
5937
6069
|
${g('gzip')} = smaller but off-chain decode only
|
|
5938
6070
|
--royalty-bps <0-10000> default 500 (5%)
|
|
6071
|
+
${g('--721c')} [recommended|0x..] opt-in ERC-721C: enroll at deploy, PERMANENTLY, with that transfer validator
|
|
6072
|
+
(${g('recommended')} = OpenSea's validator for creator-fee enforcement — only owner-initiated
|
|
6073
|
+
transfers + authorized sales move the token). Absent = plain ERC-721, forever (the default).
|
|
6074
|
+
Manage later with ${g('abx set-transfer-validator')} (re-point or suspend; never un-enroll).
|
|
5939
6075
|
${g('--no-mint')} deploy without minting (warm the resolver, then ${g('abx mint')})
|
|
5940
6076
|
--salt 0x..<64hex> deploy to a reserved / vanity address (see ${g('abx predict')})
|
|
5941
6077
|
--public-base-url <url> bake a public resolver URL on-chain (off-chain custody REQUIRES a public URL; localhost is refused)
|
|
@@ -5965,6 +6101,8 @@ const COMMAND_HELP = {
|
|
|
5965
6101
|
a huge collection should set traits post-deploy via ${g('abx set-field')} under a gas budget.
|
|
5966
6102
|
--minter 0x.. authorize a single minting contract --primary-payee 0x.. primary-sale payout
|
|
5967
6103
|
--unpaused open the mint at deploy (default: paused ⇒ owner-only until ${g('abx unpause')})
|
|
6104
|
+
${g('--721c')} [recommended|0x..] opt-in ERC-721C: enroll at deploy, PERMANENTLY, with that transfer validator (absent = plain
|
|
6105
|
+
ERC-721 forever). ${g('recommended')} = OpenSea's creator-fee-enforcement validator. Manage: ${g('abx set-transfer-validator')}
|
|
5968
6106
|
--salt 0x.. · --backend … (storage override) · ${g('--storage-signer')} arweave|eth (who pays Turbo uploads) · signing: ${g('--send')} · ${g('--sign')} · ${g('--unsigned')}
|
|
5969
6107
|
${g('--dry-run')} preview (address · per-token plan · custody · mint plan) WITHOUT sending or storing
|
|
5970
6108
|
${dim('(a dry-run needs a deployer to compute the address — pass --for 0x.. if no signing key is set)')}
|
|
@@ -6018,8 +6156,8 @@ const COMMAND_HELP = {
|
|
|
6018
6156
|
${g('--onchain-uri')} the CHAIN-COMPLETE lane: tokenURI/contractURI resolve ON-CHAIN via the canonical metadata renderer, and
|
|
6019
6157
|
animation_url is COMPUTED on-chain by the canonical ${bold('AbxGenerator')} (template: the full HTML document;
|
|
6020
6158
|
directory: a parameterized gateway URL — 8KB URL budget, gateway liveness applies). No resolver base is baked
|
|
6021
|
-
(no --public-base-url needed); the
|
|
6022
|
-
|
|
6159
|
+
(no --public-base-url needed); the param surface is enumerated ON-CHAIN, so there is nothing to declare or
|
|
6160
|
+
keep in sync. The marketplace still is rendered off-chain by the effect runner ${bold('for a JS program')} (--script/--code-dir) — but with ${g('--image-renderer')} the image is computed ON-CHAIN too (nothing to render). Verify with ${g('abx verify <addr>')}.
|
|
6023
6161
|
(--generator 0x.. / --renderer 0x.. override the canonical singletons; ABX_GENERATOR / ABX_RENDERER via env.)
|
|
6024
6162
|
--schema key:Type:Auth,… governed PostParams (e.g. ${g('palette:HexColor:TokenOwner')}) — set values later with ${g('abx configure-param')},
|
|
6025
6163
|
change the SCHEMA later with ${g('abx set-schema')}. An Address leg names its writer inline —
|
|
@@ -6046,6 +6184,8 @@ const COMMAND_HELP = {
|
|
|
6046
6184
|
a CDN-served record is the normal production path, on-chain bytes are the durability floor).
|
|
6047
6185
|
--max N supply cap (default 16) mint timing: ${g('--mint-all')} · ${g('--mint-count')} N · (default: defer, then ${g('abx mint')})
|
|
6048
6186
|
--no-seed opt out of the canonical mint-time seed (seeds settle once assigned; curated pre-set seeds win)
|
|
6187
|
+
${g('--721c')} [recommended|0x..] opt-in ERC-721C: enroll at deploy, PERMANENTLY, with that transfer validator (absent = plain
|
|
6188
|
+
ERC-721 forever). ${g('recommended')} = OpenSea's creator-fee-enforcement validator. Manage: ${g('abx set-transfer-validator')}
|
|
6049
6189
|
--backend ipfs|arweave directory-mode custody for the build upload ${g('--unpaused')} · --minter 0x.. · --primary-payee 0x..
|
|
6050
6190
|
${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.')}
|
|
6051
6191
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} ${dim('(needs --for 0x..)')} ${g('--bootstrap-factory')} ${dim('(private chains/sandboxes only)')}
|
|
@@ -6112,8 +6252,8 @@ const COMMAND_HELP = {
|
|
|
6112
6252
|
Select by label, address, bool). Auth is per the schema leg (Artist=owner · TokenOwner — delegate.xyz honored · Address).
|
|
6113
6253
|
A change ${bold('re-addresses renders')} → the still becomes a placeholder until you re-render (${g('abx render <addr> <id>')} or the runner).
|
|
6114
6254
|
${g('<tokenId> "-"')} CONTRACT scope (owner-only raw setter; schema-less keys only) — the write path of well-known params
|
|
6115
|
-
like ${g('
|
|
6116
|
-
|
|
6255
|
+
like ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
|
|
6256
|
+
A contract-scope param applies to every token, and enumerates on-chain like any other.
|
|
6117
6257
|
--file <path> read the value from a file (String / Bytes payloads)
|
|
6118
6258
|
${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
|
|
6119
6259
|
the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
|
|
@@ -6150,7 +6290,8 @@ const COMMAND_HELP = {
|
|
|
6150
6290
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')}`,
|
|
6151
6291
|
state: `
|
|
6152
6292
|
${bold('abx state')} <address> ${dim('— read-only, on-chain operational snapshot (no tx, no local index).')}
|
|
6153
|
-
Shows owner · supply (minted / max, nextTokenId) · paused · minter · primary payee · royalty · renderer
|
|
6293
|
+
Shows owner · supply (minted / max, nextTokenId) · paused · minter · primary payee · royalty · renderer
|
|
6294
|
+
(+ the 721C transfer validator, for a collection that enrolled at deploy — plain ERC-721s show nothing new).
|
|
6154
6295
|
Series-only fields are shown for a Series; a 1/1 shows just supply + royalty + renderer.
|
|
6155
6296
|
Handy before/after owner ops (mint · pause/unpause · set-minter · set-primary-payee).
|
|
6156
6297
|
${dim('state = what the CHAIN says. For who is SERVING it and how fresh that is, see `abx status`.')}`,
|
|
@@ -6205,6 +6346,14 @@ const COMMAND_HELP = {
|
|
|
6205
6346
|
deploy to Vercel). ${dim('Offer this after a sale is live (configure → set-minter → set-primary-payee → unpause).')}`,
|
|
6206
6347
|
'set-royalty': `
|
|
6207
6348
|
${bold('abx set-royalty')} <address> --bps <0-10000> [--receiver 0x..] ${dim('— change the royalty. Sends a tx.')}`,
|
|
6349
|
+
'set-transfer-validator': `
|
|
6350
|
+
${bold('abx set-transfer-validator')} <address> <0x..|none|recommended> ${dim('— manage an ERC-721C collection\'s transfer validator. Owner-only. Sends a tx.')}
|
|
6351
|
+
<0x..> re-point enforcement at that validator (must be a DEPLOYED contract — checked before any gas)
|
|
6352
|
+
${g('none')} suspend enforcement (validator → address(0); the collection STAYS enrolled)
|
|
6353
|
+
${g('recommended')} the chain's recommended validator (OpenSea's, for creator-fee enforcement)
|
|
6354
|
+
${dim('Only for a collection that enrolled AT DEPLOY (--721c on the deploy commands) — a plain ERC-721 is refused')}
|
|
6355
|
+
${dim('up front: enrollment is a deploy-time decision and can never be added (or fully removed) later.')}
|
|
6356
|
+
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} · ${g('--dry-run')} preview (encodes + shows the tx, sends nothing)`,
|
|
6208
6357
|
attach: `
|
|
6209
6358
|
${bold('abx attach')} <address> <key> <ipfs://… | ar://… | https://…> ${dim('— attach a named file to a token. Sends a tx.')}
|
|
6210
6359
|
${dim('The data-plane verb: the file joins the token\'s served')} ${bold('artifacts')} ${dim('manifest as {key, mimeType, uri}.')}
|
|
@@ -6215,7 +6364,8 @@ const COMMAND_HELP = {
|
|
|
6215
6364
|
${g('--dry-run')} preview the tx, send nothing · signing: ${g('--send')} (default, needs a hot key) · ${g('--sign')} (browser wallet, no key) · ${g('--unsigned')}
|
|
6216
6365
|
${dim('Need the URI first? Upload the file with')} ${g('abx storage upload <path> --backend arweave|ipfs')} ${dim('— it prints the locator to pass here.')}
|
|
6217
6366
|
${dim('mimeType is declared from the URL EXTENSION (…/master.tiff → image/tiff) — give the file a real extension.')}
|
|
6218
|
-
${dim('Surfaces in a resolver\'s artifacts listing (/data/<key> fetches it); a bare on-chain tokenURI shows reserved fields only
|
|
6367
|
+
${dim('Surfaces in a resolver\'s artifacts listing (/data/<key> fetches it); a bare on-chain tokenURI shows reserved fields + abx_params only —')}
|
|
6368
|
+
${dim('configured params DO appear on-chain (as')} ${bold('abx_params')}${dim('); ATTACHMENTS are the surface that needs a resolver.')}
|
|
6219
6369
|
${dim('The image/animation are just reserved members of the same manifest. `artifacts` itself is COMPUTED — not settable.')}`,
|
|
6220
6370
|
'set-field': `
|
|
6221
6371
|
${bold('abx set-field')} <address> --field <name> ${dim('— set an on-chain metadata field (the low-level primitive). Sends a tx.')}
|
|
@@ -6223,7 +6373,7 @@ const COMMAND_HELP = {
|
|
|
6223
6373
|
--field <name> a reserved display field (image · description · animation_url · external_url · attributes ·
|
|
6224
6374
|
background_color · …), a collection authorship/rights field with ${g('--collection')}
|
|
6225
6375
|
(artist · display_notes · artist_links · license), OR any custom key (which becomes an
|
|
6226
|
-
${bold('artifacts')} entry). NOT ${g('artifacts')}/${g('abx_provenance')} (computed).
|
|
6376
|
+
${bold('artifacts')} entry). NOT ${g('artifacts')}/${g('abx_params')}/${g('abx_provenance')} (computed).
|
|
6227
6377
|
--text "<s>" store literal UTF-8 ON-CHAIN (representation defaults ${g('inline')}) — NOT for an off-chain URL (use ${bold('attach')})
|
|
6228
6378
|
--file <path> store a FILE on-chain in SSTORE2 chunks behind the shared reader (any size; multi-chunk)
|
|
6229
6379
|
${g('--compress')} none|fastlz|gzip for --file (default none): ${g('fastlz')} = on-chain decode, stays renderable, cheaper storage;
|
|
@@ -6285,7 +6435,7 @@ const COMMAND_HELP = {
|
|
|
6285
6435
|
storage: `
|
|
6286
6436
|
${bold('abx storage')} <show|upload|balance|topup|backup-key> ${dim('— inspect / operate byte custody. Mostly read-only.')}
|
|
6287
6437
|
${g('show')} the resolved backend (fs | cloud | ipfs | arweave) + where each value came from
|
|
6288
|
-
${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run]
|
|
6438
|
+
${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run] [${g('--json')}]
|
|
6289
6439
|
${g('balance')} · ${g('topup')} --usd <n> Turbo (arweave) upload credits · ${g('backup-key')} --out <path> copy the managed key`,
|
|
6290
6440
|
demo: `
|
|
6291
6441
|
${bold('abx demo')} <${dim('no args')}> ${dim('— deploy a throwaway 1/1 to the testnet, index it, and serve it — a guided first run. Sends a tx.')}
|
|
@@ -6437,6 +6587,7 @@ function help() {
|
|
|
6437
6587
|
${g('abx set-contract-uri')} <addr> re-point the collection URI --uri <url>
|
|
6438
6588
|
${g('abx migrate')} <addr> move off-chain state to a NEW resolver (verifies parity; no cutover) --from <old> --to <new>
|
|
6439
6589
|
${g('abx set-royalty')} <addr> change royalty --bps <0-10000> [--receiver 0x..]
|
|
6590
|
+
${g('abx set-transfer-validator')} <addr> <0x..|none|recommended> manage an ERC-721C collection's validator (enrolled-at-deploy only)
|
|
6440
6591
|
${g('abx attach')} <addr> <key> <ipfs://…|ar://…|https://…> attach a named file → the token's ${bold('artifacts')} manifest (data plane)
|
|
6441
6592
|
${g('abx set-field')} <addr> set an on-chain metadata field (low-level) --field <name> (--text "…" | --value 0x..) [--collection]
|
|
6442
6593
|
${g('abx lock-field')} <addr> freeze a field forever --field <name> [--collection]
|
|
@@ -6470,6 +6621,48 @@ function help() {
|
|
|
6470
6621
|
${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('.')}
|
|
6471
6622
|
`);
|
|
6472
6623
|
}
|
|
6624
|
+
/**
|
|
6625
|
+
* Resolve `--721c` into the InitParams `transferValidator`. Absent → `zeroAddress`: a plain
|
|
6626
|
+
* ERC-721, byte-for-byte the pre-721C deploy — no prompts, no output, no mention of 721C.
|
|
6627
|
+
* `--721c` / `--721c recommended` → the per-chain recommended validator (refused, naming the
|
|
6628
|
+
* chains that have one, when the manifest has no entry); `--721c 0x…` → checksum-validated AND
|
|
6629
|
+
* pre-checked for code on this chain (the factory would revert `InvalidTransferValidator()` —
|
|
6630
|
+
* surface it before any upload/staging/gas). When enrolling, prints the one plain statement of
|
|
6631
|
+
* what enforcement means — once, adapted to the chosen validator.
|
|
6632
|
+
*/
|
|
6633
|
+
async function resolveTransferValidatorFlag(flags, publicClient, dryRun) {
|
|
6634
|
+
const raw = flags['721c'];
|
|
6635
|
+
if (raw === undefined)
|
|
6636
|
+
return zeroAddress;
|
|
6637
|
+
const chainId = resolveChain(CHAIN).id;
|
|
6638
|
+
const validator = parseTransferValidatorValue(raw, { chainId, chainLabel: CHAIN });
|
|
6639
|
+
// Has-code precheck — for the recommended constant too (presence on THIS chain is the fact that
|
|
6640
|
+
// matters; a sandbox/private chain won't have it). A dry run tolerates an unreachable RPC (it
|
|
6641
|
+
// sends nothing); a real deploy refuses to enroll blind.
|
|
6642
|
+
let code;
|
|
6643
|
+
try {
|
|
6644
|
+
code = await publicClient.getCode({ address: validator });
|
|
6645
|
+
}
|
|
6646
|
+
catch (err) {
|
|
6647
|
+
if (!dryRun)
|
|
6648
|
+
throw new Error(`--721c: couldn't verify the validator has code at ${validator} (${err.message}) — refusing to enroll blind; retry when the RPC answers.`);
|
|
6649
|
+
code = null; // offline preview — note it below
|
|
6650
|
+
}
|
|
6651
|
+
if (code === null) {
|
|
6652
|
+
info(dim(`--721c: RPC unreachable, so the validator code check is deferred — a real deploy verifies ${validator} has code first.`));
|
|
6653
|
+
}
|
|
6654
|
+
else if (!code || code === '0x') {
|
|
6655
|
+
const rec = resolveRecommendedTransferValidator(chainId);
|
|
6656
|
+
throw new Error(`--721c: no contract code at ${validator} on ${CHAIN} — the deploy would revert InvalidTransferValidator(). ` +
|
|
6657
|
+
`A transfer validator must be a DEPLOYED contract on this chain` +
|
|
6658
|
+
(rec && rec !== validator ? ` (the known-good one: --721c recommended → ${rec})` : '') + `.`);
|
|
6659
|
+
}
|
|
6660
|
+
// The one plain statement — printed once, only when enrolling. Never appears un-enrolled.
|
|
6661
|
+
info(validator === resolveRecommendedTransferValidator(chainId)
|
|
6662
|
+
? `ERC-721C enforcement: only owner-initiated transfers and OpenSea-authorized sales will transfer; other marketplaces/operators are blocked. Manage with \`abx set-transfer-validator\`.`
|
|
6663
|
+
: `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\`.`);
|
|
6664
|
+
return validator;
|
|
6665
|
+
}
|
|
6473
6666
|
/** Validate an explicit --salt (a 32-byte hex). Undefined when absent. */
|
|
6474
6667
|
function parseSaltFlag(raw) {
|
|
6475
6668
|
if (!raw || raw === 'true')
|