@artblocks/abx-cli 0.1.0-alpha.15 → 0.1.0-alpha.17
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 +1476 -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/jsonout.d.ts +37 -0
- package/dist/jsonout.d.ts.map +1 -0
- package/dist/jsonout.js +68 -0
- package/dist/jsonout.js.map +1 -0
- package/dist/main.js +791 -42
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +42 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +238 -53
- package/dist/ownerops.js.map +1 -1
- package/dist/resume.d.ts +96 -0
- package/dist/resume.d.ts.map +1 -0
- package/dist/resume.js +95 -0
- package/dist/resume.js.map +1 -0
- 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/dist/served.d.ts +46 -0
- package/dist/served.d.ts.map +1 -0
- package/dist/served.js +65 -0
- package/dist/served.js.map +1 -0
- package/package.json +7 -6
- package/skill/SKILL.md +7 -4
- package/skill/reference/code-projects.md +8 -6
- package/skill/reference/operating.md +2 -0
- package/skill/reference/setup.md +2 -1
- package/skill/reference/troubleshooting.md +13 -0
package/dist/main.js
CHANGED
|
@@ -17,12 +17,14 @@
|
|
|
17
17
|
* abx preview run a code project on localhost while it's still being made (no chain)
|
|
18
18
|
* abx deploy-code deploy a code project (SeriesCode): --script <file> (on-chain template)
|
|
19
19
|
* or --code-dir <dir> (build directory → ipfs/arweave `code` field)
|
|
20
|
+
* --resume <addr>: finish one whose setup tx failed (sends only what's missing)
|
|
20
21
|
* abx add <address> register + index a project (--remote <name|url>: on a remote resolver, not this machine)
|
|
21
22
|
* abx remote [<name|url>] inspect a remote service (descriptor · chains · managed rendering · your projects there)
|
|
22
23
|
* abx index [<address>] re-index a project from chain (replay; --remote to nudge a remote resolver)
|
|
23
24
|
* abx verify <address> re-hash served bytes vs the on-chain commitment (no server)
|
|
24
25
|
* abx tokenuri <address> read tokenURI(id) from the contract + decode · contracturi: the ERC-7572 collection JSON
|
|
25
26
|
* (the contract holds the URL — never hand-build a resolver path)
|
|
27
|
+
* abx tokens <address> every token's owner + seed + params, from chain alone (--json for the machine path)
|
|
26
28
|
* abx configure-param <addr> <id> <key> <value> set a governed PostParam (typed encode; any lane)
|
|
27
29
|
* abx set-param-hooks <addr> wire/clear a SeriesCode's configure/augment/transfer param hooks
|
|
28
30
|
* abx render <addr> [id…] effect-runner repair lane: render missing stills/traits
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
* abx migrate <addr> move a contract's off-chain state to another resolver (--from/--to)
|
|
35
37
|
* abx set-token-uri <addr> · set-contract-uri · set-royalty · set-field · lock-field
|
|
36
38
|
* abx storage show show byte custody (fs | cloud | ipfs | arweave); stateless — choose per command with flags
|
|
39
|
+
* abx storage status <loc> is a locator RETRIEVABLE yet, not just accepted? (Arweave propagation is minutes)
|
|
37
40
|
* abx storage balance|topup Turbo (arweave) upload credits — view / buy by card (credits attach to the signing identity)
|
|
38
41
|
* abx storage backup-key copy the managed Turbo/Arweave key (holds credits) to a safe path
|
|
39
42
|
* abx status list indexed projects + node info
|
|
@@ -55,14 +58,18 @@ import { effectsArtifact, resolverArtifact } from './provision.js';
|
|
|
55
58
|
import { mintPageArtifact, PUBLIC_RPC } from './mintpage.js';
|
|
56
59
|
import { formatEther, toHex, zeroAddress } from 'viem';
|
|
57
60
|
import { encodeFunctionData } from 'viem';
|
|
58
|
-
import { DEP_RESOLUTION, deploySeedSource, deploySeriesCodeFactory, prepareCodeSetup, prepareDeploySeriesCode, resolveGenerator, resolveSeedSource, resolveSeriesCodeFactory, seriesCodeAbi, readParamSchema, seriesCodeFactoryAbi, } from '@artblocks/abx-sdk';
|
|
61
|
+
import { AUTH_OPTIONS, DEP_RESOLUTION, PARAM_TYPES, deploySeedSource, deploySeriesCodeFactory, listTokens, prepareCodeSetup, prepareDeploySeriesCode, resolveGenerator, resolveSeedSource, resolveSeriesCodeFactory, seriesCodeAbi, readParamSchema, seriesCodeFactoryAbi, } from '@artblocks/abx-sdk';
|
|
59
62
|
import { checkRegistryDeps, dependencySetupCalls, parseDepFlag, resolveDepRegistryPointer } from './deps.js';
|
|
60
63
|
import { expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport, readLegacyParamsKeys, readParamSchemaKeys, readSetParamKeys, } from './onchain-uri.js';
|
|
61
|
-
import { parseFlags, unknownFlags } from './flags.js';
|
|
64
|
+
import { parseFlags, positionalArgs, unknownFlags } from './flags.js';
|
|
62
65
|
import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, readSkillVersion, SKILL_DIR_NAME, } from './update-check.js';
|
|
63
66
|
import { analyzeScript, recommendLane } from './inspect.js';
|
|
64
67
|
import { previewConfigFromFlags, previewDepTags, parsePreviewParams, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
65
68
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
69
|
+
import { copyRendererScaffold } from './scaffold.js';
|
|
70
|
+
import { jsonSafe, withJson } from './jsonout.js';
|
|
71
|
+
import { fetchServedTokenUri, prettyBody, servedOk } from './served.js';
|
|
72
|
+
import { planResume } from './resume.js';
|
|
66
73
|
import { declinesSkillInstall } from './prompt.js';
|
|
67
74
|
import { pinGas, waitForCodeAt } from './gas.js';
|
|
68
75
|
import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
|
|
@@ -77,7 +84,7 @@ import { uploadAndLocate } from './upload.js';
|
|
|
77
84
|
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
85
|
import { SelfHostIndexer, SqliteStore } from '@artblocks/abx-indexer';
|
|
79
86
|
import { artContentHash, currentRenderArtifact, generateArt, resolveBaseUrl, startChainWatcher, startTokenApiServer, verifyProject, watchIntervalMs, DEFAULT_PORT, } from '@artblocks/abx-token-api';
|
|
80
|
-
import { ARWEAVE_FREE_UPLOAD_LIMIT, arweaveAddress, arweaveFunding, contentTypeFromPath, hashContent, resolveBackend, turboBalanceForAddress, turboUploadCostUsd, turboUploadWinc } from '@artblocks/abx-storage';
|
|
87
|
+
import { ARWEAVE_FREE_UPLOAD_LIMIT, arweaveAddress, arweaveFunding, contentTypeFromPath, hashContent, locatorStatus, resolveBackend, turboBalanceForAddress, turboUploadCostUsd, turboUploadWinc } from '@artblocks/abx-storage';
|
|
81
88
|
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
89
|
import { openWalletSession, signTx } from './signer.js';
|
|
83
90
|
import { describeRemoteError, listConfiguredRemotes, misnamedRemoteVars, requireRemoteToken, resolveRemote, serviceClient, tokenSourceLabel } from './remote.js';
|
|
@@ -161,9 +168,16 @@ async function maybeNotifyUpdate(flags) {
|
|
|
161
168
|
}
|
|
162
169
|
const latest = await checkForCliUpdate(current);
|
|
163
170
|
if (latest) {
|
|
171
|
+
// The notes pointer must RESOLVE. This printed github.com/ArtBlocks/abx/releases, which 404s
|
|
172
|
+
// for everyone outside the org (the repo is private) — so a tester reconstructed the diff by
|
|
173
|
+
// running the same dry run on both versions, which is how they discovered the canonical
|
|
174
|
+
// singletons had moved and then had no way to tell whether that needed them to act. The
|
|
175
|
+
// packaged CHANGELOG.md ships with every install (see package.json `files`) and carries the
|
|
176
|
+
// real per-release notes, so it's readable offline and always matches the version you have.
|
|
164
177
|
console.error(`\n ${c.orange}⚠${c.reset} update available: ${bold('abx')} ${dim(current)} → ${g(latest)}\n` +
|
|
165
178
|
` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx @artblocks/abx-cli@latest <command>')}\n` +
|
|
166
|
-
` release notes:
|
|
179
|
+
` release notes: ${g('abx changelog')} ${dim('(ships with the CLI) · all versions: https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions')}\n` +
|
|
180
|
+
` ${dim('silence: ABX_NO_UPDATE_CHECK=1')}\n`);
|
|
167
181
|
}
|
|
168
182
|
}
|
|
169
183
|
catch {
|
|
@@ -212,13 +226,15 @@ async function main() {
|
|
|
212
226
|
case 'inspect': return cmdInspect(rest[0], flags);
|
|
213
227
|
case 'preview': return cmdPreview(flags);
|
|
214
228
|
case 'scaffold-renderer': return cmdScaffoldRenderer(rest, flags);
|
|
229
|
+
case 'changelog': return cmdChangelog(flags);
|
|
215
230
|
case 'predict': return cmdPredict(flags);
|
|
216
231
|
case 'add': return cmdAdd(rest[0], flags);
|
|
217
232
|
case 'index': return cmdIndex(rest[0], flags);
|
|
218
233
|
case 'verify': return cmdVerify(rest[0], flags);
|
|
219
234
|
case 'render': return cmdRender(rest[0], rest.slice(1), flags);
|
|
220
235
|
case 'effects': return cmdEffects(flags);
|
|
221
|
-
case 'tokenuri': return cmdTokenUri(rest[0], flags);
|
|
236
|
+
case 'tokenuri': return cmdTokenUri(rest[0], flags, rest.slice(1));
|
|
237
|
+
case 'tokens': return cmdTokens(rest[0], flags);
|
|
222
238
|
case 'contracturi': return cmdContractUri(rest[0], flags);
|
|
223
239
|
case 'serve': return cmdServe(flags);
|
|
224
240
|
// owner operations — write + sign (hot/wallet/cold lane), then re-index
|
|
@@ -1152,6 +1168,11 @@ async function remoteLocators(address, stored, flags) {
|
|
|
1152
1168
|
}
|
|
1153
1169
|
// ── deploy (+ optionally serve) ──────────────────────────────────────────────
|
|
1154
1170
|
async function cmdDeploy(flags, serveAfter) {
|
|
1171
|
+
// `--json`: the deployed ADDRESS is the value a program came for (B19). Narration goes to stderr,
|
|
1172
|
+
// so `ADDR=$(abx deploy … --json | jq -r .address)` works without parsing a success banner.
|
|
1173
|
+
return withJson(flags, async (emit) => cmdDeployBody(flags, serveAfter, emit));
|
|
1174
|
+
}
|
|
1175
|
+
async function cmdDeployBody(flags, serveAfter, emit) {
|
|
1155
1176
|
refuseStrayFlags(flags, DEPLOY_FLAGS, 'deploy');
|
|
1156
1177
|
warnSignWithoutFor(flags);
|
|
1157
1178
|
const dimension = resolveDimension(flags.type ?? '1of1');
|
|
@@ -1202,6 +1223,8 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1202
1223
|
` Nothing was deployed. Stop that one (Ctrl-C), or run this on another port: \`abx demo --port ${wanted + 1}\`.`);
|
|
1203
1224
|
}
|
|
1204
1225
|
}
|
|
1226
|
+
// (demo keeps its own message: "Nothing was deployed" is the load-bearing part here, and it must
|
|
1227
|
+
// be true — this check runs before anything irreversible. assertPortFree covers serve/preview.)
|
|
1205
1228
|
const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
|
|
1206
1229
|
// A keyless preview needs `--for` (the address is a pure function of factory+salt+deployer). Check
|
|
1207
1230
|
// it HERE, before the trust-anchor/content/plan steps print — hitting this after a wall of output
|
|
@@ -1516,6 +1539,10 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1516
1539
|
// values — nothing is added, inferred, or substituted between here and the on-chain tx.
|
|
1517
1540
|
info(dim('the values above are exactly what a real deploy writes — nothing else is added.'));
|
|
1518
1541
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
|
|
1542
|
+
// A dry run DOES know the address (it is a pure function of factory+salt), so --json reports it
|
|
1543
|
+
// with `sent: false` rather than nothing. Note `saltPinned`: without --salt a real re-run
|
|
1544
|
+
// reserves a fresh salt and lands elsewhere, so a caller must not treat this as reserved.
|
|
1545
|
+
emit(jsonSafe({ command: 'deploy', dryRun: true, sent: false, address: predicted, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol }));
|
|
1519
1546
|
return;
|
|
1520
1547
|
}
|
|
1521
1548
|
// opt-in --confirm: a final y/N before the real send (no-op without --confirm; never blocks scripts)
|
|
@@ -1629,6 +1656,9 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1629
1656
|
blockNumber = result.blockNumber;
|
|
1630
1657
|
ok(`deployed ${clone}`);
|
|
1631
1658
|
}
|
|
1659
|
+
// Emitted the moment the address is known, and again below once minting is settled — so a crash in
|
|
1660
|
+
// the indexing steps that follow still yields the address of a contract that really does exist.
|
|
1661
|
+
emit(jsonSafe({ command: 'deploy', address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol }));
|
|
1632
1662
|
step(serveAfter ? 'What the chain knows now' : 'Index it — replay the event spine from chain');
|
|
1633
1663
|
const indexer = new SelfHostIndexer();
|
|
1634
1664
|
// Off-chain traits ride in the registration (on-chain ones are already in the contract fields).
|
|
@@ -1764,6 +1794,9 @@ const tokenFieldOf = (tokenId, f) => ({
|
|
|
1764
1794
|
value: f.value,
|
|
1765
1795
|
});
|
|
1766
1796
|
async function cmdDeploySeries(flags) {
|
|
1797
|
+
return withJson(flags, async (emit) => cmdDeploySeriesBody(flags, emit));
|
|
1798
|
+
}
|
|
1799
|
+
async function cmdDeploySeriesBody(flags, emit) {
|
|
1767
1800
|
refuseStrayFlags(flags, DEPLOY_SERIES_FLAGS, 'deploy-series');
|
|
1768
1801
|
warnSignWithoutFor(flags);
|
|
1769
1802
|
const name = flags.name ?? 'ABX Series';
|
|
@@ -2104,6 +2137,7 @@ async function cmdDeploySeries(flags) {
|
|
|
2104
2137
|
console.log(` ${bold(deploySeriesCommandLine(flags, salt))}`);
|
|
2105
2138
|
}
|
|
2106
2139
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored.')}\n`);
|
|
2140
|
+
emit(jsonSafe({ command: 'deploy-series', dryRun: true, sent: false, address: predicted, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt, saltPinned: !!explicitSalt, name, symbol }));
|
|
2107
2141
|
return;
|
|
2108
2142
|
}
|
|
2109
2143
|
let clone;
|
|
@@ -2198,6 +2232,7 @@ async function cmdDeploySeries(flags) {
|
|
|
2198
2232
|
blockNumber = result.blockNumber;
|
|
2199
2233
|
ok(`deployed ${clone}`);
|
|
2200
2234
|
}
|
|
2235
|
+
emit(jsonSafe({ command: 'deploy-series', address: clone, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, deployBlock: blockNumber, name, symbol }));
|
|
2201
2236
|
step('Index it — replay the event spine from chain');
|
|
2202
2237
|
const indexer = new SelfHostIndexer();
|
|
2203
2238
|
const baseReg = {
|
|
@@ -2881,6 +2916,21 @@ async function walkthroughReadBack(state, baseUrl, onChainUri) {
|
|
|
2881
2916
|
}
|
|
2882
2917
|
console.log(` ${dim('prove the bytes match the chain:')} ${bold(`abx verify ${state.address}`)}`);
|
|
2883
2918
|
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Refuse a port that's already bound, with a formatted one-liner naming the port and the fix.
|
|
2921
|
+
*
|
|
2922
|
+
* Without this, `listen()` has no `'error'` handler and EADDRINUSE reaches Node's default handler:
|
|
2923
|
+
* the creator gets a raw stack trace through `node:net` and our own `dist/` paths, which reads as a
|
|
2924
|
+
* crash inside abx rather than "something else is on this port" — and it's the one unformatted error
|
|
2925
|
+
* surface in a CLI where every other error is formatted. `preview`'s default port colliding with a
|
|
2926
|
+
* studio left running in another terminal was reported as especially hard to diagnose.
|
|
2927
|
+
*/
|
|
2928
|
+
async function assertPortFree(port, cmd) {
|
|
2929
|
+
if (!(await portInUse(port)))
|
|
2930
|
+
return;
|
|
2931
|
+
throw new Error(`port ${port} is already in use — most likely an \`abx ${cmd}\`/\`abx serve\`/\`abx preview\` still running in another terminal.\n` +
|
|
2932
|
+
` Stop that one (Ctrl-C), or run this on a different port: \`abx ${cmd} --port ${port + 1}\`.`);
|
|
2933
|
+
}
|
|
2884
2934
|
/** Is a TCP port already bound on localhost? Used to preflight a serve BEFORE spending a tx. */
|
|
2885
2935
|
async function portInUse(port) {
|
|
2886
2936
|
const { createServer } = await import('node:net');
|
|
@@ -2957,7 +3007,11 @@ async function cmdPreview(flags) {
|
|
|
2957
3007
|
if (cfg.deps.some((d) => d.display.startsWith('0x'))) {
|
|
2958
3008
|
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.');
|
|
2959
3009
|
}
|
|
2960
|
-
|
|
3010
|
+
// `--shoot` takes an ephemeral port (0), so only the studio lane can collide.
|
|
3011
|
+
const previewPort = shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT);
|
|
3012
|
+
if (previewPort !== 0)
|
|
3013
|
+
await assertPortFree(previewPort, 'preview');
|
|
3014
|
+
const server = await startPreviewServer(cfg, previewPort);
|
|
2961
3015
|
if (shootDir) {
|
|
2962
3016
|
step(`Render ${count} seeds headlessly`);
|
|
2963
3017
|
try {
|
|
@@ -3115,12 +3169,15 @@ const DEPLOY_CODE_FLAGS = new Set([
|
|
|
3115
3169
|
'sign', 'unsigned', 'for', 'salt', 'factory', 'bootstrap-factory', 'sign-url-file',
|
|
3116
3170
|
// preview / confirm
|
|
3117
3171
|
'dry-run', 'confirm', 'yes',
|
|
3172
|
+
'json', // B19: the deployed ADDRESS as data, narration to stderr
|
|
3173
|
+
'resume', // B15: finish an EXISTING contract whose setup tx never landed (no deploy)
|
|
3118
3174
|
]);
|
|
3119
3175
|
// Storage + signing + preview flags shared by every deploy path (mirrors storageOverrides() + the lanes).
|
|
3120
3176
|
const SHARED_DEPLOY_FLAGS = [
|
|
3121
3177
|
'backend', 'endpoint', 'bucket', 'region', 'prefix', 'public-base', 'gateway', 'mode', 'api-url', 'upload-url', 'provider', 'storage-signer',
|
|
3122
3178
|
'sign', 'unsigned', 'for', 'sign-url-file', 'dry-run', 'confirm', 'yes', 'salt', 'factory', 'bootstrap-factory', 'port', 'renderer', 'public-base-url',
|
|
3123
3179
|
'721c', // opt-in ERC-721C enrollment (recommended | 0x…) — every deploy path takes it
|
|
3180
|
+
'json', // B19: the deployed ADDRESS as data, narration to stderr — every deploy path takes it
|
|
3124
3181
|
];
|
|
3125
3182
|
// A 1/1 `abx deploy` accepts these (see `abx help deploy`). Anything else warns (typo'd/unsupported).
|
|
3126
3183
|
const DEPLOY_FLAGS = new Set([
|
|
@@ -3196,6 +3253,9 @@ function warnSignWithoutFor(flags) {
|
|
|
3196
3253
|
}
|
|
3197
3254
|
}
|
|
3198
3255
|
async function cmdDeployCode(flags) {
|
|
3256
|
+
return withJson(flags, async (emit) => cmdDeployCodeBody(flags, emit));
|
|
3257
|
+
}
|
|
3258
|
+
async function cmdDeployCodeBody(flags, emit) {
|
|
3199
3259
|
const usage = 'abx deploy-code (--script <file> | --code-dir <dir>) --name "Title" --symbol SYM ' +
|
|
3200
3260
|
'(--public-base-url https://your.resolver.domain | --onchain-uri) ' +
|
|
3201
3261
|
'[--description "<s>"] [--external-url <url>] [--image-base <url> | --image-renderer 0x..] [--attributes-renderer 0x..] ' +
|
|
@@ -3690,7 +3750,11 @@ async function cmdDeployCode(flags) {
|
|
|
3690
3750
|
}
|
|
3691
3751
|
// --dry-run: emit the full plan (the on-chain values the skill formats into its confirm table),
|
|
3692
3752
|
// then stop — no upload, no factory/seed deploy, no send.
|
|
3693
|
-
|
|
3753
|
+
//
|
|
3754
|
+
// `--resume` skips this preview entirely and previews its OWN plan further down. Without the guard,
|
|
3755
|
+
// `--resume 0xLive --dry-run` printed a fresh-deploy plan complete with a newly-reserved salt and a
|
|
3756
|
+
// different predicted address — a preview of the exact thing the flag exists not to do.
|
|
3757
|
+
if (dryRun && flags.resume === undefined) {
|
|
3694
3758
|
// The deployer is needed ONLY to derive the CREATE2 salt → the deterministic address (and the
|
|
3695
3759
|
// pinned-salt re-run line). Everything else in the plan — content, deps, chain-complete
|
|
3696
3760
|
// expectation, tx count, cost — is deployer-independent, so a wallet-less creator (the common
|
|
@@ -3800,7 +3864,13 @@ async function cmdDeployCode(flags) {
|
|
|
3800
3864
|
catch { /* gas pricing unavailable — cost guidance is a bonus, skip silently */ }
|
|
3801
3865
|
// ── Surfaces — every marketplace-facing dimension, resolved NOW (none is backfillable) ──────
|
|
3802
3866
|
const renderHome = process.env.ABX_STORAGE_BACKEND || 'fs'; // empty string counts as unset (=fs)
|
|
3803
|
-
|
|
3867
|
+
// A surface is BROKEN only when it resolves to nothing a marketplace can see. A dropped param is
|
|
3868
|
+
// NOT that: the token still renders, the param just takes its default. Folding it in here made
|
|
3869
|
+
// the block contradict itself two lines apart — "thumbnail: ON-CHAIN ✓ / traits: on-chain ✓"
|
|
3870
|
+
// followed by "one or more surfaces resolve to NOTHING", re-recommending the very flags that
|
|
3871
|
+
// were already set. The alarm now covers only the surfaces it can honestly speak for, and names
|
|
3872
|
+
// only the remedies for what is actually broken.
|
|
3873
|
+
const anySurfaceBroken = imageOrphaned || traitsBroken;
|
|
3804
3874
|
step('Surfaces — what marketplaces will see (all DEPLOY-TIME; not backfillable)');
|
|
3805
3875
|
(imageOrphaned ? warn : info)(`thumbnail: ${imageDisposition}`);
|
|
3806
3876
|
// Render-mode / render-home guidance is for the RENDERED-still lanes only. The in-chain
|
|
@@ -3818,12 +3888,25 @@ async function cmdDeployCode(flags) {
|
|
|
3818
3888
|
(traitsBroken ? warn : info)(`traits: ${traitsDisposition}`);
|
|
3819
3889
|
const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
|
|
3820
3890
|
((undeclaredParams.length || paramsNudge ? warn : info))(`postparams: ${paramsDisposition}`);
|
|
3821
|
-
if (anySurfaceBroken)
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3891
|
+
if (anySurfaceBroken) {
|
|
3892
|
+
const remedies = [
|
|
3893
|
+
imageOrphaned && `thumbnail ⇒ ${bold('--image-renderer <Solidity SVG>')} / ${bold('--image-base <public bucket>')} / a resolver`,
|
|
3894
|
+
traitsBroken && `traits ⇒ ${bold('--attributes-renderer')} or a resolver`,
|
|
3895
|
+
].filter(Boolean).join(' · ');
|
|
3896
|
+
const which = imageOrphaned && traitsBroken ? 'the thumbnail and traits surfaces resolve' : `the ${imageOrphaned ? 'thumbnail' : 'traits'} surface resolves`;
|
|
3897
|
+
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}.`);
|
|
3898
|
+
}
|
|
3899
|
+
// A dropped param is its own, milder problem: the piece renders, that input just takes its
|
|
3900
|
+
// default. Kept separate from the broken-surface alarm above (see the note there).
|
|
3901
|
+
if (undeclaredParams.length)
|
|
3902
|
+
info(` ${dim('dropped params render with their defaults — declare them to make them real:')} ${bold('--schema <key>:<Type>:<Auth>')}`);
|
|
3903
|
+
// The runner/verify line is for lanes that need an off-chain STILL. An on-chain image renderer has
|
|
3904
|
+
// no still to render, host, or refresh — whether or not there's also a program driving
|
|
3905
|
+
// animation_url — so pointing at a runner and a bucket backend there is simply wrong.
|
|
3906
|
+
if (hasImageRenderer) {
|
|
3907
|
+
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. ` +
|
|
3908
|
+
`verify from chain: ${bold('abx tokenuri ' + (predicted ?? '<address>'))} ${dim('(decodes name + on-chain SVG + traits)')}`);
|
|
3909
|
+
}
|
|
3827
3910
|
else {
|
|
3828
3911
|
const remoteRender = !(onChainUri && hasImageBase);
|
|
3829
3912
|
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>'))}`);
|
|
@@ -3844,6 +3927,9 @@ async function cmdDeployCode(flags) {
|
|
|
3844
3927
|
info(`pass ${bold('--for 0x..')} to see the exact deterministic address + a pinned-salt re-run command.`);
|
|
3845
3928
|
}
|
|
3846
3929
|
console.log(`\n ${g('dry run')} ${dim('— nothing sent, no bytes stored. Re-run without --dry-run to deploy.')}\n`);
|
|
3930
|
+
// `address` is null in the wallet lane with no --for: the signer decides the salt, so no address
|
|
3931
|
+
// exists yet. Reporting null is the honest answer; inventing one would be a wrong reservation.
|
|
3932
|
+
emit(jsonSafe({ command: 'deploy-code', dryRun: true, sent: false, address: predicted ?? null, chain: CHAIN, chainId: resolveChain(CHAIN).id, factory, salt: salt ?? null, saltPinned: !!parseSaltFlag(flags.salt), name, symbol, onChainUri }));
|
|
3847
3933
|
return;
|
|
3848
3934
|
}
|
|
3849
3935
|
// Real deploy: perform the deferred directory upload now (past the confirm gate).
|
|
@@ -3892,12 +3978,39 @@ async function cmdDeployCode(flags) {
|
|
|
3892
3978
|
tokenFields: [],
|
|
3893
3979
|
contractFields,
|
|
3894
3980
|
});
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3981
|
+
// The setup legs, GROUPED. The normal deploy flattens them; `--resume` diffs them against chain
|
|
3982
|
+
// state and sends only what is missing (see resume.ts). One builder for both, so a resume can never
|
|
3983
|
+
// drift from what a fresh deploy would have written — a second implementation of this sequence is
|
|
3984
|
+
// the failure mode a repair verb most easily introduces.
|
|
3985
|
+
const setupLegGroups = (owner) => ({
|
|
3986
|
+
chunks: scriptChunks.map((chunk, i) => ({
|
|
3987
|
+
index: i,
|
|
3988
|
+
hex: chunk,
|
|
3989
|
+
data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] }),
|
|
3990
|
+
})),
|
|
3991
|
+
schemas: schemas.map(({ key, paramType, auth, authAddress, lockAfter, min, max, selectOptions }) => ({
|
|
3992
|
+
key,
|
|
3993
|
+
data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth, authAddress, lockAfter, min, max, selectOptions] }),
|
|
3994
|
+
})),
|
|
3995
|
+
deps: { count: deps.length, registry: depRegistry ?? null, calls: dependencySetupCalls(deps, depRegistry) },
|
|
3996
|
+
// program lane only: animation_url field · the URI renderers (before the mints)
|
|
3997
|
+
uri: { calls: onchainUriLegs ?? [], animationField: onchainUriLegs?.length ? F.animationUrl : null },
|
|
3998
|
+
// The INTENDED TOTAL, not a count to add — a resume mints the shortfall. `mintCount` covers both
|
|
3999
|
+
// lanes: folded-into-init (renderer-only) and setup-carried, since either way it is what the
|
|
4000
|
+
// creator asked for.
|
|
4001
|
+
mints: { intendedTotal: mintCount, data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'mint', args: [owner] }) },
|
|
4002
|
+
});
|
|
4003
|
+
const setupCalls = (owner) => {
|
|
4004
|
+
const g = setupLegGroups(owner);
|
|
4005
|
+
return [
|
|
4006
|
+
...g.chunks.map((c) => c.data),
|
|
4007
|
+
...g.schemas.map((x) => x.data),
|
|
4008
|
+
...g.deps.calls,
|
|
4009
|
+
...g.uri.calls,
|
|
4010
|
+
// folded mints ride init, not here — so this uses setupMintCount, not the intended total.
|
|
4011
|
+
...Array.from({ length: setupMintCount }, () => g.mints.data),
|
|
4012
|
+
];
|
|
4013
|
+
};
|
|
3901
4014
|
const cid = resolveChain(CHAIN).id;
|
|
3902
4015
|
const preparedFor = async (owner) => {
|
|
3903
4016
|
const salt = parseSaltFlag(flags.salt) ?? saltFor(owner);
|
|
@@ -3908,6 +4021,139 @@ async function cmdDeployCode(flags) {
|
|
|
3908
4021
|
txs.push(prepareCodeSetup({ contract: clone, calls, chainId: cid, chunkCount: scriptChunks.length, chunkBytes: scriptChunks.map((h) => (h.length - 2) / 2), schemaKeys: schemas.map((x) => x.key), deps: deps.map((d) => d.display), onchainUri: onChainUri && hasProgram }));
|
|
3909
4022
|
return { clone, txs };
|
|
3910
4023
|
};
|
|
4024
|
+
// ── --resume: finish an EXISTING contract, deploy nothing (backlog B15) ─────────────────────
|
|
4025
|
+
// A code deploy is two transactions. When the second fails you own a live-but-unusable contract and
|
|
4026
|
+
// the salt reserved for its address is spent, so the dry run's pinned-salt reproduce command can
|
|
4027
|
+
// never be run again. The contract is recoverable, not lost — a tester proved that by resending the
|
|
4028
|
+
// setup by hand with `cast`. This is that, as a verb: read what is missing, send only that.
|
|
4029
|
+
const resumeAddr = flags.resume;
|
|
4030
|
+
if (resumeAddr !== undefined) {
|
|
4031
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(resumeAddr)) {
|
|
4032
|
+
throw new Error(`--resume wants the address of the contract to finish; '${resumeAddr}' isn't a 0x address.`);
|
|
4033
|
+
}
|
|
4034
|
+
const target = resumeAddr;
|
|
4035
|
+
const code = await publicClient.getCode({ address: target }).catch(() => undefined);
|
|
4036
|
+
if (!code || code === '0x') {
|
|
4037
|
+
throw new Error(`--resume ${target}: no contract at that address on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}). ` +
|
|
4038
|
+
`There is nothing to finish — if the DEPLOY tx is what failed, run a normal deploy instead. ` +
|
|
4039
|
+
`If you JUST deployed, give the tx a block or two to mine.`);
|
|
4040
|
+
}
|
|
4041
|
+
// Deploy-time-only flags are refused rather than ignored: --salt/--721c/--bootstrap-factory all
|
|
4042
|
+
// describe how a contract is CREATED, and this creates nothing. 721C especially — enrollment is
|
|
4043
|
+
// permanent and deploy-time-only, so accepting the flag here would imply it can be added later.
|
|
4044
|
+
for (const [flag, why] of [
|
|
4045
|
+
['salt', 'the address already exists, so no salt is used'],
|
|
4046
|
+
['721c', 'ERC-721C enrollment is deploy-time-only and PERMANENT — it cannot be added to an existing collection'],
|
|
4047
|
+
['bootstrap-factory', 'no factory is involved: nothing is being created'],
|
|
4048
|
+
['mint-all', 'use --mint-count <n> on a resume; --mint-all is resolved against the cap at deploy time'],
|
|
4049
|
+
]) {
|
|
4050
|
+
if (flags[flag] !== undefined)
|
|
4051
|
+
throw new Error(`--resume cannot be combined with --${flag}: ${why}.`);
|
|
4052
|
+
}
|
|
4053
|
+
const owner = await publicClient
|
|
4054
|
+
.readContract({ address: target, abi: seriesCodeAbi, functionName: 'owner' })
|
|
4055
|
+
.catch(() => undefined);
|
|
4056
|
+
if (!owner)
|
|
4057
|
+
throw new Error(`--resume ${target}: could not read owner() — is this an ABX SeriesCode contract?`);
|
|
4058
|
+
step('Resume — read what is missing, send only that');
|
|
4059
|
+
info(`target ${bold(target)} ${dim(`· owner ${owner}`)}`);
|
|
4060
|
+
// Every read is a view on the target; the intended legs come from the SAME builder a fresh deploy
|
|
4061
|
+
// uses, so what gets sent is by construction what the deploy would have written.
|
|
4062
|
+
const legs = setupLegGroups(owner);
|
|
4063
|
+
const readAt = async (functionName, args = []) => {
|
|
4064
|
+
try {
|
|
4065
|
+
return (await publicClient.readContract({ address: target, abi: seriesCodeAbi, functionName, args }));
|
|
4066
|
+
}
|
|
4067
|
+
catch {
|
|
4068
|
+
return null;
|
|
4069
|
+
}
|
|
4070
|
+
};
|
|
4071
|
+
const nonZero = (a) => (a && a !== zeroAddress ? a : null);
|
|
4072
|
+
const reader = {
|
|
4073
|
+
scriptChunkCount: async () => Number((await readAt('scriptChunkCount')) ?? 0n),
|
|
4074
|
+
scriptChunk: (index) => readAt('scriptChunk', [BigInt(index)]),
|
|
4075
|
+
schemaExists: async (key) => {
|
|
4076
|
+
const r = await readAt('paramSchema', [encodeTag(key)]);
|
|
4077
|
+
return !!r?.[0];
|
|
4078
|
+
},
|
|
4079
|
+
dependencyCount: async () => Number((await readAt('dependencyCount')) ?? 0n),
|
|
4080
|
+
dependencyRegistry: async () => nonZero(await readAt('dependencyRegistry')),
|
|
4081
|
+
tokenURIRenderer: async () => nonZero(await readAt('tokenURIRenderer')),
|
|
4082
|
+
contractURIRenderer: async () => nonZero(await readAt('contractURIRenderer')),
|
|
4083
|
+
contractFieldSet: async (field) => {
|
|
4084
|
+
const r = await readAt('contractField', [encodeTag(field)]);
|
|
4085
|
+
return !!r && r[0] !== `0x${'0'.repeat(64)}`;
|
|
4086
|
+
},
|
|
4087
|
+
totalSupply: async () => Number((await readAt('totalSupply')) ?? 0n),
|
|
4088
|
+
};
|
|
4089
|
+
const plan = await planResume(reader, legs);
|
|
4090
|
+
for (const line of plan.done)
|
|
4091
|
+
ok(line);
|
|
4092
|
+
for (const line of plan.todo)
|
|
4093
|
+
info(`will send — ${line}`);
|
|
4094
|
+
if (!plan.calls.length) {
|
|
4095
|
+
ok(`nothing missing — this contract's setup is already complete. ${dim('Confirm with `abx verify ' + target + '`.')}`);
|
|
4096
|
+
emit(jsonSafe({ command: 'deploy-code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, sent: false, complete: true, sentLegs: 0 }));
|
|
4097
|
+
console.log('');
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
const tx = prepareCodeSetup({
|
|
4101
|
+
contract: target,
|
|
4102
|
+
calls: plan.calls,
|
|
4103
|
+
chainId: resolveChain(CHAIN).id,
|
|
4104
|
+
chunkCount: plan.sending.chunkIndices.length,
|
|
4105
|
+
chunkBytes: plan.sending.chunkBytes,
|
|
4106
|
+
schemaKeys: plan.sending.schemaKeys,
|
|
4107
|
+
deps: plan.sending.deps ? deps.map((d) => d.display) : [],
|
|
4108
|
+
onchainUri: plan.sending.uri,
|
|
4109
|
+
});
|
|
4110
|
+
if (flags['dry-run']) {
|
|
4111
|
+
console.log(`\n ${bold('\u25c6 ' + tx.summary)} ${dim('(dry run — nothing sent)')}`);
|
|
4112
|
+
for (const [k, v] of Object.entries(tx.fields))
|
|
4113
|
+
console.log(` ${dim(k.padEnd(14))} ${v}`);
|
|
4114
|
+
console.log(dim(`\n Re-run without --dry-run to send (lane: ${lane}).\n`));
|
|
4115
|
+
emit(jsonSafe({ command: 'deploy-code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, dryRun: true, sent: false, complete: false, sentLegs: plan.calls.length }));
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
await confirmSend(`finish setup on ${target} (${plan.calls.length} call(s), one transaction)`, flags);
|
|
4119
|
+
// Signed through the same choke point as every other write, so --sign / --unsigned / the owner
|
|
4120
|
+
// check / the chain-id guard all apply exactly as they do to a deploy.
|
|
4121
|
+
const result = await signTx(() => tx, {
|
|
4122
|
+
lane,
|
|
4123
|
+
chainKey: CHAIN,
|
|
4124
|
+
yes: !!flags.yes,
|
|
4125
|
+
expectedSigner: owner,
|
|
4126
|
+
port: flags.port ? Number(flags.port) : undefined,
|
|
4127
|
+
signUrlFile: flags['sign-url-file'],
|
|
4128
|
+
});
|
|
4129
|
+
if (!result) {
|
|
4130
|
+
console.log(`\n${dim(' unsigned — broadcast it, then re-run `abx verify ' + target + '` to confirm the setup completed.')}\n`);
|
|
4131
|
+
emit(jsonSafe({ command: 'deploy-code', resumed: target, chain: CHAIN, chainId: resolveChain(CHAIN).id, sent: false, complete: false, sentLegs: plan.calls.length }));
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
ok(`${tx.summary}`);
|
|
4135
|
+
// Locals, not the outer clone/deployBlock: this branch never falls through to the deploy path, and
|
|
4136
|
+
// the authoritative scan floor is still the clone's CREATION block (getCode search), not this
|
|
4137
|
+
// repair tx's block — flooring the resolver above the deploy would hide the `code` field again.
|
|
4138
|
+
const resumedFloor = (await discoverDeployBlock(publicClient, target)) ?? result.blockNumber;
|
|
4139
|
+
emit(jsonSafe({
|
|
4140
|
+
command: 'deploy-code',
|
|
4141
|
+
resumed: target,
|
|
4142
|
+
address: target,
|
|
4143
|
+
chain: CHAIN,
|
|
4144
|
+
chainId: resolveChain(CHAIN).id,
|
|
4145
|
+
deployBlock: resumedFloor,
|
|
4146
|
+
sent: true,
|
|
4147
|
+
complete: true,
|
|
4148
|
+
sentLegs: plan.calls.length,
|
|
4149
|
+
txHash: result.txHash,
|
|
4150
|
+
}));
|
|
4151
|
+
step('Index');
|
|
4152
|
+
await cmdAdd(target, { 'from-block': resumedFloor.toString(), factory, label: name });
|
|
4153
|
+
console.log(`\n ${g('\u2713 setup finished')} \u2014 ${dim('confirm it resolves:')} ${bold(`abx verify ${target}`)}` +
|
|
4154
|
+
`${onChainUri ? dim(' (chain-complete + the on-chain tokenURI)') : ''}\n`);
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
3911
4157
|
let clone;
|
|
3912
4158
|
let deployBlock;
|
|
3913
4159
|
if (lane === 'unsigned') {
|
|
@@ -3990,6 +4236,20 @@ async function cmdDeployCode(flags) {
|
|
|
3990
4236
|
if (discovered !== null)
|
|
3991
4237
|
deployBlock = discovered;
|
|
3992
4238
|
ok(`SeriesCode live: ${clone} ${dim(`(from block ${deployBlock})`)}`);
|
|
4239
|
+
// Emitted here rather than at the end: a code deploy is TWO transactions, and if the second (setup)
|
|
4240
|
+
// fails, the address of the live-but-incomplete contract is the single most valuable thing a caller
|
|
4241
|
+
// can be told — it is exactly the input `--resume <address>` takes to finish the job.
|
|
4242
|
+
emit(jsonSafe({
|
|
4243
|
+
command: 'deploy-code',
|
|
4244
|
+
address: clone,
|
|
4245
|
+
chain: CHAIN,
|
|
4246
|
+
chainId: resolveChain(CHAIN).id,
|
|
4247
|
+
factory,
|
|
4248
|
+
deployBlock,
|
|
4249
|
+
name,
|
|
4250
|
+
symbol,
|
|
4251
|
+
onChainUri,
|
|
4252
|
+
}));
|
|
3993
4253
|
step('Index');
|
|
3994
4254
|
await cmdAdd(clone, { 'from-block': deployBlock.toString(), factory, label: name });
|
|
3995
4255
|
if (onChainUri) {
|
|
@@ -4149,6 +4409,34 @@ async function cmdRender(address, tokenIds, flags) {
|
|
|
4149
4409
|
warn(`${summary}\n ${dim(stats.errors[0] ?? 'see error above')}`);
|
|
4150
4410
|
else
|
|
4151
4411
|
ok(summary);
|
|
4412
|
+
if (stats.ran)
|
|
4413
|
+
noteArweavePropagation(flags);
|
|
4414
|
+
}
|
|
4415
|
+
/**
|
|
4416
|
+
* After an Arweave publish, say that a fresh 404 at the gateway is PROPAGATION, not a failed render.
|
|
4417
|
+
*
|
|
4418
|
+
* `arweave.net` lags Turbo uploads by minutes: a tester found 32/32 of their renders 404ing there
|
|
4419
|
+
* while Turbo reported CONFIRMED and 22/32 already served fine from other ar.io gateways. The bytes
|
|
4420
|
+
* were never in doubt — only the gateway was behind. Unexplained, a "broken" thumbnail on a fresh
|
|
4421
|
+
* drop reads as a failed render, and the natural next move is `abx render --force` on all of them:
|
|
4422
|
+
* a full re-upload that fixes nothing.
|
|
4423
|
+
*
|
|
4424
|
+
* Note this is advisory only, and deliberately so — the locator is baked into what the resolver
|
|
4425
|
+
* registers at publish time, so it cannot be repaired by a redirect later. Choosing the gateway is
|
|
4426
|
+
* the operator's call (`ABX_ARWEAVE_GATEWAY`), which is why this names it.
|
|
4427
|
+
*/
|
|
4428
|
+
function noteArweavePropagation(flags) {
|
|
4429
|
+
const opts = storageOptions(storageOverrides(flags));
|
|
4430
|
+
if (resolveBackend(opts).id !== 'arweave')
|
|
4431
|
+
return;
|
|
4432
|
+
const gateway = opts.arweave?.gateway ?? 'https://arweave.net';
|
|
4433
|
+
if (!/(^|\/\/)([^/]*\.)?arweave\.net/.test(gateway))
|
|
4434
|
+
return; // a gateway they chose — don't lecture
|
|
4435
|
+
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.')}`);
|
|
4436
|
+
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.')}`);
|
|
4437
|
+
// The note is advisory; this is the answer. Naming it here is the difference between "wait and
|
|
4438
|
+
// hope" and a check a script can gate on — which is what both reporters ended up building.
|
|
4439
|
+
info(` ${dim('to KNOW rather than wait:')} ${bold('abx storage status <locator>')} ${dim('— ready vs propagating vs unreachable, exits non-zero until it serves.')}`);
|
|
4152
4440
|
}
|
|
4153
4441
|
/**
|
|
4154
4442
|
* The publish topology's one hard prerequisite, checked BEFORE any capture.
|
|
@@ -4283,13 +4571,19 @@ async function cmdIndex(address, flags) {
|
|
|
4283
4571
|
// so an agent can confirm integrity right after deploy/mint without curling.
|
|
4284
4572
|
async function cmdVerify(address, flags) {
|
|
4285
4573
|
if (!address || address.startsWith('--')) {
|
|
4286
|
-
console.error('usage: abx verify <address
|
|
4574
|
+
console.error('usage: abx verify <address> [--json] [--remote <name|url>]\n');
|
|
4287
4575
|
process.exit(1);
|
|
4288
4576
|
}
|
|
4289
4577
|
allowLargeScan(flags);
|
|
4290
4578
|
const remote = remoteFlag(flags);
|
|
4291
4579
|
if (remote)
|
|
4292
4580
|
return cmdVerifyRemote(address, remote);
|
|
4581
|
+
// `--json` matters most here of all the read commands: verify already exits non-zero on a byte
|
|
4582
|
+
// mismatch, so it is the one command a CI job would gate on — and gating on prose is what B19 is
|
|
4583
|
+
// about. The payload's `ok` is the same boolean the exit code carries.
|
|
4584
|
+
return withJson(flags, async (emit) => cmdVerifyBody(address, flags, emit));
|
|
4585
|
+
}
|
|
4586
|
+
async function cmdVerifyBody(address, flags, emit) {
|
|
4293
4587
|
const indexer = new SelfHostIndexer();
|
|
4294
4588
|
let state = indexer.getProject(address);
|
|
4295
4589
|
if (indexer.store.getRegistration(address)) {
|
|
@@ -4300,6 +4594,20 @@ async function cmdVerify(address, flags) {
|
|
|
4300
4594
|
}
|
|
4301
4595
|
if (!state)
|
|
4302
4596
|
throw new Error(`could not load state for ${address}`);
|
|
4597
|
+
// Accumulated as the checks run, re-emitted at each stage so a payload survives an early return.
|
|
4598
|
+
const verifyReport = {
|
|
4599
|
+
address,
|
|
4600
|
+
chain: CHAIN,
|
|
4601
|
+
name: state.name ?? null,
|
|
4602
|
+
canonical: state.isCanonical, // TRUE tri-state: true | false | null (couldn't check)
|
|
4603
|
+
owner: state.owner ?? null,
|
|
4604
|
+
contentChecks: [],
|
|
4605
|
+
contentIntegrity: 'no-commitments',
|
|
4606
|
+
renders: null,
|
|
4607
|
+
onChainUri: null,
|
|
4608
|
+
ok: true,
|
|
4609
|
+
};
|
|
4610
|
+
emit(verifyReport);
|
|
4303
4611
|
console.log(bold(`\n verify ${state.name ?? address}`));
|
|
4304
4612
|
// isCanonical is a TRUE TRI-STATE (true | false | null) and collapsing it lost the only
|
|
4305
4613
|
// distinction that matters: "the chain says this is NOT a clone of the configured factory" is a
|
|
@@ -4315,6 +4623,15 @@ async function cmdVerify(address, flags) {
|
|
|
4315
4623
|
const tok = state.tokens.find((s) => s.tokenId === t.tokenId);
|
|
4316
4624
|
info(`token #${t.tokenId}: ${tok?.minted ? 'minted' : 'not yet minted'}`);
|
|
4317
4625
|
for (const ch of t.checks) {
|
|
4626
|
+
verifyReport.contentChecks.push({
|
|
4627
|
+
tokenId: t.tokenId,
|
|
4628
|
+
minted: !!tok?.minted,
|
|
4629
|
+
kind: ch.kind,
|
|
4630
|
+
// tri-state again: true (re-hashed, matched) · false (MISMATCH) · null (pointer-only,
|
|
4631
|
+
// nothing local to recompute). Collapsing null into false would report a normal state as a
|
|
4632
|
+
// failure, which is the bug this command exists to not have.
|
|
4633
|
+
verified: ch.verified,
|
|
4634
|
+
});
|
|
4318
4635
|
anyCheck = true;
|
|
4319
4636
|
if (ch.verified === true)
|
|
4320
4637
|
ok(`${ch.kind} matches on-chain commitment — content is authentic`);
|
|
@@ -4328,6 +4645,9 @@ async function cmdVerify(address, flags) {
|
|
|
4328
4645
|
}
|
|
4329
4646
|
if (!anyCheck && !isCodeProject(state))
|
|
4330
4647
|
info('no content commitments on this project');
|
|
4648
|
+
verifyReport.contentIntegrity = !anyCheck ? 'no-commitments' : allGood ? 'ok' : 'mismatch';
|
|
4649
|
+
verifyReport.ok = !anyCheck || allGood;
|
|
4650
|
+
emit(verifyReport);
|
|
4331
4651
|
// Code projects have no on-chain `image` commitment (the still is rendered off-chain by the
|
|
4332
4652
|
// effect runner), so the hash checks above are silent for them. Instead report what actually
|
|
4333
4653
|
// matters after mint: does a REAL thumbnail exist yet, or is the marketplace image still the
|
|
@@ -4361,14 +4681,36 @@ async function cmdVerify(address, flags) {
|
|
|
4361
4681
|
// `abx verify <addr> --remote <resolver>` probes what the resolver actually serves (the truthful check).
|
|
4362
4682
|
if (minted.length)
|
|
4363
4683
|
info(dim(`render check is against THIS node's store; for a HOSTED drop use \`abx verify ${address} --remote <resolver>\``));
|
|
4684
|
+
// ONE line per outcome, not per token. This printed the same full-sentence advisory 32 times on a
|
|
4685
|
+
// 32-token project (~4KB of identical text) and pushed the four lines that answer "did my deploy
|
|
4686
|
+
// work" off the top of the screen; at a 1000-token supply it is unreadable. The per-token detail
|
|
4687
|
+
// that survives is the token LIST, which is the only part that differs.
|
|
4688
|
+
const missing = [];
|
|
4689
|
+
let present = 0;
|
|
4364
4690
|
for (const token of minted) {
|
|
4365
4691
|
const { found } = await currentRenderArtifact(client, state, token, storageForRender, 'image');
|
|
4366
4692
|
if (found)
|
|
4367
|
-
|
|
4368
|
-
else
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4693
|
+
present++;
|
|
4694
|
+
else
|
|
4695
|
+
missing.push(String(token.tokenId));
|
|
4696
|
+
}
|
|
4697
|
+
verifyReport.renders = {
|
|
4698
|
+
minted: minted.length,
|
|
4699
|
+
present,
|
|
4700
|
+
missing, // token ids, so a caller can re-render exactly these
|
|
4701
|
+
// Named for what it IS: this node's store. A render PUBLISHED to a hosted resolver lives
|
|
4702
|
+
// there, not here, so `missing` is a false negative for a hosted drop — hence the scope.
|
|
4703
|
+
scope: "this node's store",
|
|
4704
|
+
};
|
|
4705
|
+
emit(verifyReport);
|
|
4706
|
+
if (present)
|
|
4707
|
+
ok(`${present}/${minted.length} minted token(s): real render present (in this node's store)`);
|
|
4708
|
+
if (missing.length) {
|
|
4709
|
+
renderGap = true;
|
|
4710
|
+
const ids = missing.length > 12 ? `${missing.slice(0, 12).join(', ')}, …+${missing.length - 12} more` : missing.join(', ');
|
|
4711
|
+
console.log(` ${c.orange}⚠${c.reset} ${missing.length}/${minted.length} token(s) have no render in THIS node's store ${dim(`(#${ids})`)}`);
|
|
4712
|
+
console.log(` ${dim('published to a hosted resolver? check there:')} ${bold(`abx verify ${address} --remote <resolver>`)}`);
|
|
4713
|
+
console.log(` ${dim('else render them:')} ${bold(`abx render ${address}`)} ${dim('(once) ·')} ${bold('abx effects')} ${dim('(continuous)')}`);
|
|
4372
4714
|
}
|
|
4373
4715
|
}
|
|
4374
4716
|
// The on-chain URI lane (a non-zero tokenURIRenderer, or an animation field pointing at the
|
|
@@ -4380,6 +4722,18 @@ async function cmdVerify(address, flags) {
|
|
|
4380
4722
|
try {
|
|
4381
4723
|
const report = await onChainUriReport(client, state, flags.generator);
|
|
4382
4724
|
const { status, probe } = report;
|
|
4725
|
+
verifyReport.onChainUri = jsonSafe({
|
|
4726
|
+
generator: report.generator,
|
|
4727
|
+
tokenURIRenderer: state.tokenURIRenderer ?? null,
|
|
4728
|
+
branch: status.branchName,
|
|
4729
|
+
chainComplete: !!status.chainComplete,
|
|
4730
|
+
unresolvedRefs: [...status.unresolvedRefs],
|
|
4731
|
+
urlOverBudget: !!status.urlOverBudget,
|
|
4732
|
+
probe: probe
|
|
4733
|
+
? { tokenId: probe.tokenId, onChainJson: !!probe.onChainJson, animation: probe.animation }
|
|
4734
|
+
: null,
|
|
4735
|
+
});
|
|
4736
|
+
emit(verifyReport);
|
|
4383
4737
|
info(`on-chain URI lane: generator ${report.generator}${state.tokenURIRenderer ? ` · tokenURI renderer ${state.tokenURIRenderer}` : ' · tokenURIRenderer NOT set (animation field only — tokenURI still resolves off-chain)'}`);
|
|
4384
4738
|
if (status.branchName === 'template') {
|
|
4385
4739
|
ok(`generator branch: template — the document assembles from the on-chain script chunks`);
|
|
@@ -4642,15 +4996,28 @@ function decodeOnChainJson(uri, verbatim = false) {
|
|
|
4642
4996
|
return verbatim ? raw : raw.slice(0, 600);
|
|
4643
4997
|
}
|
|
4644
4998
|
}
|
|
4645
|
-
async function cmdTokenUri(address, flags) {
|
|
4999
|
+
async function cmdTokenUri(address, flags, extra = []) {
|
|
4646
5000
|
if (!address || address.startsWith('--')) {
|
|
4647
|
-
console.error('usage: abx tokenuri <address> [--token <id>] [--json]\n');
|
|
5001
|
+
console.error('usage: abx tokenuri <address> [--token <id>] [--fetch] [--json]\n');
|
|
4648
5002
|
process.exit(1);
|
|
4649
5003
|
}
|
|
4650
5004
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
4651
5005
|
console.error(`abx tokenuri: '${address}' isn't a 0x contract address.\n`);
|
|
4652
5006
|
process.exit(1);
|
|
4653
5007
|
}
|
|
5008
|
+
// `abx tokenuri <addr> 0` silently ignored the `0` and printed token 0 — a COINCIDENTALLY correct
|
|
5009
|
+
// answer, which is the dangerous kind: `… <addr> 7` would have printed token 0 just as confidently
|
|
5010
|
+
// and exited 0. The token id is a flag here, so name it rather than guessing at intent.
|
|
5011
|
+
const strayPositionals = positionalArgs(extra);
|
|
5012
|
+
if (strayPositionals.length) {
|
|
5013
|
+
const first = strayPositionals[0];
|
|
5014
|
+
const looksLikeTokenId = /^\d+$/.test(first);
|
|
5015
|
+
console.error(`abx tokenuri: unexpected extra argument '${first}'.` +
|
|
5016
|
+
(looksLikeTokenId
|
|
5017
|
+
? ` The token id is a flag — did you mean:\n abx tokenuri ${address} --token ${first}\n`
|
|
5018
|
+
: `\n usage: abx tokenuri <address> [--token <id>] [--fetch] [--json]\n`));
|
|
5019
|
+
process.exit(1);
|
|
5020
|
+
}
|
|
4654
5021
|
const tokenId = BigInt(flags.token ?? '0');
|
|
4655
5022
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
4656
5023
|
// A creator verifying their work often runs this on a wrong / not-yet-mined address — turn viem's
|
|
@@ -4679,10 +5046,33 @@ async function cmdTokenUri(address, flags) {
|
|
|
4679
5046
|
}
|
|
4680
5047
|
process.exit(1);
|
|
4681
5048
|
}
|
|
5049
|
+
// --fetch: FOLLOW the URL the contract commits to and print what is actually served (backlog B14).
|
|
5050
|
+
// The gap this closes: `tokenuri` read the chain, `verify` re-hashed bytes, `status` reported the
|
|
5051
|
+
// lifecycle — none of them showed the served JSON body, so an agent in a cold sweep fell back to raw
|
|
5052
|
+
// `curl`. It also makes a provider mismatch self-evident without a warning that cries wolf: if you
|
|
5053
|
+
// registered with one provider while a different base is baked on-chain, the fetch shows you what
|
|
5054
|
+
// the BAKED base returns (often a 404, or another project's document entirely).
|
|
5055
|
+
const served = flags.fetch !== undefined ? await fetchServedTokenUri(uri) : null;
|
|
4682
5056
|
// --json: the verbatim document, nothing else on stdout — no banner, no ANSI, no truncation, so
|
|
4683
5057
|
// `abx tokenuri <addr> --json | jq` is a supported read path rather than something to regex.
|
|
4684
5058
|
if (flags.json !== undefined) {
|
|
4685
|
-
|
|
5059
|
+
const onChain = decodeOnChainJson(uri, true);
|
|
5060
|
+
if (served === null) {
|
|
5061
|
+
process.stdout.write((onChain ?? uri) + '\n');
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
// With --fetch the payload has to say WHICH answer is which: a marketplace reads the served body,
|
|
5065
|
+
// and conflating it with the on-chain URI is the confusion the flag exists to remove.
|
|
5066
|
+
process.stdout.write(JSON.stringify({
|
|
5067
|
+
tokenId: tokenId.toString(),
|
|
5068
|
+
contract: address,
|
|
5069
|
+
chain: CHAIN,
|
|
5070
|
+
tokenURI: uri,
|
|
5071
|
+
onChain: onChain !== null,
|
|
5072
|
+
served,
|
|
5073
|
+
}, null, 2) + '\n');
|
|
5074
|
+
if (served.status !== null && (served.status < 200 || served.status >= 300))
|
|
5075
|
+
process.exitCode = 1;
|
|
4686
5076
|
return;
|
|
4687
5077
|
}
|
|
4688
5078
|
console.log(`\n ${bold(`tokenURI(${tokenId})`)} ${dim(`— read directly from ${address} on ${CHAIN}, no server`)}`);
|
|
@@ -4694,6 +5084,36 @@ async function cmdTokenUri(address, flags) {
|
|
|
4694
5084
|
else {
|
|
4695
5085
|
console.log(` ${uri.slice(0, 240)}${uri.length > 240 ? dim(`… (${uri.length} chars)`) : ''}\n`);
|
|
4696
5086
|
}
|
|
5087
|
+
if (served)
|
|
5088
|
+
reportServedTokenUri(served, address);
|
|
5089
|
+
}
|
|
5090
|
+
/** The human readout for `--fetch`. */
|
|
5091
|
+
function reportServedTokenUri(served, address) {
|
|
5092
|
+
if (served.skipped) {
|
|
5093
|
+
info(`--fetch: ${served.skipped}`);
|
|
5094
|
+
return;
|
|
5095
|
+
}
|
|
5096
|
+
console.log(` ${bold('served')} ${dim(`— GET ${served.url}`)}`);
|
|
5097
|
+
if (served.status === null) {
|
|
5098
|
+
warn(`the baked URL did not respond: ${served.error}. That host is what a marketplace will ask, so this is what they see too.`);
|
|
5099
|
+
return;
|
|
5100
|
+
}
|
|
5101
|
+
const line = `HTTP ${served.status}${served.contentType ? dim(` · ${served.contentType.split(';')[0]}`) : ''}`;
|
|
5102
|
+
if (servedOk(served))
|
|
5103
|
+
ok(line);
|
|
5104
|
+
else
|
|
5105
|
+
console.log(` ${c.orange}\u26a0${c.reset} ${line}`);
|
|
5106
|
+
const pretty = prettyBody(served.body);
|
|
5107
|
+
const clipped = pretty.length > 1200 ? `${pretty.slice(0, 1200)}\n \u2026 (${pretty.length} chars \u2014 --json for the full body)` : pretty;
|
|
5108
|
+
console.log(clipped.split('\n').map((l) => ' ' + l).join('\n'));
|
|
5109
|
+
if (!servedOk(served)) {
|
|
5110
|
+
// The mismatch case, stated where it is actionable rather than as a standing warning that would
|
|
5111
|
+
// cry wolf on the common custom-domain setup.
|
|
5112
|
+
console.log(`\n ${dim('the contract commits to that URL, so a non-2xx is about the SERVICE, never a mistyped path:')} ` +
|
|
5113
|
+
`${dim('the project may not be registered there, the service may serve a different chain, or the base baked on-chain may point at a provider you never registered with.')}\n` +
|
|
5114
|
+
` ${dim('check the lifecycle:')} ${bold(`abx status ${address} --remote <name|url>`)}`);
|
|
5115
|
+
}
|
|
5116
|
+
console.log('');
|
|
4697
5117
|
}
|
|
4698
5118
|
// ── contracturi ──────────────────────────────────────────────────────────────
|
|
4699
5119
|
/**
|
|
@@ -4793,6 +5213,7 @@ async function cmdContractUri(address, _flags) {
|
|
|
4793
5213
|
// ── serve ──────────────────────────────────────────────────────────────────--
|
|
4794
5214
|
async function cmdServe(flags) {
|
|
4795
5215
|
const port = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
|
|
5216
|
+
await assertPortFree(port, 'serve');
|
|
4796
5217
|
const baseUrl = resolveBaseUrl(port);
|
|
4797
5218
|
const indexer = new SelfHostIndexer();
|
|
4798
5219
|
// Surface the most-recently-reconstructed project (what you just deployed), not
|
|
@@ -4896,6 +5317,9 @@ async function cmdStorage(rest) {
|
|
|
4896
5317
|
if (sub === 'backup-key') {
|
|
4897
5318
|
return cmdStorageBackupKey(flags);
|
|
4898
5319
|
}
|
|
5320
|
+
if (sub === 'status') {
|
|
5321
|
+
return cmdStorageStatus(args[0], flags);
|
|
5322
|
+
}
|
|
4899
5323
|
// show (default) — the RESOLVED effective config + where each value came from. No persisted state.
|
|
4900
5324
|
const { backend, source } = backendResolution();
|
|
4901
5325
|
const opts = storageOptions(); // resolved from env → default (no per-command flags here)
|
|
@@ -4919,6 +5343,91 @@ async function cmdStorage(rest) {
|
|
|
4919
5343
|
console.log(dim('\n choose per command: --backend <fs|cloud|ipfs|arweave> [--gateway/--bucket/--region/…] · or declare in .env'));
|
|
4920
5344
|
console.log(dim(' secrets from env only: ABX_S3_ACCESS_KEY_ID · ABX_S3_SECRET_ACCESS_KEY · PINATA_JWT · ARWEAVE_UPLOAD_TOKEN · ARWEAVE_JWK\n'));
|
|
4921
5345
|
}
|
|
5346
|
+
/**
|
|
5347
|
+
* `abx storage status <locator>` — is it **retrievable** yet, not just accepted?
|
|
5348
|
+
*
|
|
5349
|
+
* The gap this closes: an upload service answers "accepted" the moment it holds your bytes, a gateway
|
|
5350
|
+
* answers "serving" only once they reach it, and on Arweave that runs to minutes. Two independent
|
|
5351
|
+
* integrators built their own version of this check eight days apart; the second saw 32/32 renders
|
|
5352
|
+
* 404 on `arweave.net` while 22/32 already served elsewhere and the uploader said `CONFIRMED`. The
|
|
5353
|
+
* expensive part is what a creator does next: a placeholder on a fresh drop reads as a failed render,
|
|
5354
|
+
* so you re-run `abx render --force` and re-upload everything for nothing.
|
|
5355
|
+
*/
|
|
5356
|
+
const STORAGE_STATUS_FLAGS = new Set(['json', 'gateway', 'backend', 'primary-only', 'timeout']);
|
|
5357
|
+
async function cmdStorageStatus(locator, flags) {
|
|
5358
|
+
if (!locator || locator.startsWith('--')) {
|
|
5359
|
+
console.error('usage: abx storage status <locator> [--json] [--gateway <url>] [--primary-only]\n' +
|
|
5360
|
+
' <locator> — ar://<txid> · ipfs://<cid> · an https:// gateway URL · or a bare txid/CID\n');
|
|
5361
|
+
process.exit(1);
|
|
5362
|
+
}
|
|
5363
|
+
warnStrayFlags(flags, STORAGE_STATUS_FLAGS, 'storage status');
|
|
5364
|
+
const jsonMode = flags.json !== undefined;
|
|
5365
|
+
// The gateway that matters is the one a baked locator resolves through, so default to the active
|
|
5366
|
+
// backend's configured gateway rather than a hardcoded host — otherwise the verdict is about a
|
|
5367
|
+
// gateway this project will never use.
|
|
5368
|
+
const opts = storageOptions(storageOverrides(flags));
|
|
5369
|
+
const configured = flags.gateway ?? opts.arweave?.gateway ?? opts.ipfs?.gateway ?? undefined;
|
|
5370
|
+
const status = await locatorStatus(locator, {
|
|
5371
|
+
gateway: configured,
|
|
5372
|
+
primaryOnly: flags['primary-only'] !== undefined,
|
|
5373
|
+
timeoutMs: flags.timeout === undefined ? undefined : Number(flags.timeout),
|
|
5374
|
+
});
|
|
5375
|
+
if (jsonMode) {
|
|
5376
|
+
process.stdout.write(JSON.stringify(status, null, 2) + '\n');
|
|
5377
|
+
// A program's whole reason to call this is to gate on it, so the verdict is also the exit code:
|
|
5378
|
+
// 0 = ready, 1 = not yet. `until abx storage status <loc> --json; do sleep 10; done` just works.
|
|
5379
|
+
if (status.readiness !== 'ready')
|
|
5380
|
+
process.exitCode = 1;
|
|
5381
|
+
return;
|
|
5382
|
+
}
|
|
5383
|
+
const probeLine = (p, label) => {
|
|
5384
|
+
const verdict = p.serving
|
|
5385
|
+
? `${g('serving')}`
|
|
5386
|
+
: p.status !== null
|
|
5387
|
+
? `${c.orange}${p.status}${c.reset}`
|
|
5388
|
+
: `${c.orange}no response${c.reset}`;
|
|
5389
|
+
const size = p.bytes !== null ? dim(` · ${p.bytes < 1024 ? `${p.bytes} B` : `${(p.bytes / 1024).toFixed(1)} KB`}`) : '';
|
|
5390
|
+
const type = p.contentType ? dim(` · ${p.contentType.split(';')[0]}`) : '';
|
|
5391
|
+
const why = p.error ? dim(` · ${p.error}`) : '';
|
|
5392
|
+
console.log(` ${label} ${bold(p.gateway)} → ${verdict}${size}${type} ${dim(`${p.ms}ms`)}${why}`);
|
|
5393
|
+
};
|
|
5394
|
+
console.log(`\n ${bold('storage status')} ${dim(`— ${status.network} · ${status.id.length > 60 ? status.id.slice(0, 60) + '…' : status.id}`)}`);
|
|
5395
|
+
probeLine(status.primary, dim('your gateway '));
|
|
5396
|
+
for (const a of status.alternates)
|
|
5397
|
+
probeLine(a, dim('also '));
|
|
5398
|
+
console.log('');
|
|
5399
|
+
if (status.readiness === 'ready') {
|
|
5400
|
+
ok(`${bold('ready')} — your gateway serves these bytes. Safe to reference in a token.`);
|
|
5401
|
+
}
|
|
5402
|
+
else if (status.readiness === 'propagating') {
|
|
5403
|
+
const where = status.alternates.filter((a) => a.serving).map((a) => a.gateway).join(', ');
|
|
5404
|
+
warn(`${bold('propagating')} — the data is provably on the network (${where} serves it) but ${bold(status.primary.gateway)} hasn't caught up.`);
|
|
5405
|
+
info('Waiting is the fix. Do NOT re-render or re-upload — the bytes are already stored, and a second upload just pays twice.');
|
|
5406
|
+
if (status.network === 'arweave') {
|
|
5407
|
+
info(`To serve sooner from a gateway that already has it, set ${bold('ABX_ARWEAVE_GATEWAY')} before the upload that bakes the locator ${dim('(it cannot be repaired afterwards — the gateway is part of the stored value).')}`);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
else {
|
|
5411
|
+
// Never call this "propagating": from outside, a settling locator and a wrong one look the same.
|
|
5412
|
+
warn(`${bold('not retrievable')} — no gateway probed is serving it.`);
|
|
5413
|
+
// One exception worth reading the statuses for. Gateways answer 404 for "I don't have these
|
|
5414
|
+
// bytes" (settling and wrong are identical there) but 400 for "that isn't a valid id at all".
|
|
5415
|
+
// Every probe agreeing on a non-404 client error is real evidence, and it matters because the
|
|
5416
|
+
// inverse mistake — waiting out a typo — costs more than a needless re-upload.
|
|
5417
|
+
const probes = [status.primary, ...status.alternates];
|
|
5418
|
+
const answered = probes.filter((p) => p.status !== null);
|
|
5419
|
+
const allInvalid = answered.length > 0 && answered.every((p) => p.status >= 400 && p.status < 500 && p.status !== 404);
|
|
5420
|
+
if (allInvalid) {
|
|
5421
|
+
info(`every gateway rejected the id itself (${answered.map((p) => p.status).join(', ')}), not merely "don't have it" — so this is very likely a ${bold('malformed locator')} (truncated txid/CID, or a stray character), not propagation. Re-check the value you stored; waiting will not fix it.`);
|
|
5422
|
+
}
|
|
5423
|
+
else {
|
|
5424
|
+
info('Two causes, and this check cannot tell them apart: it is still settling (wait and re-run — minutes on Arweave), or the locator is wrong (a truncated txid/CID, or the upload never completed).');
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
console.log(`\n ${dim('gate a script on this:')} ${bold(`abx storage status ${locator} --json`)} ${dim('(exit 0 only when ready)')}\n`);
|
|
5428
|
+
if (status.readiness !== 'ready')
|
|
5429
|
+
process.exitCode = 1;
|
|
5430
|
+
}
|
|
4922
5431
|
/**
|
|
4923
5432
|
* `abx storage upload <path> [--backend ipfs|arweave|cloud]` — upload one file to a durable
|
|
4924
5433
|
* backend and print the **locator** to point at. This is the missing first half of the data-plane
|
|
@@ -5049,6 +5558,106 @@ async function cmdStorageFunding(sub, flags) {
|
|
|
5049
5558
|
info(' Credits land on the address above and persist for future uploads. Re-check with `abx storage balance`.');
|
|
5050
5559
|
}
|
|
5051
5560
|
// ── status ────────────────────────────────────────────────────────────────--
|
|
5561
|
+
// ── tokens: every token's seed + params, read straight from chain ────────────────────────────
|
|
5562
|
+
// For a generative collection the seed list IS the collection — it's the first thing you want after
|
|
5563
|
+
// a drop ("what did the seeds actually deal?") and, before this existed, the one thing no command
|
|
5564
|
+
// could answer. An agent's workaround was: start `abx serve`, GET the project API, base64-decode
|
|
5565
|
+
// each tokenURI, base64-decode the animation_url inside it, then regex `0x[0-9a-f]{64}` out of the
|
|
5566
|
+
// resulting HTML — 32 times. Every input to that was already a plain contract read.
|
|
5567
|
+
const TOKENS_FLAGS = new Set(['json', 'limit', 'from']);
|
|
5568
|
+
/** An owner address abbreviated for a human table. Abbreviating for eyes is fine; abbreviating for a
|
|
5569
|
+
* PROGRAM is not — which is why every value here is also reachable verbatim via `--json`. */
|
|
5570
|
+
function shortAddr(a) {
|
|
5571
|
+
return `${a.slice(0, 6)}…${a.slice(-4)}`;
|
|
5572
|
+
}
|
|
5573
|
+
async function cmdTokens(address, flags) {
|
|
5574
|
+
if (!address || address.startsWith('--')) {
|
|
5575
|
+
console.error('usage: abx tokens <address> [--json] [--from <id>] [--limit <n>] — every token\'s owner, seed, and params, from chain\n');
|
|
5576
|
+
process.exit(1);
|
|
5577
|
+
}
|
|
5578
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
5579
|
+
console.error(`abx tokens: '${address}' isn't a 0x contract address.\n`);
|
|
5580
|
+
process.exit(1);
|
|
5581
|
+
}
|
|
5582
|
+
warnStrayFlags(flags, TOKENS_FLAGS, 'tokens');
|
|
5583
|
+
const jsonMode = flags.json !== undefined;
|
|
5584
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
5585
|
+
const code = await publicClient.getCode({ address }).catch(() => undefined);
|
|
5586
|
+
if (!code || code === '0x') {
|
|
5587
|
+
// Name the endpoint we actually asked — "no contract here" and "wrong node" are otherwise
|
|
5588
|
+
// indistinguishable (same reasoning as `tokenuri` / `contracturi`).
|
|
5589
|
+
console.error(`abx tokens: no contract at ${address} on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}) — ` +
|
|
5590
|
+
`double-check the address, and that this endpoint is the network you deployed to. ` +
|
|
5591
|
+
`If you JUST deployed, give the tx a block or two to mine.\n`);
|
|
5592
|
+
process.exit(1);
|
|
5593
|
+
}
|
|
5594
|
+
const listing = await listTokens(publicClient, address, {
|
|
5595
|
+
from: flags.from === undefined ? undefined : Number(flags.from),
|
|
5596
|
+
limit: flags.limit === undefined ? undefined : Number(flags.limit),
|
|
5597
|
+
// Progress on stderr only — stdout must stay pure JSON in --json mode (the ANSI-in-a-locator
|
|
5598
|
+
// lesson: a program's channel is not a place to narrate).
|
|
5599
|
+
onProgress: (done, total) => total > 50 && done % 25 === 0 ? process.stderr.write(` reading ${done}/${total}…\r`) : undefined,
|
|
5600
|
+
});
|
|
5601
|
+
if (listing.tokens.length > 50)
|
|
5602
|
+
process.stderr.write(' \r');
|
|
5603
|
+
if (jsonMode) {
|
|
5604
|
+
process.stdout.write(JSON.stringify(listing, null, 2) + '\n');
|
|
5605
|
+
return;
|
|
5606
|
+
}
|
|
5607
|
+
const supply = listing.totalSupply ?? listing.tokens.length;
|
|
5608
|
+
const cap = listing.maxInvocations !== null && listing.maxInvocations > 0 ? ` of ${listing.maxInvocations} max` : '';
|
|
5609
|
+
console.log(`\n ${bold(`tokens ${address}`)} ${dim(`— ${supply} minted${cap} on ${CHAIN}`)}`);
|
|
5610
|
+
info('read straight from the contract — no indexer, no resolver, no server in the loop');
|
|
5611
|
+
if (!listing.hasParams) {
|
|
5612
|
+
info('this token type has no params extension — so no seeds or params to list (that is correct, not a failed read). Owners below.');
|
|
5613
|
+
}
|
|
5614
|
+
else if (!listing.hasParamEnumeration) {
|
|
5615
|
+
warn('this contract predates on-chain param enumeration — seeds still read, but params cannot be listed. Redeploy on a current implementation to enumerate them.');
|
|
5616
|
+
}
|
|
5617
|
+
const contractKeys = Object.keys(listing.contractParams);
|
|
5618
|
+
if (contractKeys.length) {
|
|
5619
|
+
info(`contract-scope params ${dim('(every token inherits these; a token-scope value of the same key wins)')}`);
|
|
5620
|
+
for (const k of contractKeys.sort())
|
|
5621
|
+
console.log(` ${g(k)}=${listing.contractParams[k]}`);
|
|
5622
|
+
}
|
|
5623
|
+
if (!listing.tokens.length) {
|
|
5624
|
+
console.log(`\n ${dim('no tokens in range — nothing minted yet, or --from is past the mint frontier.')}\n`);
|
|
5625
|
+
return;
|
|
5626
|
+
}
|
|
5627
|
+
const anySeed = listing.tokens.some((t) => t.seed !== null);
|
|
5628
|
+
console.log(`\n ${dim('id'.padStart(4))} ${anySeed ? dim('seed'.padEnd(66)) + ' ' : ''}${dim('owner')}`);
|
|
5629
|
+
for (const t of listing.tokens) {
|
|
5630
|
+
// A seed is the whole reason this command exists, so it prints in FULL. The owner truncates —
|
|
5631
|
+
// an address stays recognizable abbreviated, and `--json` carries both verbatim either way.
|
|
5632
|
+
const seedCell = anySeed ? `${t.seed ?? dim('—'.padEnd(66))} ` : '';
|
|
5633
|
+
const owner = t.owner ? shortAddr(t.owner) : dim('— unminted or burned');
|
|
5634
|
+
console.log(` ${bold(('#' + t.tokenId).padStart(4))} ${seedCell}${owner}`);
|
|
5635
|
+
const keys = Object.keys(t.params).sort();
|
|
5636
|
+
if (keys.length)
|
|
5637
|
+
console.log(` ${keys.map((k) => `${g(k)}=${t.params[k]}`).join(dim(' · '))}`);
|
|
5638
|
+
}
|
|
5639
|
+
// The token-scope param SPREAD — what collectors actually chose, which is the question a param
|
|
5640
|
+
// dump is usually a step toward. Deliberately NOT called a trait spread: traits come from running
|
|
5641
|
+
// the script against the seed (that's `abx render`), not from params.
|
|
5642
|
+
const spread = new Map();
|
|
5643
|
+
for (const t of listing.tokens) {
|
|
5644
|
+
for (const [k, v] of Object.entries(t.params)) {
|
|
5645
|
+
const counts = spread.get(k) ?? new Map();
|
|
5646
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
5647
|
+
spread.set(k, counts);
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
if (spread.size && listing.tokens.length > 1) {
|
|
5651
|
+
console.log(`\n ${bold('token-scope param spread')} ${dim('(what has actually been set — not the trait spread, which comes from running the script)')}`);
|
|
5652
|
+
for (const key of [...spread.keys()].sort()) {
|
|
5653
|
+
const counts = [...spread.get(key).entries()].sort((a, b) => b[1] - a[1]);
|
|
5654
|
+
const shown = counts.slice(0, 8).map(([v, n]) => `${v} ${dim('×' + n)}`).join(dim(' · '));
|
|
5655
|
+
const rest = counts.length > 8 ? dim(` …+${counts.length - 8} more values`) : '';
|
|
5656
|
+
console.log(` ${g(key)}: ${shown}${rest}`);
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
console.log(`\n ${dim('every value above verbatim + machine-readable:')} ${bold(`abx tokens ${address} --json`)}\n`);
|
|
5660
|
+
}
|
|
5052
5661
|
// ── state: a one-glance operational snapshot of a deployed contract, read straight from chain ──
|
|
5053
5662
|
// Read-only (no tx, no local index). Series-only getters revert on a 1/1, so each read is defensive
|
|
5054
5663
|
// → the same command works for both. The agent-friendly "what's the state before/after an op" call.
|
|
@@ -5071,6 +5680,11 @@ async function cmdState(address, flags) {
|
|
|
5071
5680
|
console.error('usage: abx state <address> — a read-only, on-chain operational snapshot\n');
|
|
5072
5681
|
process.exit(1);
|
|
5073
5682
|
}
|
|
5683
|
+
// `--json` emits the same facts as data (B19: a value a program needs must be obtainable without
|
|
5684
|
+
// parsing prose). The human narration below is unchanged and rides stderr under --json.
|
|
5685
|
+
return withJson(flags, async (emit) => cmdStateBody(address, flags, emit));
|
|
5686
|
+
}
|
|
5687
|
+
async function cmdStateBody(address, flags, emit) {
|
|
5074
5688
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
5075
5689
|
const read = async (functionName, args = []) => {
|
|
5076
5690
|
try {
|
|
@@ -5099,6 +5713,29 @@ async function cmdState(address, flags) {
|
|
|
5099
5713
|
}
|
|
5100
5714
|
const isSeries = maxInvocations !== undefined; // series-only getter
|
|
5101
5715
|
const addrOrNone = (a) => (a && a !== zeroAddress ? a : dim('none'));
|
|
5716
|
+
// The payload mirrors what is printed, with `undefined` (getter absent) kept distinct from a zero
|
|
5717
|
+
// address (present and deliberately unset) — collapsing those is what makes a readout lie.
|
|
5718
|
+
const zeroToNull = (a) => (a === undefined ? null : a === zeroAddress ? null : a);
|
|
5719
|
+
const payload = jsonSafe({
|
|
5720
|
+
address,
|
|
5721
|
+
chain: CHAIN,
|
|
5722
|
+
chainId: resolveChain(CHAIN).id,
|
|
5723
|
+
type: isSeries ? 'series' : '1of1',
|
|
5724
|
+
owner: owner ?? null,
|
|
5725
|
+
totalSupply: totalSupply ?? null,
|
|
5726
|
+
nextTokenId: nextTokenId ?? null,
|
|
5727
|
+
maxInvocations: maxInvocations ?? null,
|
|
5728
|
+
paused: paused ?? null,
|
|
5729
|
+
minter: zeroToNull(minter),
|
|
5730
|
+
primaryPayee: zeroToNull(primaryPayee),
|
|
5731
|
+
tokenURIRenderer: zeroToNull(renderer),
|
|
5732
|
+
onChainTokenUri: !!(renderer && renderer !== zeroAddress),
|
|
5733
|
+
royalty: royalty ? { receiver: royalty[0], bps: Number(royalty[1]) } : null,
|
|
5734
|
+
creatorToken: !!creatorToken,
|
|
5735
|
+
transferValidator: creatorToken ? zeroToNull(transferValidator) : null,
|
|
5736
|
+
params: { governed: [], contractScopeSet: [], legacy: false },
|
|
5737
|
+
});
|
|
5738
|
+
emit(payload);
|
|
5102
5739
|
console.log(bold(`\n ${isSeries ? 'Series' : '1/1'} state`) + dim(` ${address} · ${CHAIN}`));
|
|
5103
5740
|
info(`owner ${owner ?? dim('?')}`);
|
|
5104
5741
|
if (isSeries) {
|
|
@@ -5142,6 +5779,21 @@ async function cmdState(address, flags) {
|
|
|
5142
5779
|
: [];
|
|
5143
5780
|
const governed = rows.filter((r) => r.s.exists);
|
|
5144
5781
|
const now = Math.floor(Date.now() / 1000);
|
|
5782
|
+
payload.params.legacy = legacy;
|
|
5783
|
+
payload.params.governed = jsonSafe(governed.map(({ key, s }) => ({
|
|
5784
|
+
key,
|
|
5785
|
+
// Canonical NAMES, not the raw enum indices — a caller must not have to know the Solidity
|
|
5786
|
+
// enum's ordering to read this (the same reason `retired` is a boolean below).
|
|
5787
|
+
paramType: PARAM_TYPES[s.paramType] ?? String(s.paramType),
|
|
5788
|
+
auth: AUTH_OPTIONS[s.auth] ?? String(s.auth),
|
|
5789
|
+
authAddress: s.authAddress === zeroAddress ? null : s.authAddress,
|
|
5790
|
+
lockAfter: s.lockAfter,
|
|
5791
|
+
// A past lockAfter IS how a param is retired — surfaced as a boolean so a caller doesn't
|
|
5792
|
+
// have to re-derive the convention from a timestamp.
|
|
5793
|
+
retired: s.lockAfter !== 0 && s.lockAfter < now,
|
|
5794
|
+
selectOptions: s.selectOptions,
|
|
5795
|
+
})));
|
|
5796
|
+
emit(payload);
|
|
5145
5797
|
if (governed.length) {
|
|
5146
5798
|
console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/artist set these; the generator injects them`)}` +
|
|
5147
5799
|
(legacy ? dim(' (legacy project — read from its params.keys list)') : ''));
|
|
@@ -5161,6 +5813,8 @@ async function cmdState(address, flags) {
|
|
|
5161
5813
|
// it is a tokenData coordinate, read directly.
|
|
5162
5814
|
const set = legacy ? null : await readSetParamKeys(publicClient, address);
|
|
5163
5815
|
if (set?.contract.length) {
|
|
5816
|
+
payload.params.contractScopeSet = [...set.contract].sort();
|
|
5817
|
+
emit(payload);
|
|
5164
5818
|
info(`${dim('contract-scope params set:')} ${[...set.contract].sort().join(', ')} ${dim('(apply to every token)')}`);
|
|
5165
5819
|
}
|
|
5166
5820
|
}
|
|
@@ -5441,6 +6095,16 @@ async function cmdRemote(spec, flags) {
|
|
|
5441
6095
|
// SOURCE resolver's PUBLIC api. The two resolvers never talk: we read public endpoints + chain
|
|
5442
6096
|
// and write through the admin control plane. It does NOT cut over — after a clean migration the
|
|
5443
6097
|
// operator re-points DNS (custom domain) or the on-chain base URI (provider endpoint).
|
|
6098
|
+
//
|
|
6099
|
+
// ⚠ IF YOU EVER ADD A REDEPLOY-STYLE MIGRATION HERE — anything that deploys a NEW contract and
|
|
6100
|
+
// re-points identity to it (plausible under the greenfield-redeploy stance) — it MUST read the
|
|
6101
|
+
// source collection's creator-token status (`readCreatorTokenStatus`) and carry the enrollment +
|
|
6102
|
+
// active validator into the new deploy's `transferValidator` InitParam. ERC-721C enrollment is
|
|
6103
|
+
// deploy-time-only and PERMANENT, so a migration that forgets it silently converts an enforced
|
|
6104
|
+
// collection into a plain ERC-721 — unrecoverable except by yet another redeploy. Corollary: a
|
|
6105
|
+
// plain source collection must stay plain; do not "helpfully" enroll on migration. Today's
|
|
6106
|
+
// migrate moves off-chain state only, so enrollment lives on the untouched contract and none of
|
|
6107
|
+
// this applies — which is exactly why the trap is easy to walk into later.
|
|
5444
6108
|
async function cmdMigrate(address, flags) {
|
|
5445
6109
|
// Both sides accept a named remote or a URL. Only the DESTINATION needs a credential — the
|
|
5446
6110
|
// source is read via its PUBLIC api (the exit ramp works with zero provider cooperation).
|
|
@@ -6153,6 +6817,15 @@ const COMMAND_HELP = {
|
|
|
6153
6817
|
The live view 302s to the gateway, so the gateway ${bold('MUST serve HTML')} (a dedicated Pinata gateway or Arweave — never the shared public one)
|
|
6154
6818
|
--name <s> --symbol <s> ERC-721 identity --royalty-bps <0-10000> (default 500)
|
|
6155
6819
|
${bold('--public-base-url <url>')} the resolver you run (${bold('REQUIRED')} unless --onchain-uri — a code project resolves live on-chain state; localhost/missing is refused)
|
|
6820
|
+
${g('--resume')} <address> ${bold('FINISH an existing contract whose setup transaction failed')} — deploys nothing. A code deploy is TWO
|
|
6821
|
+
txns (create the clone, then one atomic setup multicall); when the second fails you own a
|
|
6822
|
+
live-but-unusable contract AND the salt for its address is spent, so the pinned-salt re-run can
|
|
6823
|
+
never work. Pass the SAME content flags the original deploy used: it reads what is already
|
|
6824
|
+
on-chain and sends only what is missing, in one transaction. Chunks compare by CONTENT (so a
|
|
6825
|
+
hand repair is respected), schemas are left alone if present (re-writing one is an upsert that
|
|
6826
|
+
can strand values), and mints are a SHORTFALL against current supply — never a re-send, since a
|
|
6827
|
+
token cannot be un-minted. Nothing missing ⇒ it sends nothing and says so.
|
|
6828
|
+
${dim('Refuses --salt / --721c / --bootstrap-factory / --mint-all: those describe how a contract is CREATED.')}
|
|
6156
6829
|
${g('--onchain-uri')} the CHAIN-COMPLETE lane: tokenURI/contractURI resolve ON-CHAIN via the canonical metadata renderer, and
|
|
6157
6830
|
animation_url is COMPUTED on-chain by the canonical ${bold('AbxGenerator')} (template: the full HTML document;
|
|
6158
6831
|
directory: a parameterized gateway URL — 8KB URL budget, gateway liveness applies). No resolver base is baked
|
|
@@ -6255,6 +6928,9 @@ const COMMAND_HELP = {
|
|
|
6255
6928
|
like ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
|
|
6256
6929
|
A contract-scope param applies to every token, and enumerates on-chain like any other.
|
|
6257
6930
|
--file <path> read the value from a file (String / Bytes payloads)
|
|
6931
|
+
${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`)}
|
|
6932
|
+
${dim('string is refused, because there is no safe guess between "these characters" and "these bytes".')}
|
|
6933
|
+
${dim(`(The docs' "Bytes becomes base64" describes how your ${bold('program')} receives the value, not how you write it.)`)}
|
|
6258
6934
|
${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
|
|
6259
6935
|
the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
|
|
6260
6936
|
resolver running the chain watcher (the ${g('abx serve')} default) sees the change on its next poll (~12s)
|
|
@@ -6294,6 +6970,7 @@ const COMMAND_HELP = {
|
|
|
6294
6970
|
(+ the 721C transfer validator, for a collection that enrolled at deploy — plain ERC-721s show nothing new).
|
|
6295
6971
|
Series-only fields are shown for a Series; a 1/1 shows just supply + royalty + renderer.
|
|
6296
6972
|
Handy before/after owner ops (mint · pause/unpause · set-minter · set-primary-payee).
|
|
6973
|
+
${g('--json')} the same facts as data (uncoloured, untruncated) — the machine path; narration moves to stderr.
|
|
6297
6974
|
${dim('state = what the CHAIN says. For who is SERVING it and how fresh that is, see `abx status`.')}`,
|
|
6298
6975
|
predict: `
|
|
6299
6976
|
${bold('abx predict')} ${dim('— pre-compute a deploy address (read-only).')}
|
|
@@ -6302,6 +6979,8 @@ const COMMAND_HELP = {
|
|
|
6302
6979
|
${bold('abx mint')} <address> ${dim('— mint the NEXT token (a 1/1\'s #0, or a Series\' next-in-order). Owner-only. Sends a tx.')}
|
|
6303
6980
|
--to 0x.. recipient (default: the owner — pre-mint; a buyer = primary sale)
|
|
6304
6981
|
${g('--count')} <n> mint N tokens in order in ONE tx (Series) — e.g. reserves/airdrops (do them BEFORE you unpause the sale)
|
|
6982
|
+
${g('--json')} ${g('{tokenIds, txHash, blockNumber, sent}')} on stdout — the TOKEN ID as data, read from the mint's own
|
|
6983
|
+
Transfer logs (not by re-reading nextTokenId, which a concurrent mint would make wrong).
|
|
6305
6984
|
${dim('minting is sequential (by token id); repeat with different --to for an airdrop to N wallets.')}
|
|
6306
6985
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} · ${g('--dry-run')} preview`,
|
|
6307
6986
|
refresh: `
|
|
@@ -6355,10 +7034,13 @@ const COMMAND_HELP = {
|
|
|
6355
7034
|
${dim('up front: enrollment is a deploy-time decision and can never be added (or fully removed) later.')}
|
|
6356
7035
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} · ${g('--dry-run')} preview (encodes + shows the tx, sends nothing)`,
|
|
6357
7036
|
attach: `
|
|
6358
|
-
${bold('abx attach')} <address> <key> <
|
|
6359
|
-
${dim('The data-plane verb:
|
|
7037
|
+
${bold('abx attach')} <address> <key> <uri> [<key> <uri> …] ${dim('— attach named file(s) to a token. Sends ONE tx.')}
|
|
7038
|
+
${dim('The data-plane verb: each file joins the token\'s served')} ${bold('artifacts')} ${dim('manifest as {key, mimeType, uri}.')}
|
|
6360
7039
|
<key> any name YOU choose (print · certificate · stems · readme · source) — how it appears in the manifest
|
|
6361
7040
|
<uri> the file's locator; representation is ${bold('auto-detected')} from the scheme (ipfs → ipfs, ar → arweave, https → url)
|
|
7041
|
+
${bold('Several pairs in one invocation become ONE transaction')} ${dim('— all-or-nothing, so a revert lands NONE of them. Attaching')}
|
|
7042
|
+
${dim('artifacts one command at a time is what leaves a half-written token on a failure partway through, and a mint')}
|
|
7043
|
+
${dim('cannot be undone. Every pair is validated before anything is sent, and a key repeated in one batch is refused.')}
|
|
6362
7044
|
${g('--file')} <path> instead of a URI: store TINY bytes on-chain (SSTORE2, ≈200 gas/byte) — locators are the norm otherwise
|
|
6363
7045
|
--collection attach to the collection scope · else --token <id> (default 0)
|
|
6364
7046
|
${g('--dry-run')} preview the tx, send nothing · signing: ${g('--send')} (default, needs a hot key) · ${g('--sign')} (browser wallet, no key) · ${g('--unsigned')}
|
|
@@ -6383,8 +7065,12 @@ const COMMAND_HELP = {
|
|
|
6383
7065
|
${g('--dry-run')} preview the tx, send nothing
|
|
6384
7066
|
--collection target collection (ERC-7572) scope · else --token <id> (default 0)`,
|
|
6385
7067
|
'lock-field': `
|
|
6386
|
-
${bold('abx lock-field')} <address> --field <name> ${dim('— freeze a
|
|
6387
|
-
--collection | --token <id
|
|
7068
|
+
${bold('abx lock-field')} <address> --field <name> ${dim('— freeze a metadata FIELD forever (irreversible). Sends a tx.')}
|
|
7069
|
+
--collection | --token <id>
|
|
7070
|
+
--force-field proceed even if a PostParam shares this name (you mean the field)
|
|
7071
|
+
${dim('FIELDS AND PARAMS ARE DIFFERENT NAMESPACES and may share a name. This locks the field only —')}
|
|
7072
|
+
${dim(`to weld a PostParam use ${g('abx set-schema <addr> --schema <key>:<Type>:<Auth>:lock=now')}. Passing a`)}
|
|
7073
|
+
${dim('declared param key here is refused, because locking the field leaves the param writable.')}`,
|
|
6388
7074
|
'set-renderer': `
|
|
6389
7075
|
${bold('abx set-renderer')} <address> ${dim('— toggle URI resolution between off-chain and on-chain. Sends a tx.')}
|
|
6390
7076
|
(default) point at the chain's canonical renderer (deploys it if needed) → resolve ON-CHAIN
|
|
@@ -6433,9 +7119,15 @@ const COMMAND_HELP = {
|
|
|
6433
7119
|
lifecycle: ${dim('queued')} → ${dim('backfilling')} → ${g('live')} · ${c.orange}stale${c.reset} ${dim('(was live, now lagging — still serving)')} · ${c.orange}failed${c.reset} ${dim('(carries a cause; retried with backoff)')}
|
|
6434
7120
|
${dim('status = who is SERVING it and how fresh. For what the CHAIN says (owner, royalty, locks), see `abx state`.')}`,
|
|
6435
7121
|
storage: `
|
|
6436
|
-
${bold('abx storage')} <show|upload|balance|topup|backup-key> ${dim('— inspect / operate byte custody. Mostly read-only.')}
|
|
7122
|
+
${bold('abx storage')} <show|upload|status|balance|topup|backup-key> ${dim('— inspect / operate byte custody. Mostly read-only.')}
|
|
6437
7123
|
${g('show')} the resolved backend (fs | cloud | ipfs | arweave) + where each value came from
|
|
6438
7124
|
${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run] [${g('--json')}]
|
|
7125
|
+
${g('status')} <locator> is it ${bold('RETRIEVABLE')} yet, not just accepted? [${g('--json')}] [--gateway <url>] [--primary-only]
|
|
7126
|
+
An upload service says "accepted" the moment it holds your bytes; a gateway serves them only once
|
|
7127
|
+
they propagate — minutes, on Arweave. Probes YOUR gateway plus two others, because propagation is
|
|
7128
|
+
per-gateway: ${g('ready')} (yours serves it) · ${g('propagating')} (another does, so the data provably exists —
|
|
7129
|
+
${bold('wait, do NOT re-upload')}) · ${g('unreachable')} (nothing does — still settling, or a bad locator).
|
|
7130
|
+
Exits non-zero unless ready, so ${g('until abx storage status <loc>; do sleep 10; done')} is the whole wait.
|
|
6439
7131
|
${g('balance')} · ${g('topup')} --usd <n> Turbo (arweave) upload credits · ${g('backup-key')} --out <path> copy the managed key`,
|
|
6440
7132
|
demo: `
|
|
6441
7133
|
${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.')}
|
|
@@ -6451,7 +7143,8 @@ const COMMAND_HELP = {
|
|
|
6451
7143
|
${g('buy')} <token> [--to 0x..] buy ONE token (public; any funded signer). ETH sales attach the price.
|
|
6452
7144
|
${dim('the full go-live: deploy-series → minter configure → set-minter <token> --minter <printed> → set-primary-payee → unpause')}
|
|
6453
7145
|
--minter-contract 0x.. override the shared minter address (else ABX_FIXED_PRICE_MINTER → manifest)
|
|
6454
|
-
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} · ${g('--dry-run')} preview (safe — sends nothing)
|
|
7146
|
+
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} · ${g('--dry-run')} preview (safe — sends nothing)
|
|
7147
|
+
${g('--json')} the deployed ADDRESS as data on stdout (narration → stderr). With ${g('--dry-run')}: the PREDICTED address + saltPinned.`,
|
|
6455
7148
|
add: `
|
|
6456
7149
|
${bold('abx add')} <address> ${dim('— register + index a project. Also edits off-chain display metadata + traits.')}
|
|
6457
7150
|
--from-block <n> --factory 0x.. --label "<s>" --description "<s>" --external-url <url> [--full] [--yes]
|
|
@@ -6473,12 +7166,29 @@ const COMMAND_HELP = {
|
|
|
6473
7166
|
or 200 bytes), so it accounts for a render PUBLISHED to that resolver. ${bold('Use this for a code project whose')}
|
|
6474
7167
|
${bold('renders were published to a hosted resolver')} — a plain \`abx verify\` only checks THIS machine's store and will
|
|
6475
7168
|
report a false placeholder for a render that lives on the resolver.
|
|
7169
|
+
${g('--json')} the findings as data, with ${g('ok')} matching the exit code — so CI can gate on a byte mismatch
|
|
7170
|
+
without parsing prose (a mismatch exits non-zero either way).
|
|
6476
7171
|
${dim('On-chain URI lane (a non-zero tokenURIRenderer / a generator-backed animation field): also eth_calls the generator')}
|
|
6477
7172
|
${dim(`${g('onChainStatus')} (branch · chain-complete · unresolved refs · URL budget) and decodes ${g('tokenURI')} straight from the contract.`)}`,
|
|
6478
7173
|
tokenuri: `
|
|
6479
7174
|
${bold('abx tokenuri')} <address> [--token <id>] ${dim('— read tokenURI(id) straight from the contract on-chain + decode the JSON (read-only; no server).')}
|
|
6480
7175
|
${dim('The proof a fully on-chain token self-resolves: any RPC returns the renderer-assembled metadata. Default token 0.')}
|
|
6481
|
-
${
|
|
7176
|
+
${g('--fetch')} FOLLOW the URL the contract commits to and print ${bold('what is actually served')} — the body a marketplace reads.
|
|
7177
|
+
On a fully on-chain token there is nothing to fetch (the tokenURI IS the document) and it says so.
|
|
7178
|
+
${dim('This is also how you catch a provider mismatch: it shows what the BAKED base returns, so registering with')}
|
|
7179
|
+
${dim('one provider while another is baked on-chain shows up as their 404 rather than as a warning that guesses.')}
|
|
7180
|
+
${dim('A non-2xx exits non-zero under --json. A bad status here is about the SERVICE, never a mistyped path —')}
|
|
7181
|
+
${dim('the URL came from the chain, so it is right by construction.')}
|
|
7182
|
+
${dim('Collection-level (ERC-7572) counterpart:')} ${g('abx contracturi <address>')}${dim('. Whole-collection counterpart:')} ${g('abx tokens <address>')}`,
|
|
7183
|
+
tokens: `
|
|
7184
|
+
${bold('abx tokens')} <address> ${dim('— every token’s owner, seed, and params, read straight from the contract (read-only; no server).')}
|
|
7185
|
+
${g('--json')} the listing as data: ${g('{chainId, address, contractParams, tokens: [{tokenId, owner, seed, params}]}')} — verbatim,
|
|
7186
|
+
uncoloured, untruncated. The machine path; the human table abbreviates owners only.
|
|
7187
|
+
${g('--from <id>')} first token id to read (default 0) ${g('--limit <n>')} read only n ids from there
|
|
7188
|
+
${dim('For a generative collection the seed list IS the collection — this is the "what did the seeds actually deal?" read,')}
|
|
7189
|
+
${dim('and the input to any distribution check before a real launch. Chain-only: no indexer projection and no running')}
|
|
7190
|
+
${dim('resolver, because the params store enumerates its own keys on-chain and')} ${g('seed')} ${dim('is a reserved param read by name.')}
|
|
7191
|
+
${dim('Traits are NOT here: a trait comes from running the script against the seed —')} ${g('abx render')}${dim(' does that.')}`,
|
|
6482
7192
|
contracturi: `
|
|
6483
7193
|
${bold('abx contracturi')} <address> ${dim('— read contractURI() (ERC-7572 collection metadata) from the contract, FOLLOW it, and decode (read-only).')}
|
|
6484
7194
|
${dim('The collection-level counterpart of')} ${g('tokenuri')}${dim('. On-chain lane: decodes the data: URI. Off-chain lane: fetches the')}
|
|
@@ -6514,6 +7224,11 @@ const COMMAND_HELP = {
|
|
|
6514
7224
|
project to the effects layer (${g('ABX_EFFECTS_URL')} + ${g('ABX_EFFECTS_TOKEN')}) → thumbnails auto-re-render.
|
|
6515
7225
|
${g('ABX_WATCH_INTERVAL_MS')} tunes the cadence; ${g('0')} disables (state then updates only on explicit add/index).
|
|
6516
7226
|
${dim('Reorg note: no lookback (post-PoS reorgs are rare); the repair is the deterministic full replay — `abx index <addr> --full`.')}`,
|
|
7227
|
+
changelog: `
|
|
7228
|
+
${bold('abx changelog')} ${dim('— what changed in this and recent releases. Reads nothing from the network. No tx.')}
|
|
7229
|
+
${g('--all')} the full history (default: the 3 most recent releases)
|
|
7230
|
+
${dim('The notes ship INSIDE the package, so they always match the version you have and work offline.')}
|
|
7231
|
+
${dim('Every published version is listed at https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions')}`,
|
|
6517
7232
|
};
|
|
6518
7233
|
function printCommandHelp(cmd) {
|
|
6519
7234
|
if (cmd && COMMAND_HELP[cmd]) {
|
|
@@ -6556,6 +7271,7 @@ function help() {
|
|
|
6556
7271
|
${g('abx verify')} <addr> re-hash served bytes vs the on-chain commitment (no server needed)
|
|
6557
7272
|
${g('abx tokenuri')} <addr> read tokenURI(0) on-chain + decode the JSON (proof a self-resolving token works)
|
|
6558
7273
|
${g('abx contracturi')} <addr> read contractURI() (ERC-7572 collection metadata) on-chain, follow it, decode — never hand-build the URL
|
|
7274
|
+
${g('abx tokens')} <addr> EVERY token's owner + seed + params, from chain flags: [--json] [--from <id>] [--limit <n>]
|
|
6559
7275
|
${g('abx state')} <addr> one-glance on-chain snapshot: owner · supply · paused · minter · payee · royalty · renderer
|
|
6560
7276
|
${g('abx serve')} [--port ..] serve the token API + dashboard — and WATCH the chain: auto-index every registered
|
|
6561
7277
|
project + notify the effects layer on change (${g('ABX_WATCH_INTERVAL_MS')}; 0 = off)
|
|
@@ -6608,6 +7324,7 @@ function help() {
|
|
|
6608
7324
|
${dim('storage is stateless — no config file. Pick a backend PER COMMAND with flags')}
|
|
6609
7325
|
${dim('(--backend ipfs --gateway … · --backend cloud --bucket … --public-base … · --backend arweave), or set defaults in .env.')}
|
|
6610
7326
|
${g('abx storage')} upload <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend ipfs|arweave|cloud] [--dry-run]
|
|
7327
|
+
${g('abx storage')} status <locator> is that locator RETRIEVABLE yet (not just accepted)? — ready | propagating | unreachable [--json]
|
|
6611
7328
|
${g('abx storage')} balance show Turbo (arweave) upload credits + the funded address
|
|
6612
7329
|
${g('abx storage')} topup buy Turbo credits by card --usd <n> (one-time; <100 KB is always free)
|
|
6613
7330
|
${g('abx storage')} backup-key copy the managed Turbo/Arweave key (holds credits) to a safe path --out <path>
|
|
@@ -6616,6 +7333,7 @@ function help() {
|
|
|
6616
7333
|
${g('abx doctor')} check environment (key, RPC, balance, factory, storage)
|
|
6617
7334
|
${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
|
|
6618
7335
|
${g('abx version')} print the installed CLI version
|
|
7336
|
+
${g('abx changelog')} what changed in this and recent releases (ships with the CLI; offline) [--all]
|
|
6619
7337
|
|
|
6620
7338
|
${dim('Run')} ${g('abx <command> --help')} ${dim('for per-command usage. --help / -h never executes — it only prints usage.')}
|
|
6621
7339
|
${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('.')}
|
|
@@ -6709,6 +7427,40 @@ function resolveBundledSkill() {
|
|
|
6709
7427
|
return canonical;
|
|
6710
7428
|
return null;
|
|
6711
7429
|
}
|
|
7430
|
+
/**
|
|
7431
|
+
* `abx changelog [--all]` — print the release notes that ship with THIS install.
|
|
7432
|
+
*
|
|
7433
|
+
* The update banner used to point at `github.com/ArtBlocks/abx/releases`, which 404s for anyone
|
|
7434
|
+
* outside the org, and no changelog shipped in the package — so "what changed?" was unanswerable
|
|
7435
|
+
* without diffing two versions' dry-run output by hand. `CHANGELOG.md` now ships (package.json
|
|
7436
|
+
* `files`), which makes the notes offline, version-matched, and readable by an agent.
|
|
7437
|
+
*
|
|
7438
|
+
* Default: the most recent few entries (what an upgrade needs). `--all`: the whole file.
|
|
7439
|
+
*/
|
|
7440
|
+
function cmdChangelog(flags) {
|
|
7441
|
+
const pkgDir = resolvePath(fileURLToPath(import.meta.url), '..', '..');
|
|
7442
|
+
const candidates = [joinPath(pkgDir, 'CHANGELOG.md')];
|
|
7443
|
+
const root = findRepoRoot();
|
|
7444
|
+
if (root)
|
|
7445
|
+
candidates.push(joinPath(root, 'packages', 'cli', 'CHANGELOG.md')); // dev checkout
|
|
7446
|
+
const path = candidates.find((p) => existsSync(p));
|
|
7447
|
+
if (!path) {
|
|
7448
|
+
throw new Error(`no CHANGELOG.md found beside this install (looked in ${candidates.join(', ')}). ` +
|
|
7449
|
+
'Version history: https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions');
|
|
7450
|
+
}
|
|
7451
|
+
const text = readFileSync(path, 'utf8');
|
|
7452
|
+
if (flags.all !== undefined) {
|
|
7453
|
+
console.log(text);
|
|
7454
|
+
return;
|
|
7455
|
+
}
|
|
7456
|
+
// Entries are `## <version>` sections; show the newest few and say how to see the rest.
|
|
7457
|
+
const lines = text.split('\n');
|
|
7458
|
+
const heads = lines.map((l, i) => (/^## /.test(l) ? i : -1)).filter((i) => i >= 0);
|
|
7459
|
+
const end = heads.length > 3 ? heads[3] : lines.length;
|
|
7460
|
+
console.log(`\n${lines.slice(0, end).join('\n').trimEnd()}\n`);
|
|
7461
|
+
if (heads.length > 3)
|
|
7462
|
+
info(`showing the ${3} most recent releases — ${bold('abx changelog --all')} for the full history ${dim(`(${path})`)}`);
|
|
7463
|
+
}
|
|
6712
7464
|
/** Locate the in-chain renderer Foundry scaffold: bundled beside the CLI (published), else the
|
|
6713
7465
|
* repo copy (dev). Same layout in both — it lives under the CLI package's `assets/`. */
|
|
6714
7466
|
function resolveRendererScaffold() {
|
|
@@ -6737,12 +7489,9 @@ function cmdScaffoldRenderer(rest, flags) {
|
|
|
6737
7489
|
if (existsSync(dir) && readdirSync(dir).length > 0 && flags.force === undefined) {
|
|
6738
7490
|
throw new Error(`${dir} already exists and is not empty — pass a fresh path, or --force to write into it.`);
|
|
6739
7491
|
}
|
|
6740
|
-
|
|
6741
|
-
//
|
|
6742
|
-
|
|
6743
|
-
recursive: true,
|
|
6744
|
-
filter: (s) => !/(^|\/)(out|cache|dependencies|broadcast|node_modules)(\/|$)/.test(s),
|
|
6745
|
-
});
|
|
7492
|
+
// Copy + assert it landed. Throws rather than printing a success banner over an empty directory —
|
|
7493
|
+
// the alpha.9→alpha.14 failure mode. See src/scaffold.ts for why this is not inline.
|
|
7494
|
+
copyRendererScaffold(src, dir);
|
|
6746
7495
|
ok(`in-chain renderer scaffold → ${dir}`);
|
|
6747
7496
|
step('The in-chain Solidity art lane — nothing to run after deploy, tokenURI resolves from chain forever');
|
|
6748
7497
|
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).`);
|