@artblocks/abx-cli 0.1.0-alpha.40 → 0.1.0-alpha.42
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 +222 -0
- package/assets/renderer-scaffold/README.md +12 -0
- package/assets/renderer-scaffold/foundry.toml +4 -0
- package/assets/renderer-scaffold/script/Preview.s.sol +99 -0
- package/dist/capabilities.d.ts +9 -4
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +13 -4
- package/dist/capabilities.js.map +1 -1
- package/dist/commands/deploy.d.ts +51 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1008 -172
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/maintenance.d.ts +4 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +114 -0
- package/dist/commands/maintenance.js.map +1 -0
- package/dist/commands/project.d.ts +175 -3
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +359 -29
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts +21 -0
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +171 -4
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +5 -0
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/service.d.ts.map +1 -1
- package/dist/commands/service.js +7 -0
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +32 -3
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/submit-app.d.ts +43 -0
- package/dist/commands/submit-app.d.ts.map +1 -1
- package/dist/commands/submit-app.js +115 -6
- package/dist/commands/submit-app.js.map +1 -1
- package/dist/config.d.ts +16 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +73 -7
- package/dist/config.js.map +1 -1
- package/dist/deploy-plan.d.ts +221 -0
- package/dist/deploy-plan.d.ts.map +1 -0
- package/dist/deploy-plan.js +20 -0
- package/dist/deploy-plan.js.map +1 -0
- package/dist/flag-allowlists.d.ts.map +1 -1
- package/dist/flag-allowlists.js +5 -1
- package/dist/flag-allowlists.js.map +1 -1
- package/dist/flags.d.ts +6 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +14 -0
- package/dist/flags.js.map +1 -1
- package/dist/jsonout.d.ts.map +1 -1
- package/dist/jsonout.js +11 -0
- package/dist/jsonout.js.map +1 -1
- package/dist/main.js +62 -13
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +139 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +382 -19
- package/dist/ownerops.js.map +1 -1
- package/dist/riskgate.d.ts +36 -0
- package/dist/riskgate.d.ts.map +1 -1
- package/dist/riskgate.js +91 -17
- package/dist/riskgate.js.map +1 -1
- package/dist/script-chunks.d.ts +1 -1
- package/dist/script-chunks.d.ts.map +1 -1
- package/dist/script-chunks.js +2 -2
- package/dist/script-chunks.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +6 -4
- package/skill/reference/code.md +31 -8
- package/skill/reference/deploy.md +9 -2
- package/skill/reference/diagnose.md +15 -2
- package/skill/reference/hosting.md +26 -0
- package/skill/reference/operate.md +55 -4
- package/skill/reference/setup.md +14 -0
package/dist/commands/project.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { resolve as resolvePath } from 'node:path';
|
|
9
|
-
import { verifyProvenance, AUTH_OPTIONS, CREATOR_TOKEN_INTERFACE_ID, PARAM_TYPES, hasOnChainUriLane, isCodeProject, isCurrentFactory, isCurrentOneOfOneEditionFactory, makePublicClient, makeWalletClient, normalizeAttributes, onChainUriReport, parseTraitPairs, predictClone, readParamHooks, readParamHooksLocked, readParamSchema, readParamSchemaKeys, readSetParamKeys, resolveChain, resolveSeriesCodeFactory, saltFor, saltGuard, sleep, readCollectionPolicy, rpcEnvVar, tryReadContract, } from '@artblocks/abx-sdk';
|
|
9
|
+
import { verifyProvenance, AUTH_OPTIONS, CREATOR_TOKEN_INTERFACE_ID, PARAM_TYPES, hasOnChainUriLane, isCodeProject, isCurrentFactory, isCurrentOneOfOneEditionFactory, makePublicClient, makeWalletClient, normalizeAttributes, onChainUriReport, parseTraitPairs, predictClone, readParamHooks, readParamHooksLocked, readParamSchema, readParamSchemaKeys, readSetParamKeys, resolveChain, resolveSeriesCodeFactory, saltFor, saltGuard, sleep, readCollectionPolicy, rpcEnvVar, tryReadContract, encodeTag, METADATA_FIELD, METADATA_REPRESENTATION, fieldOf, isCurrentRenderer, isCurrentGenerator, } from '@artblocks/abx-sdk';
|
|
10
10
|
import { resolveBackend } from '@artblocks/abx-storage';
|
|
11
11
|
import { currentRenderArtifact, verifyProject } from '@artblocks/abx-token-api';
|
|
12
12
|
import { zeroAddress } from 'viem';
|
|
13
13
|
import { CHAIN, activeBackendId, factoryAddress, localIndexer, remoteLocators, seriesFactoryAddress, oneOfOneEditionFactoryAddress, editionFactoryAddress, editionCodeFactoryAddress, storageOptions, } from '../config.js';
|
|
14
|
-
import { isDryRun, parseSaltFlag } from '../flags.js';
|
|
14
|
+
import { isDryRun, parseBlockTagFlag, parseSaltFlag } from '../flags.js';
|
|
15
15
|
import { jsonSafe, withJson } from '../jsonout.js';
|
|
16
16
|
import { allowLargeScan, bold, c, detectCanonicalFactory, dim, g, info, ok, registerAndIndexLocally, resolveScanFloor, warn } from '../output.js';
|
|
17
17
|
import { parseCopies } from './deploy.js';
|
|
@@ -239,14 +239,20 @@ export async function cmdIndex(address, flags) {
|
|
|
239
239
|
}
|
|
240
240
|
allowLargeScan(flags);
|
|
241
241
|
const full = !!flags.full; // force a full replay from the deploy block (durability proof)
|
|
242
|
+
// Stop the scan at a REORG-SAFE boundary instead of the head. `latest` (the default) can include
|
|
243
|
+
// blocks that a reorg later replaces, which would persist a watermark for history that no longer
|
|
244
|
+
// exists; `safe`/`finalized` trade freshness for a boundary the chain won't take back. Resolved to
|
|
245
|
+
// a concrete block number before scanning (see resolveBlockTag) — a stored watermark is always a
|
|
246
|
+
// number, never a tag.
|
|
247
|
+
const blockTag = parseBlockTagFlag(flags['to-block']);
|
|
242
248
|
const indexer = localIndexer();
|
|
243
249
|
const line = (state, elapsedMs, mode) => ok(`${state.name ?? state.address}: ${state.eventCount} events in ${elapsedMs}ms ${dim(`(${mode})`)}`);
|
|
244
250
|
if (address) {
|
|
245
|
-
const { state, elapsedMs, mode } = await indexer.reindex(address, { full });
|
|
251
|
+
const { state, elapsedMs, mode } = await indexer.reindex(address, { full, blockTag });
|
|
246
252
|
line(state, elapsedMs, mode);
|
|
247
253
|
return;
|
|
248
254
|
}
|
|
249
|
-
const results = await indexer.reindexAll({ full });
|
|
255
|
+
const results = await indexer.reindexAll({ full, blockTag });
|
|
250
256
|
if (results.length === 0) {
|
|
251
257
|
console.log(dim('No registered projects. Deploy one with `abx deploy`.'));
|
|
252
258
|
return;
|
|
@@ -266,21 +272,96 @@ export async function cmdVerify(address, flags) {
|
|
|
266
272
|
}
|
|
267
273
|
allowLargeScan(flags);
|
|
268
274
|
const remote = remoteFlag(flags);
|
|
269
|
-
if (remote)
|
|
270
|
-
return cmdVerifyRemote(address, remote);
|
|
271
275
|
// `--json` matters most here of all the read commands: verify already exits non-zero on a byte
|
|
272
276
|
// mismatch, so it is the one command a CI job would gate on. The payload's `ok` is the same
|
|
273
277
|
// boolean the exit code carries; callers never need to parse prose.
|
|
278
|
+
//
|
|
279
|
+
// The remote lane used to route around `withJson` entirely (`return cmdVerifyRemote(address,
|
|
280
|
+
// remote)`), so `abx verify <addr> --remote <r> --json` printed the same human prose as without
|
|
281
|
+
// the flag and emitted NOTHING on stdout — the one lane of this command a CI job is most likely
|
|
282
|
+
// to run (checking what a HOSTED resolver serves) had no machine-readable output at all. Both
|
|
283
|
+
// lanes now go through the same `withJson` wrapper and emit the same payload shape.
|
|
284
|
+
if (remote)
|
|
285
|
+
return withJson(flags, async (emit) => cmdVerifyRemote(address, remote, emit));
|
|
274
286
|
return withJson(flags, async (emit) => cmdVerifyBody(address, flags, emit));
|
|
275
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Pure by design — every caller (`cmdVerifyBody`'s local lane and `cmdVerifyRemote`'s hosted lane)
|
|
290
|
+
* feeds it facts it already gathered rather than each re-deriving its own verdict, so the two
|
|
291
|
+
* lanes' JSON payloads read the same word for the same situation.
|
|
292
|
+
*
|
|
293
|
+
* `isCode` selects which signal decides the verdict: a code project's render presence
|
|
294
|
+
* (`minted`/`present`), or — for everything else — whether a byte-check actually ran
|
|
295
|
+
* (`anyCheck`) versus a locator this run could not independently re-hash (`unrecomputablePointers`).
|
|
296
|
+
* The two signals are never combined into one number on purpose: "3 of 5 renders present" and "2
|
|
297
|
+
* locator commitments unconfirmed" are different facts about different projects, and mixing them
|
|
298
|
+
* would produce a verdict that answers neither question honestly.
|
|
299
|
+
*/
|
|
300
|
+
export function computeAvailability(opts) {
|
|
301
|
+
if (opts.isCode) {
|
|
302
|
+
if (opts.minted === 0)
|
|
303
|
+
return { status: 'unknown', note: 'no tokens minted yet — nothing to render' };
|
|
304
|
+
if (opts.present === opts.minted)
|
|
305
|
+
return { status: 'available', note: `${opts.present}/${opts.minted} minted token(s) have a real render` };
|
|
306
|
+
if (opts.present === 0)
|
|
307
|
+
return { status: 'unavailable', note: `0/${opts.minted} minted token(s) have a real render — every one is still a placeholder` };
|
|
308
|
+
return { status: 'partial', note: `${opts.present}/${opts.minted} minted token(s) have a real render` };
|
|
309
|
+
}
|
|
310
|
+
if (opts.anyCheck) {
|
|
311
|
+
return { status: 'available', note: 'served bytes were fetched and compared against the on-chain commitment (see `ok` for whether they matched)' };
|
|
312
|
+
}
|
|
313
|
+
if (opts.unrecomputablePointers > 0) {
|
|
314
|
+
return {
|
|
315
|
+
status: 'unknown',
|
|
316
|
+
note: `${opts.unrecomputablePointers} locator commitment(s) (ipfs/arweave/url) exist but were not independently re-fetched by this command`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return { status: 'available', note: 'no content commitments on this project — nothing to serve' };
|
|
320
|
+
}
|
|
321
|
+
/** Locator representations `verifyProject` never emits a check for (only `keccak256`/`sha256`
|
|
322
|
+
* fields get one) — a real commitment this command cannot re-hash from the field alone: ipfs/
|
|
323
|
+
* arweave carry a content-addressed locator this run doesn't fetch, and url/url-template carry
|
|
324
|
+
* no hash at all. Reported as a `verified: null` entry, same tri-state slot a hash check uses,
|
|
325
|
+
* so a caller sees ONE consistent shape rather than silence for these tokens. */
|
|
326
|
+
const POINTER_REPRESENTATIONS = new Set([
|
|
327
|
+
METADATA_REPRESENTATION.ipfs,
|
|
328
|
+
METADATA_REPRESENTATION.arweave,
|
|
329
|
+
METADATA_REPRESENTATION.url,
|
|
330
|
+
METADATA_REPRESENTATION.urlTemplate,
|
|
331
|
+
]);
|
|
332
|
+
/**
|
|
333
|
+
* Does this token's `image` field carry a locator commitment `verifyProject` never produced a
|
|
334
|
+
* check for? Exported and pure (a plain `fields` array in, a verdict out — no chain, no indexer)
|
|
335
|
+
* so #112's three-way distinction is unit-testable directly, rather than only reachable through a
|
|
336
|
+
* full `cmdVerifyBody` run against real chain state.
|
|
337
|
+
*
|
|
338
|
+
* Returns the representation name to report (`ipfs`/`arweave`/`url`/`url-template`) when the image
|
|
339
|
+
* field is a bare locator, else `null` — including when there's no `image` field at all, which is
|
|
340
|
+
* "no commitment", a different fact from "an unrecomputable one".
|
|
341
|
+
*/
|
|
342
|
+
export function pointerOnlyImageCheck(fields) {
|
|
343
|
+
const image = fieldOf(fields, METADATA_FIELD.image);
|
|
344
|
+
if (image && POINTER_REPRESENTATIONS.has(image.representation))
|
|
345
|
+
return image.representation;
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
276
348
|
export async function cmdVerifyBody(address, flags, emit) {
|
|
277
349
|
const indexer = localIndexer();
|
|
278
350
|
let state = indexer.getProject(address);
|
|
351
|
+
// FAIL FAST, before any scan. `abx verify`'s own help already admits it needs the project
|
|
352
|
+
// registered on THIS node — this is that check enforced, not just documented. Without it, a
|
|
353
|
+
// caller who follows the previous version of this message's advice literally (`abx add <addr>`
|
|
354
|
+
// with no thought to whether the project is actually served by a REMOTE resolver instead) walks
|
|
355
|
+
// straight into a full historical log scan on a public RPC — exactly what a cold-agent sweep hit
|
|
356
|
+
// (repeated HTTP 429s, minutes wasted, reads like an outage). Naming `--remote <name>` here as a
|
|
357
|
+
// real alternative is the fix: it steers a resolver-served project away from a needless local
|
|
358
|
+
// backfill instead of just failing fast into the same trap one command later.
|
|
279
359
|
if (indexer.store.getRegistration(address)) {
|
|
280
360
|
({ state } = await indexer.reindex(address)); // freshest state from chain
|
|
281
361
|
}
|
|
282
362
|
else if (!state) {
|
|
283
|
-
throw new Error(`${address} isn't
|
|
363
|
+
throw new Error(`${address} isn't registered on this node — verify checks a local projection, not the chain directly, and never scans until this check passes. ` +
|
|
364
|
+
`Run \`abx add ${address}\` to index it here, or if a resolver already serves it, check that instead: \`abx verify ${address} --remote <name>\`.`);
|
|
284
365
|
}
|
|
285
366
|
if (!state)
|
|
286
367
|
throw new Error(`could not load state for ${address}`);
|
|
@@ -295,7 +376,13 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
295
376
|
contentIntegrity: 'no-commitments',
|
|
296
377
|
renders: null,
|
|
297
378
|
onChainUri: null,
|
|
298
|
-
|
|
379
|
+
// Fail closed for the same reason as the remote lane below: this object is emitted early and
|
|
380
|
+
// printed in its FINAL state, so a run that dies partway must not leave `ok: true` standing.
|
|
381
|
+
// The verdict is assigned unconditionally further down, before the second `emit`.
|
|
382
|
+
ok: false,
|
|
383
|
+
// A SIBLING of `ok`, not a rename — see `computeAvailability`'s doc comment for why the two
|
|
384
|
+
// must never collapse into one bit. Filled in below, once the facts it's computed from exist.
|
|
385
|
+
availability: null,
|
|
299
386
|
};
|
|
300
387
|
emit(verifyReport);
|
|
301
388
|
console.log(bold(`\n verify ${state.name ?? address}`));
|
|
@@ -309,6 +396,12 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
309
396
|
const tokens = result.tokens ?? [];
|
|
310
397
|
let anyCheck = false;
|
|
311
398
|
let allGood = true;
|
|
399
|
+
// Locator commitments (ipfs/arweave/url/url-template `image` fields) `verifyProject` never
|
|
400
|
+
// produces a check for — see `POINTER_REPRESENTATIONS`'s doc comment. Counted SEPARATELY from
|
|
401
|
+
// `anyCheck`/`allGood` on purpose: a pointer this command didn't re-fetch is not an integrity
|
|
402
|
+
// verdict either way, so it must never move `contentIntegrity`/`ok` — it feeds `availability`
|
|
403
|
+
// instead, below.
|
|
404
|
+
let unrecomputablePointers = 0;
|
|
312
405
|
for (const t of tokens) {
|
|
313
406
|
const tok = state.tokens.find((s) => s.tokenId === t.tokenId);
|
|
314
407
|
const life = tok?.lifecycle === 'live'
|
|
@@ -339,8 +432,27 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
339
432
|
else
|
|
340
433
|
info(`${ch.kind} — pointer-only, not locally recomputable`);
|
|
341
434
|
}
|
|
435
|
+
// `verifyProject` only ever emits a check for a keccak256/sha256-committed `image` field, so a
|
|
436
|
+
// token whose image is a bare LOCATOR (ipfs/arweave/url/url-template) gets `t.checks === []` —
|
|
437
|
+
// indistinguishable, before this, from "no commitment at all". That was the real gap #112
|
|
438
|
+
// named: a real, independently-lockable commitment silently read as nothing. Surface it as the
|
|
439
|
+
// same tri-state slot a hash check uses (`verified: null`), so a caller sees one consistent
|
|
440
|
+
// shape instead of two different kinds of silence.
|
|
441
|
+
if (t.checks.length === 0 && tok) {
|
|
442
|
+
const pointerKind = pointerOnlyImageCheck(tok.fields);
|
|
443
|
+
if (pointerKind) {
|
|
444
|
+
unrecomputablePointers++;
|
|
445
|
+
verifyReport.contentChecks.push({
|
|
446
|
+
tokenId: t.tokenId,
|
|
447
|
+
lifecycle: tok.lifecycle,
|
|
448
|
+
kind: pointerKind,
|
|
449
|
+
verified: null,
|
|
450
|
+
});
|
|
451
|
+
info(`${pointerKind} — pointer-only, not locally recomputable (no outbound fetch from this command)`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
342
454
|
}
|
|
343
|
-
if (!anyCheck && !isCodeProject(state))
|
|
455
|
+
if (!anyCheck && unrecomputablePointers === 0 && !isCodeProject(state))
|
|
344
456
|
info('no content commitments on this project');
|
|
345
457
|
verifyReport.contentIntegrity = !anyCheck ? 'no-commitments' : allGood ? 'ok' : 'mismatch';
|
|
346
458
|
verifyReport.ok = !anyCheck || allGood;
|
|
@@ -351,6 +463,9 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
351
463
|
// placeholder? Same seam the /…/image route serves from (currentRenderArtifact @ current
|
|
352
464
|
// inputsHash) — so this answers "did the render land?" with one command, right after token 0.
|
|
353
465
|
let renderGap = false;
|
|
466
|
+
// Hoisted above the `isCodeProject` branch: `availability` (computed right after it, whether or
|
|
467
|
+
// not this project turns out to be a code project) needs the render counts that branch produces.
|
|
468
|
+
let codeRenders = null;
|
|
354
469
|
if (isCodeProject(state)) {
|
|
355
470
|
const client = makePublicClient({ chainKey: CHAIN });
|
|
356
471
|
const storageForRender = resolveBackend(storageOptions());
|
|
@@ -439,6 +554,7 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
439
554
|
// there, not here, so `missing` is a false negative for a hosted drop — hence the scope.
|
|
440
555
|
scope: "this node's store",
|
|
441
556
|
};
|
|
557
|
+
codeRenders = { minted: minted.length, present };
|
|
442
558
|
emit(verifyReport);
|
|
443
559
|
if (present)
|
|
444
560
|
ok(`${present}/${minted.length} minted token(s): real render present (in this node's store)`);
|
|
@@ -450,6 +566,16 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
450
566
|
console.log(` ${dim('else render them:')} ${bold(`abx render ${address}`)} ${dim('(once) ·')} ${bold('abx effects')} ${dim('(continuous)')}`);
|
|
451
567
|
}
|
|
452
568
|
}
|
|
569
|
+
// The availability verdict — computed here, after both the byte-integrity pass and the render
|
|
570
|
+
// pass above have run, from facts they already gathered (no new reads). See `computeAvailability`.
|
|
571
|
+
verifyReport.availability = jsonSafe(computeAvailability({
|
|
572
|
+
isCode: isCodeProject(state),
|
|
573
|
+
minted: codeRenders?.minted ?? 0,
|
|
574
|
+
present: codeRenders?.present ?? 0,
|
|
575
|
+
anyCheck,
|
|
576
|
+
unrecomputablePointers,
|
|
577
|
+
}));
|
|
578
|
+
emit(verifyReport);
|
|
453
579
|
// The on-chain URI lane (a non-zero tokenURIRenderer, or an animation field pointing at the
|
|
454
580
|
// generator): read the generator's honesty surface — onChainStatus (branch · chain-complete ·
|
|
455
581
|
// unresolved refs · URL budget) — then decode tokenURI straight from the contract and report
|
|
@@ -465,9 +591,23 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
465
591
|
try {
|
|
466
592
|
const report = await onChainUriReport(client, state, flags.generator);
|
|
467
593
|
const { status, probe } = report;
|
|
594
|
+
// #128: the factory-generation half of "is this pinned to a superseded deployment" already
|
|
595
|
+
// ships (ANCHOR_GENERATIONS + verifyCanonical, in the `canonical`/`abxGeneration` line above);
|
|
596
|
+
// this is the SINGLETON half — these two pointers were already read and printed here, just
|
|
597
|
+
// never compared. Binary by design (see `isCurrentGenerator`'s doc): current / not-current,
|
|
598
|
+
// never "superseded-but-known", because neither manifest keeps a prior address to recognize a
|
|
599
|
+
// mismatch against. A `false` here is NOT a defect report — an older-but-working renderer or
|
|
600
|
+
// generator still serves; it just isn't what a fresh deploy would get today.
|
|
601
|
+
const generatorCurrent = isCurrentGenerator(state.chainId, report.generator);
|
|
602
|
+
const rendererCurrent = state.tokenURIRenderer ? await isCurrentRenderer(client, state.tokenURIRenderer) : null;
|
|
468
603
|
verifyReport.onChainUri = jsonSafe({
|
|
469
604
|
generator: report.generator,
|
|
605
|
+
generatorCurrent, // true | false | null (no canonical generator recorded for this chain)
|
|
470
606
|
tokenURIRenderer: state.tokenURIRenderer ?? null,
|
|
607
|
+
// null ⇒ no renderer set at all (nothing to compare) — distinct from `false` (set, but not
|
|
608
|
+
// spec v11). See `isCurrentRenderer`: a read failure ALSO reports `false`, same as a stale
|
|
609
|
+
// renderer — the underlying probe cannot tell those apart, so neither can this field.
|
|
610
|
+
tokenURIRendererCurrent: rendererCurrent,
|
|
471
611
|
branch: status.branchName,
|
|
472
612
|
chainComplete: !!status.chainComplete,
|
|
473
613
|
unresolvedRefs: [...status.unresolvedRefs],
|
|
@@ -478,6 +618,16 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
478
618
|
});
|
|
479
619
|
emit(verifyReport);
|
|
480
620
|
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)'}`);
|
|
621
|
+
if (generatorCurrent === true)
|
|
622
|
+
ok(`generator is the CURRENT canonical one for ${CHAIN}`);
|
|
623
|
+
else if (generatorCurrent === false) {
|
|
624
|
+
info(`generator is NOT the current canonical one for ${CHAIN} ${dim('(a prior deployment this project is pinned to, or a fully custom field renderer — it still works; repoint with')} ${bold(`abx set-field ${address} --field animation_url --representation renderer --value <generator> --collection`)}${dim(')')}`);
|
|
625
|
+
} // null: no canonical generator recorded for this chain at all — nothing to compare against, so say nothing rather than a false "NOT current".
|
|
626
|
+
if (rendererCurrent === true)
|
|
627
|
+
ok(`tokenURI renderer is CURRENT (spec v11)`);
|
|
628
|
+
else if (rendererCurrent === false) {
|
|
629
|
+
info(`tokenURI renderer is NOT current (spec v11) ${dim(`— an older renderer still serves fine; it just doesn't have v11's capabilities (see the renderer's changelog in deployments.ts). Repoint with`)} ${bold(`abx set-renderer ${address} --collection`)}${dim(' (and without --collection for the per-token pointer)')}`);
|
|
630
|
+
}
|
|
481
631
|
if (status.branchName === 'template') {
|
|
482
632
|
ok(`generator branch: template — the document assembles from the on-chain script chunks`);
|
|
483
633
|
if (status.chainComplete) {
|
|
@@ -569,9 +719,32 @@ export async function cmdVerifyBody(address, flags, emit) {
|
|
|
569
719
|
// `verify` checks THIS machine's store/backend, the wrong store for a hosted drop). Probes the real
|
|
570
720
|
// `/…/image` route, so it accounts for the locator bridge (a 302 to ipfs/ar) exactly as a marketplace
|
|
571
721
|
// sees it — the truthful "did the thumbnail land?" check after a remote render.
|
|
572
|
-
export async function cmdVerifyRemote(address, remote) {
|
|
722
|
+
export async function cmdVerifyRemote(address, remote, emit) {
|
|
573
723
|
const base = remote.url.replace(/\/$/, '');
|
|
574
724
|
const chainId = resolveChain(CHAIN).id;
|
|
725
|
+
// Same shape, same field names as the local lane's `verifyReport` (cmdVerifyBody) wherever the
|
|
726
|
+
// two lanes answer the same question, so a caller doesn't need a second parser for `--remote`.
|
|
727
|
+
// `--remote --json` used to emit NOTHING at all — this command routed around `withJson` entirely
|
|
728
|
+
// (see `cmdVerify`) — so every field here is new, not a rename of something that already worked.
|
|
729
|
+
const verifyReport = {
|
|
730
|
+
address,
|
|
731
|
+
remote: base,
|
|
732
|
+
name: null,
|
|
733
|
+
watching: null,
|
|
734
|
+
contentChecks: [],
|
|
735
|
+
contentIntegrity: 'not-checked',
|
|
736
|
+
renders: null,
|
|
737
|
+
availability: null,
|
|
738
|
+
// FAIL CLOSED. `emit` registers this object and `withJson` prints its FINAL state, so every
|
|
739
|
+
// terminal path must earn its verdict — and a path that throws (a down endpoint, a 404 from a
|
|
740
|
+
// resolver that doesn't serve this project) never reaches one. This used to initialize `true`,
|
|
741
|
+
// which meant `verify --remote --json` on an unregistered project printed `"ok": true` on
|
|
742
|
+
// stdout while exiting 1 and saying "register it first" on stderr. `ok` is documented as the
|
|
743
|
+
// field CI gates on, so an un-earned `true` is the one wrong answer this payload can give:
|
|
744
|
+
// "we could not check" is not "it is fine". Every early return below sets it explicitly.
|
|
745
|
+
ok: false,
|
|
746
|
+
};
|
|
747
|
+
emit(verifyReport);
|
|
575
748
|
console.log(bold(`\n verify ${address} ${dim(`(remote → ${base})`)}`));
|
|
576
749
|
// A DOWN endpoint and a wrong-address endpoint are different problems with different fixes, and a
|
|
577
750
|
// bare `fetch failed` says neither. Match `abx status --remote` so both commands describe the same
|
|
@@ -587,6 +760,7 @@ export async function cmdVerifyRemote(address, remote) {
|
|
|
587
760
|
throw new Error(`resolver ${base} doesn't serve ${address} (HTTP ${stateRes.status}) — register it first: abx add ${address} --remote ${remote.name?.toLowerCase() ?? base}`);
|
|
588
761
|
}
|
|
589
762
|
const state = (await stateRes.json());
|
|
763
|
+
verifyReport.name = state.name ?? null;
|
|
590
764
|
info(`serving as "${state.name ?? address}"`);
|
|
591
765
|
// Is the resolver actively WATCHING the chain? Prove it from /api/watch (the meta the watcher
|
|
592
766
|
// stamps each tick) so a hosted operator who can't tail the log still sees liveness — and catches
|
|
@@ -594,6 +768,8 @@ export async function cmdVerifyRemote(address, remote) {
|
|
|
594
768
|
const watch = (await fetch(`${base}/api/watch`)
|
|
595
769
|
.then((r) => (r.ok ? r.json() : null))
|
|
596
770
|
.catch(() => null));
|
|
771
|
+
verifyReport.watching = watch?.watching ?? null;
|
|
772
|
+
emit(verifyReport);
|
|
597
773
|
if (watch?.watching) {
|
|
598
774
|
const heads = Object.entries(watch.chains).map(([ck, s]) => `${ck} @ block ${s.head}`).join(', ');
|
|
599
775
|
const ageS = watch.pollAt ? Math.round((Date.now() - Date.parse(watch.pollAt)) / 1000) : null;
|
|
@@ -610,6 +786,13 @@ export async function cmdVerifyRemote(address, remote) {
|
|
|
610
786
|
const minted = (state.tokens ?? []).filter((t) => t.lifecycle === 'live');
|
|
611
787
|
if (minted.length === 0) {
|
|
612
788
|
info('no tokens minted yet — mint token #0, then re-run.');
|
|
789
|
+
verifyReport.availability = jsonSafe({ status: 'unknown', note: 'no tokens minted yet' });
|
|
790
|
+
// Nothing committed yet is not a FAILURE — there is simply nothing to hash, and this path exits
|
|
791
|
+
// 0. Said explicitly because the initializer is now fail-closed: an un-minted project is the one
|
|
792
|
+
// early return that legitimately earns `ok`.
|
|
793
|
+
verifyReport.contentIntegrity = 'no-commitments';
|
|
794
|
+
verifyReport.ok = true;
|
|
795
|
+
emit(verifyReport);
|
|
613
796
|
console.log('');
|
|
614
797
|
return;
|
|
615
798
|
}
|
|
@@ -647,27 +830,48 @@ export async function cmdVerifyRemote(address, remote) {
|
|
|
647
830
|
else
|
|
648
831
|
ok(`renders: ${summary}`);
|
|
649
832
|
}
|
|
650
|
-
|
|
833
|
+
verifyReport.renders = jsonSafe({ minted: minted.length, upToDate, stale, rendering, failed, tokens: report.tokens });
|
|
834
|
+
verifyReport.availability = jsonSafe(computeAvailability({ isCode: report.tokens.length > 0, minted: minted.length, present: upToDate, anyCheck: false, unrecomputablePointers: 0 }));
|
|
835
|
+
emit(verifyReport);
|
|
836
|
+
const integrity = await reportRemoteByteIntegrity(address, remote, base);
|
|
837
|
+
verifyReport.contentIntegrity = integrity.status;
|
|
838
|
+
verifyReport.contentChecks = integrity.checks;
|
|
839
|
+
verifyReport.ok = integrity.status !== 'mismatch';
|
|
840
|
+
emit(verifyReport);
|
|
651
841
|
return;
|
|
652
842
|
}
|
|
653
843
|
let gap = false;
|
|
844
|
+
let present = 0;
|
|
845
|
+
const missing = [];
|
|
654
846
|
for (const t of minted) {
|
|
655
847
|
const img = await fetch(`${base}/t/${chainId}/${address}/${t.tokenId}/image`, { redirect: 'manual' });
|
|
656
848
|
const loc = img.headers.get('location');
|
|
657
849
|
const ct = img.headers.get('content-type') ?? '';
|
|
658
|
-
if (img.status >= 300 && img.status < 400 && loc)
|
|
850
|
+
if (img.status >= 300 && img.status < 400 && loc) {
|
|
851
|
+
present++;
|
|
659
852
|
ok(`token #${t.tokenId} image: real render — resolver 302s to ${loc}`);
|
|
660
|
-
|
|
853
|
+
}
|
|
854
|
+
else if (img.status === 200 && !/svg/i.test(ct)) {
|
|
855
|
+
present++;
|
|
661
856
|
ok(`token #${t.tokenId} image: real render present (${ct})`);
|
|
857
|
+
}
|
|
662
858
|
else {
|
|
663
859
|
gap = true;
|
|
860
|
+
missing.push(t.tokenId);
|
|
664
861
|
console.log(` ${c.orange}⚠${c.reset} token #${t.tokenId} image: PLACEHOLDER (${ct || 'svg'}) — run \`abx render ${address} --remote ${base}\`, or stand up the effects runner`);
|
|
665
862
|
}
|
|
666
863
|
}
|
|
667
864
|
console.log(gap
|
|
668
865
|
? ` ${dim('live view animates regardless; the placeholder only affects the static marketplace thumbnail.')}`
|
|
669
866
|
: ` ${g('✓ thumbnails are real renders')} ${dim('— served straight from the resolver.')}`);
|
|
670
|
-
|
|
867
|
+
verifyReport.renders = jsonSafe({ minted: minted.length, present, missing, scope: 'the resolver (raw image probe — no /effects route)' });
|
|
868
|
+
verifyReport.availability = jsonSafe(computeAvailability({ isCode: true, minted: minted.length, present, anyCheck: false, unrecomputablePointers: 0 }));
|
|
869
|
+
emit(verifyReport);
|
|
870
|
+
const integrity = await reportRemoteByteIntegrity(address, remote, base);
|
|
871
|
+
verifyReport.contentIntegrity = integrity.status;
|
|
872
|
+
verifyReport.contentChecks = integrity.checks;
|
|
873
|
+
verifyReport.ok = integrity.status !== 'mismatch';
|
|
874
|
+
emit(verifyReport);
|
|
671
875
|
}
|
|
672
876
|
/**
|
|
673
877
|
* The half `abx verify --remote` was missing: do the served BYTES still hash to the on-chain
|
|
@@ -683,14 +887,14 @@ export async function cmdVerifyRemote(address, remote) {
|
|
|
683
887
|
export async function reportRemoteByteIntegrity(address, remote, base) {
|
|
684
888
|
if (!remote.token) {
|
|
685
889
|
warn(`byte integrity NOT checked — that check is credentialed on the service. Set ${remote.tokenVar} (or pass --remote-token) and re-run, or run ${bold(`abx verify ${address}`)} against a node that holds the bytes.`);
|
|
686
|
-
return;
|
|
890
|
+
return { status: 'not-checked', checks: [] };
|
|
687
891
|
}
|
|
688
892
|
let report;
|
|
689
893
|
try {
|
|
690
894
|
const res = await fetch(`${base}/api/project/${address}/verify`, { headers: { authorization: `Bearer ${remote.token}` } });
|
|
691
895
|
if (res.status === 401 || res.status === 403) {
|
|
692
896
|
warn(`byte integrity NOT checked — ${base} rejected ${tokenSourceLabel(remote)} for its verify route (the read plane served fine, so this is a credential/scoping issue, not a broken project).`);
|
|
693
|
-
return;
|
|
897
|
+
return { status: 'not-checked', checks: [] };
|
|
694
898
|
}
|
|
695
899
|
if (!res.ok) {
|
|
696
900
|
// Spec'd as part of `abx-token-api/v1`, so a conforming service has it — but say it neutrally:
|
|
@@ -698,23 +902,24 @@ export async function reportRemoteByteIntegrity(address, remote, base) {
|
|
|
698
902
|
warn(`byte integrity NOT checked — ${base} serves no /api/project/…/verify route (HTTP ${res.status}). ` +
|
|
699
903
|
`Your own node? Redeploy it (\`abx deploy-resolver\`). A provider's? It's part of abx-token-api/v1 — ask them for it. ` +
|
|
700
904
|
`Meanwhile ${bold(`abx verify ${address}`)} checks the bytes on a node that holds them.`);
|
|
701
|
-
return;
|
|
905
|
+
return { status: 'not-checked', checks: [] };
|
|
702
906
|
}
|
|
703
907
|
report = (await res.json());
|
|
704
908
|
}
|
|
705
909
|
catch {
|
|
706
910
|
warn(`byte integrity NOT checked — couldn't reach ${base}'s verify route.`);
|
|
707
|
-
return;
|
|
911
|
+
return { status: 'not-checked', checks: [] };
|
|
708
912
|
}
|
|
709
913
|
const checked = (report.tokens ?? []).filter((t) => t.checks.length > 0);
|
|
710
914
|
if (checked.length === 0) {
|
|
711
915
|
info(`bytes ${dim('no on-chain byte commitment to check (this project commits no image hash)')}`);
|
|
712
|
-
return;
|
|
916
|
+
return { status: 'no-commitments', checks: [] };
|
|
713
917
|
}
|
|
918
|
+
const flatChecks = checked.flatMap((t) => t.checks.map((k) => ({ tokenId: t.tokenId, kind: k.kind, verified: k.verified })));
|
|
714
919
|
const bad = checked.filter((t) => t.checks.some((k) => !k.verified));
|
|
715
920
|
if (bad.length === 0) {
|
|
716
921
|
ok(`bytes: ${checked.length} token(s) hash-match their on-chain commitment ${dim('(what the service serves IS what the chain vouches for)')}`);
|
|
717
|
-
return;
|
|
922
|
+
return { status: 'ok', checks: flatChecks };
|
|
718
923
|
}
|
|
719
924
|
const spec = remote.name?.toLowerCase() ?? base;
|
|
720
925
|
console.log(` ${c.red}✗${c.reset} ${bold('BYTE MISMATCH')} on token(s) ${bad.map((t) => `#${t.tokenId}`).join(', ')} — ${base} does NOT serve bytes that hash to the on-chain commitment ` +
|
|
@@ -722,6 +927,7 @@ export async function reportRemoteByteIntegrity(address, remote, base) {
|
|
|
722
927
|
info(`durable bytes exist (ipfs://, ar://) but weren't bridged → ${bold(`abx add ${address} --remote ${spec}`)} forwards the locator, then re-run this.`);
|
|
723
928
|
info(`the bytes only exist on THIS machine (local fs custody) → a hosted resolver can never serve them: ${bold('abx storage upload')} to a durable backend + re-point the field, or serve the project from a node that holds them.`);
|
|
724
929
|
process.exitCode = 1; // same rule as the local lane: an integrity mismatch fails the command
|
|
930
|
+
return { status: 'mismatch', checks: flatChecks };
|
|
725
931
|
}
|
|
726
932
|
// ── state: a one-glance operational snapshot of a deployed contract, read straight from chain ──
|
|
727
933
|
// Read-only (no tx, no local index). Series-only getters revert on a 1/1, so each read is defensive
|
|
@@ -761,6 +967,16 @@ export const STATE_ABI = [
|
|
|
761
967
|
// Shown because it is the one setting an owner can re-point mid-sale that changes what a LATER
|
|
762
968
|
// buyer receives; `SeedSourceSet` puts it on the spine, and this makes it readable in one command.
|
|
763
969
|
{ type: 'function', name: 'seedSource', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
970
|
+
// The irreversible locks #122 asked `state` to report in full (previously only `paramHooksLocked`
|
|
971
|
+
// showed here). `tokenURILocked`/`contractURILocked`/`contractFieldLocked` exist on every ABX
|
|
972
|
+
// token (baseline URI + On-Chain Metadata surface); `scriptLocked`/`dependenciesLocked` exist only
|
|
973
|
+
// on the two code twins — see `readCollectionLocks`, which gates those two on `isCode` rather than
|
|
974
|
+
// trusting a revert to mean "unknown" for a contract that never had the surface at all.
|
|
975
|
+
{ type: 'function', name: 'tokenURILocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
976
|
+
{ type: 'function', name: 'contractURILocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
977
|
+
{ type: 'function', name: 'scriptLocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
978
|
+
{ type: 'function', name: 'dependenciesLocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
979
|
+
{ type: 'function', name: 'contractFieldLocked', stateMutability: 'view', inputs: [{ type: 'bytes32' }], outputs: [{ type: 'bool' }] },
|
|
764
980
|
];
|
|
765
981
|
/** The edition twin of {@link STATE_ABI} — `owner`/`maxInvocations`/`paused`/`minter`/
|
|
766
982
|
* `primaryPayee`/`tokenURIRenderer`/`royaltyInfo` are shared, unchanged, function names (see
|
|
@@ -785,6 +1001,13 @@ export const EDITION_STATE_ABI = [
|
|
|
785
1001
|
// those two getters, shared with every other integrator) — deliberately not re-declared here.
|
|
786
1002
|
// Seed Source extension — EditionCode composes it; the image editions don't. See STATE_ABI's note.
|
|
787
1003
|
{ type: 'function', name: 'seedSource', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
1004
|
+
// The same lock surface as STATE_ABI's edit — see the note there. Identical getter names on both
|
|
1005
|
+
// standards (verified against the generated ABI: every edition twin exposes the same five).
|
|
1006
|
+
{ type: 'function', name: 'tokenURILocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
1007
|
+
{ type: 'function', name: 'contractURILocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
1008
|
+
{ type: 'function', name: 'scriptLocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
1009
|
+
{ type: 'function', name: 'dependenciesLocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
1010
|
+
{ type: 'function', name: 'contractFieldLocked', stateMutability: 'view', inputs: [{ type: 'bytes32' }], outputs: [{ type: 'bool' }] },
|
|
788
1011
|
];
|
|
789
1012
|
/**
|
|
790
1013
|
* The royalty ceiling line — and the headroom, which is the part a creator has not been told.
|
|
@@ -876,6 +1099,101 @@ function warnNotCanonical(address) {
|
|
|
876
1099
|
`(a bespoke contract, a superseded factory), nothing here is broken — just make sure whoever owns the ` +
|
|
877
1100
|
`project knows they chose it.`);
|
|
878
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* The bounded field-lock scan #122's triage settled on: every field this node can enumerate is one
|
|
1104
|
+
* of the protocol registry's `METADATA_FIELD` keys (site/content/docs/protocol/metadata.mdx), read
|
|
1105
|
+
* with `contractFieldLocked`. A creator's own custom field (`stems`, `palette`, …) is a real,
|
|
1106
|
+
* independently-lockable key — but this node can only learn its NAME from the spine's
|
|
1107
|
+
* `ContractFieldLocked` event, which needs an indexer, not a bare head read. So this reports "none
|
|
1108
|
+
* of the STANDARD fields are locked", never "no fields are locked" — the caveat has to travel with
|
|
1109
|
+
* the data, not live only in a comment a caller never sees.
|
|
1110
|
+
*/
|
|
1111
|
+
async function readFieldLocks(publicClient, address, abi) {
|
|
1112
|
+
return Promise.all(Object.values(METADATA_FIELD).map(async (field) => ({
|
|
1113
|
+
field,
|
|
1114
|
+
locked: (await tryReadContract(publicClient, { address, abi, functionName: 'contractFieldLocked', args: [encodeTag(field)] })) ?? null,
|
|
1115
|
+
})));
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Read every applicable irreversible lock for one collection: token/contract URI config, the
|
|
1119
|
+
* bounded standard field set, and — for a code project — the script and dependency freezes.
|
|
1120
|
+
* `paramHooks.locked` is deliberately NOT read here: `cmdStateBody`/`cmdStateEditionBody` already
|
|
1121
|
+
* read it (`readParamHooksLocked`) as part of the existing param-hooks readout, and re-reading it
|
|
1122
|
+
* would cost a second `eth_call` for a fact already on hand — see the callers, which pass it into
|
|
1123
|
+
* {@link printLocks} instead.
|
|
1124
|
+
*
|
|
1125
|
+
* `isCode` gates `scriptLocked`/`dependenciesLocked`: those getters exist ONLY on the two code
|
|
1126
|
+
* twins (SeriesCode/EditionCode), so asking a plain image/Series contract for them isn't a failed
|
|
1127
|
+
* read to report as "unknown" — it's a surface that was never there, reported as `null` (not
|
|
1128
|
+
* applicable) via the OUTER object rather than the boolean inside it.
|
|
1129
|
+
*/
|
|
1130
|
+
export async function readCollectionLocks(publicClient, address, abi, isCode) {
|
|
1131
|
+
const read = (fn) => tryReadContract(publicClient, { address, abi, functionName: fn });
|
|
1132
|
+
const [tokenURI, contractURI, fields, script, dependencies] = await Promise.all([
|
|
1133
|
+
read('tokenURILocked'),
|
|
1134
|
+
read('contractURILocked'),
|
|
1135
|
+
readFieldLocks(publicClient, address, abi),
|
|
1136
|
+
isCode ? read('scriptLocked') : Promise.resolve(undefined),
|
|
1137
|
+
isCode ? read('dependenciesLocked') : Promise.resolve(undefined),
|
|
1138
|
+
]);
|
|
1139
|
+
return {
|
|
1140
|
+
tokenURI: tokenURI ?? null,
|
|
1141
|
+
contractURI: contractURI ?? null,
|
|
1142
|
+
fields,
|
|
1143
|
+
script: isCode ? { locked: script ?? null } : null,
|
|
1144
|
+
dependencies: isCode ? { locked: dependencies ?? null } : null,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/** One lock's human verdict — the same three words everywhere this prints, so a reader learns the
|
|
1148
|
+
* vocabulary once. */
|
|
1149
|
+
function lockVerdict(locked) {
|
|
1150
|
+
return locked === true
|
|
1151
|
+
? `${c.green}frozen${c.reset}`
|
|
1152
|
+
: locked === false
|
|
1153
|
+
? `${c.orange}unlocked${c.reset}`
|
|
1154
|
+
: dim('unknown (node refused the check)');
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Print + emit the "Locks" section shared by both `state` bodies (721 and edition) — the one
|
|
1158
|
+
* coherent summary #122 asked for, in place of a lone `paramHooks.locked` line. Every lock is its
|
|
1159
|
+
* own independent switch (freezing one does nothing to any other), which is easy to miss when each
|
|
1160
|
+
* only ever appeared alone — so the header says so explicitly, and every line names the exact
|
|
1161
|
+
* command that freezes THAT lock and nothing else.
|
|
1162
|
+
*/
|
|
1163
|
+
function printLocks(address, locks, hasParamHooks, paramHooksLock, payload, emit) {
|
|
1164
|
+
const lockedFieldNames = locks.fields.filter((f) => f.locked === true).map((f) => f.field);
|
|
1165
|
+
const unknownFieldCount = locks.fields.filter((f) => f.locked === null).length;
|
|
1166
|
+
payload.locks = jsonSafe({
|
|
1167
|
+
tokenURI: locks.tokenURI,
|
|
1168
|
+
contractURI: locks.contractURI,
|
|
1169
|
+
script: locks.script,
|
|
1170
|
+
dependencies: locks.dependencies,
|
|
1171
|
+
// `null` ⇒ no ConfigurableParams surface on this contract (not a code project) — matching the
|
|
1172
|
+
// `script`/`dependencies` null-means-N/A convention rather than reusing LockRead's "unknown".
|
|
1173
|
+
paramHooks: hasParamHooks ? (paramHooksLock ?? null) : null,
|
|
1174
|
+
fields: {
|
|
1175
|
+
// The bounded scan, keyed by name — automation gets the same three-way distinction the human
|
|
1176
|
+
// line does (true/false/null), never a collapsed "none locked".
|
|
1177
|
+
checked: locks.fields,
|
|
1178
|
+
note: "checks only the standard METADATA_FIELD set (site/content/docs/protocol/metadata.mdx); a project's own custom field keys are real and independently lockable but are NOT enumerated here — that needs an indexer over ContractFieldLocked events.",
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1181
|
+
emit(payload);
|
|
1182
|
+
console.log(`\n ${bold('Locks')} ${dim('each is INDEPENDENT — freezing one does nothing to the others')}`);
|
|
1183
|
+
info(`tokenURI ${lockVerdict(locks.tokenURI)} ${dim(`\`abx lock-uri ${address}\` freezes the pointer + renderer forever`)}`);
|
|
1184
|
+
info(`contractURI ${lockVerdict(locks.contractURI)} ${dim(`\`abx lock-uri ${address} --collection\` freezes the pointer + renderer forever`)}`);
|
|
1185
|
+
if (locks.script)
|
|
1186
|
+
info(`script ${lockVerdict(locks.script.locked)} ${dim(`\`abx lock-script ${address}\` freezes the program bytes forever`)}`);
|
|
1187
|
+
if (locks.dependencies)
|
|
1188
|
+
info(`dependencies ${lockVerdict(locks.dependencies.locked)} ${dim(`\`abx lock-dependencies ${address}\` freezes the library set forever`)}`);
|
|
1189
|
+
if (hasParamHooks)
|
|
1190
|
+
info(`param hooks ${lockVerdict(paramHooksLock ?? null)} ${dim(`\`abx lock-param-hooks ${address}\` freezes all three hook addresses forever (detail above)`)}`);
|
|
1191
|
+
info(lockedFieldNames.length
|
|
1192
|
+
? `fields ${c.green}locked${c.reset}: ${lockedFieldNames.join(', ')}`
|
|
1193
|
+
: `fields ${dim('none of the standard fields are locked')}`);
|
|
1194
|
+
info(dim(` checked the standard METADATA_FIELD set only (${locks.fields.length} keys${unknownFieldCount ? `, ${unknownFieldCount} unreadable` : ''}) — a custom field key is real and lockable but not enumerable without an indexer. ` +
|
|
1195
|
+
`\`abx lock-field ${address} --field <name> [--collection | --token 0]\` freezes one.`));
|
|
1196
|
+
}
|
|
879
1197
|
export async function cmdState(address, flags) {
|
|
880
1198
|
if (!address || address.startsWith('--')) {
|
|
881
1199
|
console.error('usage: abx state <address> — a read-only, on-chain operational snapshot\n');
|
|
@@ -1001,6 +1319,10 @@ export async function cmdStateBody(address, flags, emit) {
|
|
|
1001
1319
|
// are maintained by the contract's own write paths, so there is no list to drift. Read-only,
|
|
1002
1320
|
// best-effort, and skipped entirely for a project that has none (a 1/1, a legacy pre-enumeration
|
|
1003
1321
|
// project, or a code project with no params).
|
|
1322
|
+
// Hoisted above the try block: the Locks section (printed after it, see `printLocks`) reuses
|
|
1323
|
+
// whatever this reads rather than paying for a second `eth_call` on the same fact.
|
|
1324
|
+
let hooks = null;
|
|
1325
|
+
let paramHooksLock;
|
|
1004
1326
|
try {
|
|
1005
1327
|
const rawKeys = await readParamSchemaKeys(publicClient, address);
|
|
1006
1328
|
const enumerable = rawKeys !== null;
|
|
@@ -1054,9 +1376,9 @@ export async function cmdStateBody(address, flags, emit) {
|
|
|
1054
1376
|
// transfers and mints, so "which hooks, and are they frozen" is exactly the pair someone needs
|
|
1055
1377
|
// before buying. Printed whenever the surface exists — including with all three unset, because
|
|
1056
1378
|
// "none, and frozen" is the strongest thing this line can say and it must be visible.
|
|
1057
|
-
|
|
1379
|
+
hooks = await readParamHooks(publicClient, address);
|
|
1058
1380
|
if (hooks) {
|
|
1059
|
-
|
|
1381
|
+
paramHooksLock = await readParamHooksLocked(publicClient, address);
|
|
1060
1382
|
const shown = [['configure', hooks.configureHook], ['augment', hooks.augmentHook], ['transfer', hooks.transferHook]]
|
|
1061
1383
|
.filter(([, a]) => a !== zeroAddress)
|
|
1062
1384
|
.map(([role, a]) => `${role} ${a}`);
|
|
@@ -1066,12 +1388,12 @@ export async function cmdStateBody(address, flags, emit) {
|
|
|
1066
1388
|
transferHook: hooks.transferHook === zeroAddress ? null : hooks.transferHook,
|
|
1067
1389
|
// `null` = could not be established (see readParamHooksLocked). Never collapse it to false:
|
|
1068
1390
|
// "unknown" and "the owner can still arm a transfer veto" are different answers.
|
|
1069
|
-
locked:
|
|
1391
|
+
locked: paramHooksLock ?? null,
|
|
1070
1392
|
});
|
|
1071
1393
|
emit(payload);
|
|
1072
|
-
const lockNote =
|
|
1394
|
+
const lockNote = paramHooksLock === true
|
|
1073
1395
|
? `${c.green}frozen${c.reset} ${dim('— no hook address can ever change again')}`
|
|
1074
|
-
:
|
|
1396
|
+
: paramHooksLock === false
|
|
1075
1397
|
? `${c.orange}not frozen${c.reset} ${dim('— the owner can re-point these (`abx lock-param-hooks` freezes them forever)')}`
|
|
1076
1398
|
: dim('freeze state unknown (the node refused the check)');
|
|
1077
1399
|
info(`param hooks ${shown.length ? shown.join(' · ') : dim('none set')} ${lockNote}`);
|
|
@@ -1083,6 +1405,10 @@ export async function cmdStateBody(address, flags, emit) {
|
|
|
1083
1405
|
catch {
|
|
1084
1406
|
/* best-effort — a non-code project has no params surface to read */
|
|
1085
1407
|
}
|
|
1408
|
+
// The coherent lock summary #122 asked for — every irreversible switch this node can read, side
|
|
1409
|
+
// by side, independent of whether this project happens to compose the params surface above.
|
|
1410
|
+
const locks = await readCollectionLocks(publicClient, address, STATE_ABI, kind.kind === 'code');
|
|
1411
|
+
printLocks(address, locks, !!hooks, paramHooksLock, payload, emit);
|
|
1086
1412
|
console.log('');
|
|
1087
1413
|
}
|
|
1088
1414
|
/**
|
|
@@ -1223,8 +1549,9 @@ async function cmdStateEditionBody(address, publicClient, kind, emit) {
|
|
|
1223
1549
|
// the same pair as the 721 body (which hooks, and whether they can still change); silent for the
|
|
1224
1550
|
// image/1-of-1 edition twins, which have no params surface at all.
|
|
1225
1551
|
const hooks = await readParamHooks(publicClient, address);
|
|
1552
|
+
let paramHooksLock;
|
|
1226
1553
|
if (hooks) {
|
|
1227
|
-
|
|
1554
|
+
paramHooksLock = await readParamHooksLocked(publicClient, address);
|
|
1228
1555
|
const shown = [['configure', hooks.configureHook], ['augment', hooks.augmentHook], ['transfer', hooks.transferHook]]
|
|
1229
1556
|
.filter(([, a]) => a !== zeroAddress)
|
|
1230
1557
|
.map(([role, a]) => `${role} ${a}`);
|
|
@@ -1232,12 +1559,12 @@ async function cmdStateEditionBody(address, publicClient, kind, emit) {
|
|
|
1232
1559
|
configureHook: hooks.configureHook === zeroAddress ? null : hooks.configureHook,
|
|
1233
1560
|
augmentHook: hooks.augmentHook === zeroAddress ? null : hooks.augmentHook,
|
|
1234
1561
|
transferHook: hooks.transferHook === zeroAddress ? null : hooks.transferHook,
|
|
1235
|
-
locked:
|
|
1562
|
+
locked: paramHooksLock ?? null, // null = unknown; never collapse it to false
|
|
1236
1563
|
});
|
|
1237
1564
|
emit(payload);
|
|
1238
|
-
const lockNote =
|
|
1565
|
+
const lockNote = paramHooksLock === true
|
|
1239
1566
|
? `${c.green}frozen${c.reset} ${dim('— no hook address can ever change again')}`
|
|
1240
|
-
:
|
|
1567
|
+
: paramHooksLock === false
|
|
1241
1568
|
? `${c.orange}not frozen${c.reset} ${dim('— the owner can re-point these (`abx lock-param-hooks` freezes them forever)')}`
|
|
1242
1569
|
: dim('freeze state unknown (the node refused the check)');
|
|
1243
1570
|
info(`param hooks ${shown.length ? shown.join(' · ') : dim('none set')} ${lockNote}`);
|
|
@@ -1245,6 +1572,9 @@ async function cmdStateEditionBody(address, publicClient, kind, emit) {
|
|
|
1245
1572
|
info(dim(` the transfer hook is a VETO: if it reverts, the transfer fails — and mints too (mint = transfer from 0x0).`));
|
|
1246
1573
|
}
|
|
1247
1574
|
}
|
|
1575
|
+
// The coherent lock summary #122 asked for — see cmdStateBody's identical call for why.
|
|
1576
|
+
const locks = await readCollectionLocks(publicClient, address, EDITION_STATE_ABI, kind.kind === 'edition-code');
|
|
1577
|
+
printLocks(address, locks, !!hooks, paramHooksLock, payload, emit);
|
|
1248
1578
|
console.log('');
|
|
1249
1579
|
}
|
|
1250
1580
|
/**
|