@artblocks/abx-cli 0.1.0-alpha.17 → 0.1.0-alpha.18
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 +152 -0
- package/dist/commands/deploy.d.ts +181 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +2974 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/project.d.ts +134 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +919 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/reads.d.ts +64 -0
- package/dist/commands/reads.d.ts.map +1 -0
- package/dist/commands/reads.js +635 -0
- package/dist/commands/reads.js.map +1 -0
- package/dist/commands/scaffold.d.ts +55 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +470 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/service.d.ts +41 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +572 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/storage.d.ts +37 -0
- package/dist/commands/storage.d.ts.map +1 -0
- package/dist/commands/storage.js +335 -0
- package/dist/commands/storage.js.map +1 -0
- package/dist/config.d.ts +72 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +201 -4
- package/dist/config.js.map +1 -1
- package/dist/deps.d.ts +6 -39
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +4 -68
- package/dist/deps.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/flags.d.ts +34 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +74 -0
- package/dist/flags.js.map +1 -1
- package/dist/main.js +57 -6751
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +34 -40
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +137 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +504 -0
- package/dist/output.js.map +1 -0
- package/dist/ownerops.d.ts +23 -44
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +151 -233
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +8 -4
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +8 -17
- package/dist/preview.js.map +1 -1
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +10 -7
- package/dist/provision.js.map +1 -1
- package/dist/remote.d.ts +72 -7
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +211 -9
- package/dist/remote.js.map +1 -1
- package/dist/riskgate.d.ts +29 -0
- package/dist/riskgate.d.ts.map +1 -0
- package/dist/riskgate.js +91 -0
- package/dist/riskgate.js.map +1 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -16
- package/dist/schema.js.map +1 -1
- package/dist/served.d.ts +1 -17
- package/dist/served.d.ts.map +1 -1
- package/dist/served.js +47 -0
- package/dist/served.js.map +1 -1
- package/dist/signer.d.ts +13 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +42 -19
- package/dist/signer.js.map +1 -1
- package/dist/update-check.d.ts +27 -0
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +30 -0
- package/dist/update-check.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +7 -11
- package/skill/reference/code-projects.md +3 -3
- package/skill/reference/hosting.md +6 -6
- package/skill/reference/operating.md +2 -5
- package/skill/reference/setup.md +2 -2
- package/skill/reference/troubleshooting.md +1 -1
- package/dist/gas.d.ts +0 -63
- package/dist/gas.d.ts.map +0 -1
- package/dist/gas.js +0 -76
- package/dist/gas.js.map +0 -1
- package/dist/inspect.d.ts +0 -48
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -290
- package/dist/inspect.js.map +0 -1
- package/dist/migrate.d.ts +0 -65
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -180
- package/dist/migrate.js.map +0 -1
- package/dist/onchain-uri.d.ts +0 -81
- package/dist/onchain-uri.d.ts.map +0 -1
- package/dist/onchain-uri.js +0 -251
- package/dist/onchain-uri.js.map +0 -1
- package/dist/resume.d.ts +0 -96
- package/dist/resume.d.ts.map +0 -1
- package/dist/resume.js +0 -95
- package/dist/resume.js.map +0 -1
- package/dist/upload.d.ts +0 -28
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -41
- package/dist/upload.js.map +0 -1
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project lifecycle commands: `predict` (pre-compute a deploy address), `add` (register + index a
|
|
3
|
+
* project this node didn't deploy), `index` (re-index from chain), `verify` (re-hash served bytes
|
|
4
|
+
* vs. the on-chain commitment), `state`/`status` (on-chain snapshot / indexed-project rollup), and
|
|
5
|
+
* `forget` (drop the local registration; on-chain data is untouched).
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { resolve as resolvePath } from 'node:path';
|
|
9
|
+
import { AUTH_OPTIONS, CREATOR_TOKEN_INTERFACE_ID, PARAM_TYPES, hasOnChainUriLane, isCodeProject, isCurrentFactory, makePublicClient, makeWalletClient, normalizeAttributes, onChainUriReport, parseTraitPairs, predictClone, readParamSchema, readParamSchemaKeys, readSetParamKeys, resolveChain, resolveSeriesCodeFactory, saltFor, saltGuard, sleep, tryReadContract, } from '@artblocks/abx-sdk';
|
|
10
|
+
import { resolveBackend } from '@artblocks/abx-storage';
|
|
11
|
+
import { currentRenderArtifact, verifyProject } from '@artblocks/abx-token-api';
|
|
12
|
+
import { zeroAddress } from 'viem';
|
|
13
|
+
import { CHAIN, activeBackendId, factoryAddress, localIndexer, remoteLocators, seriesFactoryAddress, storageOptions, } from '../config.js';
|
|
14
|
+
import { isDryRun, parseSaltFlag } from '../flags.js';
|
|
15
|
+
import { jsonSafe, withJson } from '../jsonout.js';
|
|
16
|
+
import { allowLargeScan, bold, c, detectCanonicalFactory, dim, g, info, ok, registerAndIndexLocally, resolveScanFloor, warn } from '../output.js';
|
|
17
|
+
import { canonicalLabel, describeRemoteError, indexErrorAction, remoteFlag, reportRemoteIndexing, requireRemoteToken, rollUp, serviceClient, statusLabel, statusLine, statusRow, tokenSourceLabel, } from '../remote.js';
|
|
18
|
+
import { describeSchema } from '../schema.js';
|
|
19
|
+
import { looksPerTokenAttributes, parseSeriesTraitsById } from '../series-traits.js';
|
|
20
|
+
// ── predict ──────────────────────────────────────────────────────────────────
|
|
21
|
+
// Pre-compute a deploy address from a salt — so you can stand up the resolver and
|
|
22
|
+
// reserve a vanity/known address before signing. The address is a pure function of
|
|
23
|
+
// (factory, salt); the salt's leading 20 bytes are the front-run guard.
|
|
24
|
+
export async function cmdPredict(flags) {
|
|
25
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
26
|
+
// Lane-aware: each deploy command uses a DIFFERENT factory, so the deterministic address differs.
|
|
27
|
+
// Infer the lane from the content flags so `predict` matches what will actually be deployed —
|
|
28
|
+
// otherwise a code/Series creator gets the 1/1 address (a real mismatch, not just a wrong echo).
|
|
29
|
+
const chainId = resolveChain(CHAIN).id;
|
|
30
|
+
const content = flags.script ? `--script ${flags.script}` :
|
|
31
|
+
flags['code-dir'] ? `--code-dir ${flags['code-dir']}` :
|
|
32
|
+
flags['image-renderer'] ? `--image-renderer ${flags['image-renderer']}` :
|
|
33
|
+
flags.dir ? `--dir ${flags.dir}` :
|
|
34
|
+
flags.image ? `--image ${flags.image}` : '';
|
|
35
|
+
const lane = (flags.script || flags['code-dir'] || flags['image-renderer'])
|
|
36
|
+
? { cmd: 'deploy-code', factory: resolveSeriesCodeFactory(chainId, flags.factory) }
|
|
37
|
+
: flags.dir
|
|
38
|
+
? { cmd: 'deploy-series', factory: seriesFactoryAddress(flags.factory) }
|
|
39
|
+
: { cmd: 'deploy', factory: factoryAddress(flags.factory) };
|
|
40
|
+
const factory = lane.factory;
|
|
41
|
+
if (!factory)
|
|
42
|
+
throw new Error(`no canonical ${lane.cmd} factory for '${CHAIN}' — run \`abx ${lane.cmd}\` once to deploy the trust anchor, or pass --factory 0x.. (or the ABX_* env).`);
|
|
43
|
+
// The current-factory guard is the 1/1 impl-version check; the Series/code lanes verify their own
|
|
44
|
+
// factory at deploy time. predictDeterministicAddress works on any factory ABI.
|
|
45
|
+
if (lane.cmd === 'deploy' && !(await isCurrentFactory(publicClient, factory))) {
|
|
46
|
+
throw new Error(`factory ${factory} is an older/incompatible version. Run \`abx deploy\` once to deploy the current ` +
|
|
47
|
+
`trust anchor (ownerless + cheap), then predict again — or pass a current --factory.`);
|
|
48
|
+
}
|
|
49
|
+
let salt = parseSaltFlag(flags.salt);
|
|
50
|
+
if (!salt) {
|
|
51
|
+
// No explicit salt: reserve one to a deployer (front-run-proof). Default to the env key.
|
|
52
|
+
let deployer = flags.for;
|
|
53
|
+
if (!deployer)
|
|
54
|
+
deployer = makeWalletClient({ chainKey: CHAIN }).account.address;
|
|
55
|
+
salt = saltFor(deployer);
|
|
56
|
+
info(`reserved a fresh salt to ${deployer} (front-run-proof) — pass --salt to fix/vanity it`);
|
|
57
|
+
}
|
|
58
|
+
const guard = saltGuard(salt);
|
|
59
|
+
const clone = await predictClone(publicClient, { factory, salt });
|
|
60
|
+
console.log(`\n ${bold('predicted address')} ${g(clone)} ${dim(`(${lane.cmd} lane)`)}`);
|
|
61
|
+
info(`salt ${salt}`);
|
|
62
|
+
info(guard === zeroAddress ? 'guard permissionless — anyone may deploy this salt' : `guard reserved to ${guard} (only this signer can deploy it)`);
|
|
63
|
+
info(`deploy: ${bold(`abx ${lane.cmd}${content ? ` ${content}` : ''} --salt ${salt}`)}`);
|
|
64
|
+
if (lane.cmd === 'deploy')
|
|
65
|
+
info(dim('(this is the 1/1 lane; a Series/code drop uses a different factory → a different address — pass --dir / --script to predict those, or use that command\'s --dry-run)'));
|
|
66
|
+
console.log('');
|
|
67
|
+
}
|
|
68
|
+
// ── add ──────────────────────────────────────────────────────────────────────
|
|
69
|
+
// Register + index a project this node didn't deploy. LOCAL by default (this
|
|
70
|
+
// machine's store); `--remote [url]` instead tells a HOSTED resolver to index it —
|
|
71
|
+
// the bridge a local deploy can't make on its own (separate projection stores).
|
|
72
|
+
export async function cmdAdd(address, flags) {
|
|
73
|
+
if (!address || address.startsWith('--')) {
|
|
74
|
+
console.error('usage: abx add <address> [--from-block N] [--factory 0x..] [--label "..."] [--remote [name|url]]\n');
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// REFUSE `--dry-run` rather than ignoring it. `add` is a write (a local registration + index, and
|
|
79
|
+
// with `--remote` a registration on someone else's service), and it has no preview mode — so
|
|
80
|
+
// silently proceeding to DO the thing when the caller explicitly asked to preview is the one
|
|
81
|
+
// outcome we must never produce. Name what's read-only instead.
|
|
82
|
+
if (isDryRun(flags)) {
|
|
83
|
+
throw new Error('`abx add` has no --dry-run: it registers + indexes for real (and with --remote it registers on that service). ' +
|
|
84
|
+
'Nothing here touches the chain, but it does write. To look before acting: `abx state <address>` (on-chain snapshot) ' +
|
|
85
|
+
'or `abx status <address> [--remote <name>]` (what a node already has). Re-run without --dry-run when you mean it.');
|
|
86
|
+
}
|
|
87
|
+
// `--attributes` is lane-aware here exactly as at deploy: a PER-TOKEN payload edits a Series'
|
|
88
|
+
// per-token off-chain traits; a flat payload (+ `--traits`) edits the collection/1-of-1 `attributes`.
|
|
89
|
+
// Ambiguity defaults to flat (see looksPerTokenAttributes), so a 1/1 add is never mis-read.
|
|
90
|
+
const attrRaw = flags.attributes ? readFileSync(resolvePath(process.cwd(), String(flags.attributes)), 'utf8') : undefined;
|
|
91
|
+
const perTokenEdit = attrRaw != null && looksPerTokenAttributes(attrRaw);
|
|
92
|
+
const flagTraits = [];
|
|
93
|
+
if (attrRaw != null && !perTokenEdit)
|
|
94
|
+
flagTraits.push(...normalizeAttributes(JSON.parse(attrRaw)));
|
|
95
|
+
if (flags.traits)
|
|
96
|
+
flagTraits.push(...parseTraitPairs(flags.traits));
|
|
97
|
+
const editedTokenAttributes = perTokenEdit ? parseSeriesTraitsById(attrRaw) : undefined;
|
|
98
|
+
const remote = remoteFlag(flags);
|
|
99
|
+
if (remote) {
|
|
100
|
+
requireRemoteToken(remote);
|
|
101
|
+
// Bridge what a remote resolver can't derive itself: the off-chain traits and the durable
|
|
102
|
+
// content locators (ipfs://…). Prefer flags; otherwise forward what the LOCAL deploy stored
|
|
103
|
+
// (the local registration), and compute locators from this machine's content index if needed.
|
|
104
|
+
const localReg = localIndexer().store.getRegistration(address);
|
|
105
|
+
const attributes = flagTraits.length
|
|
106
|
+
? flagTraits
|
|
107
|
+
: localReg?.attributes
|
|
108
|
+
? normalizeAttributes(JSON.parse(localReg.attributes))
|
|
109
|
+
: undefined;
|
|
110
|
+
// Bridge a Series' per-token off-chain traits to the remote resolver (the resolver has no other
|
|
111
|
+
// way to derive them — they're operator metadata, not chain state). A fresh per-token `--attributes`
|
|
112
|
+
// EDITS them; otherwise forward what the LOCAL deploy stored. Best-effort parse.
|
|
113
|
+
let tokenAttributes;
|
|
114
|
+
if (editedTokenAttributes && Object.keys(editedTokenAttributes).length) {
|
|
115
|
+
tokenAttributes = editedTokenAttributes;
|
|
116
|
+
}
|
|
117
|
+
else if (localReg?.tokenAttributes) {
|
|
118
|
+
try {
|
|
119
|
+
const obj = JSON.parse(localReg.tokenAttributes);
|
|
120
|
+
const norm = {};
|
|
121
|
+
for (const [id, v] of Object.entries(obj)) {
|
|
122
|
+
const a = normalizeAttributes(v);
|
|
123
|
+
if (a.length)
|
|
124
|
+
norm[id] = a;
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(norm).length)
|
|
127
|
+
tokenAttributes = norm;
|
|
128
|
+
}
|
|
129
|
+
catch { /* skip a malformed local column */ }
|
|
130
|
+
}
|
|
131
|
+
const contentLocators = await remoteLocators(address, localReg?.contentLocators, flags);
|
|
132
|
+
// Forward the deploy block, like every other field falls back to the local registration.
|
|
133
|
+
// (Its ABSENCE here was the bug: a hosted resolver defaulted to genesis and scanned the whole
|
|
134
|
+
// chain.) Re-sending the same floor stays incremental server-side, so a nudge ≠ a re-scan.
|
|
135
|
+
const body = {
|
|
136
|
+
chainId: resolveChain(CHAIN).id,
|
|
137
|
+
address,
|
|
138
|
+
fromBlock: await resolveScanFloor(address, localReg?.fromBlock, flags),
|
|
139
|
+
factory: await detectCanonicalFactory(address, flags.factory, localReg?.factory),
|
|
140
|
+
label: flags.label,
|
|
141
|
+
description: flags.description ?? localReg?.description,
|
|
142
|
+
externalUrl: flags['external-url'] ?? localReg?.externalUrl,
|
|
143
|
+
attributes,
|
|
144
|
+
tokenAttributes,
|
|
145
|
+
contentLocators: Object.keys(contentLocators).length ? contentLocators : undefined,
|
|
146
|
+
full: flags.full ? true : undefined,
|
|
147
|
+
};
|
|
148
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(registering with the remote resolver — NOT this machine)')}`);
|
|
149
|
+
if (body.contentLocators)
|
|
150
|
+
info(`bridging image locator → ${Object.values(body.contentLocators)[0]} ${dim('(so the resolver points at IPFS, not its own localhost)')}`);
|
|
151
|
+
let r;
|
|
152
|
+
try {
|
|
153
|
+
r = await serviceClient(remote).registerProject(body);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
throw describeRemoteError(err, remote, 'remote add');
|
|
157
|
+
}
|
|
158
|
+
await reportRemoteIndexing(remote, body.chainId, address, r, flags, 'indexed');
|
|
159
|
+
info(`it now serves ${remote.url}/t/${body.chainId}/${address.toLowerCase()}/0`);
|
|
160
|
+
// "Indexed" is not "correct". This line proves the service replayed the chain and will answer at
|
|
161
|
+
// that URL — it says nothing about whether the bytes it serves match the on-chain commitment, and
|
|
162
|
+
// two reviewers stopped here and reported a blank page as fixed. Name the step that checks.
|
|
163
|
+
info(`confirm what it actually serves (bytes vs. the on-chain hash): ${bold(`abx verify ${address} --remote ${remote.name?.toLowerCase() ?? remote.url}`)}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// The local (non-`--remote`) path is shared with the deploy family's post-setup registration —
|
|
167
|
+
// see registerAndIndexLocally's doc comment (output.ts).
|
|
168
|
+
return registerAndIndexLocally(address, flags);
|
|
169
|
+
}
|
|
170
|
+
export async function cmdIndex(address, flags) {
|
|
171
|
+
const remote = remoteFlag(flags);
|
|
172
|
+
if (remote) {
|
|
173
|
+
if (!address || address.startsWith('--')) {
|
|
174
|
+
console.error('usage: abx index <address> --remote [name|url] (re-index one project on a remote resolver)\n');
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
requireRemoteToken(remote);
|
|
179
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(re-indexing on the remote resolver — the post-deploy nudge)')}`);
|
|
180
|
+
const chainId = resolveChain(CHAIN).id;
|
|
181
|
+
let r;
|
|
182
|
+
try {
|
|
183
|
+
r = await serviceClient(remote).registerProject({ chainId, address, full: flags.full ? true : undefined });
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
throw describeRemoteError(err, remote, 'remote index');
|
|
187
|
+
}
|
|
188
|
+
await reportRemoteIndexing(remote, chainId, address, r, flags, 're-indexed');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
allowLargeScan(flags);
|
|
192
|
+
const full = !!flags.full; // force a full replay from the deploy block (durability proof)
|
|
193
|
+
const indexer = localIndexer();
|
|
194
|
+
const line = (state, elapsedMs, mode) => ok(`${state.name ?? state.address}: ${state.eventCount} events in ${elapsedMs}ms ${dim(`(${mode})`)}`);
|
|
195
|
+
if (address) {
|
|
196
|
+
const { state, elapsedMs, mode } = await indexer.reindex(address, { full });
|
|
197
|
+
line(state, elapsedMs, mode);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const results = await indexer.reindexAll({ full });
|
|
201
|
+
if (results.length === 0) {
|
|
202
|
+
console.log(dim('No registered projects. Deploy one with `abx deploy`.'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const { state, elapsedMs, mode } of results)
|
|
206
|
+
line(state, elapsedMs, mode);
|
|
207
|
+
}
|
|
208
|
+
// ── verify ─────────────────────────────────────────────────────────────────--
|
|
209
|
+
// Re-hash a project's served bytes against its on-chain content commitment, from
|
|
210
|
+
// chain + custody alone — no running server. The CLI form of the `/verify` route,
|
|
211
|
+
// so an agent can confirm integrity right after deploy/mint without curling.
|
|
212
|
+
export async function cmdVerify(address, flags) {
|
|
213
|
+
if (!address || address.startsWith('--')) {
|
|
214
|
+
console.error('usage: abx verify <address> [--json] [--remote <name|url>]\n');
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
allowLargeScan(flags);
|
|
219
|
+
const remote = remoteFlag(flags);
|
|
220
|
+
if (remote)
|
|
221
|
+
return cmdVerifyRemote(address, remote);
|
|
222
|
+
// `--json` matters most here of all the read commands: verify already exits non-zero on a byte
|
|
223
|
+
// mismatch, so it is the one command a CI job would gate on — and gating on prose is what B19 is
|
|
224
|
+
// about. The payload's `ok` is the same boolean the exit code carries.
|
|
225
|
+
return withJson(flags, async (emit) => cmdVerifyBody(address, flags, emit));
|
|
226
|
+
}
|
|
227
|
+
export async function cmdVerifyBody(address, flags, emit) {
|
|
228
|
+
const indexer = localIndexer();
|
|
229
|
+
let state = indexer.getProject(address);
|
|
230
|
+
if (indexer.store.getRegistration(address)) {
|
|
231
|
+
({ state } = await indexer.reindex(address)); // freshest state from chain
|
|
232
|
+
}
|
|
233
|
+
else if (!state) {
|
|
234
|
+
throw new Error(`${address} isn't indexed by this node. Run \`abx add ${address}\` first.`);
|
|
235
|
+
}
|
|
236
|
+
if (!state)
|
|
237
|
+
throw new Error(`could not load state for ${address}`);
|
|
238
|
+
// Accumulated as the checks run, re-emitted at each stage so a payload survives an early return.
|
|
239
|
+
const verifyReport = {
|
|
240
|
+
address,
|
|
241
|
+
chain: CHAIN,
|
|
242
|
+
name: state.name ?? null,
|
|
243
|
+
canonical: state.isCanonical, // TRUE tri-state: true | false | null (couldn't check)
|
|
244
|
+
owner: state.owner ?? null,
|
|
245
|
+
contentChecks: [],
|
|
246
|
+
contentIntegrity: 'no-commitments',
|
|
247
|
+
renders: null,
|
|
248
|
+
onChainUri: null,
|
|
249
|
+
ok: true,
|
|
250
|
+
};
|
|
251
|
+
emit(verifyReport);
|
|
252
|
+
console.log(bold(`\n verify ${state.name ?? address}`));
|
|
253
|
+
// isCanonical is a TRUE TRI-STATE (true | false | null) and collapsing it lost the only
|
|
254
|
+
// distinction that matters: "the chain says this is NOT a clone of the configured factory" is a
|
|
255
|
+
// trust finding; "we couldn't run the check" (no factory configured, or none deployed on this
|
|
256
|
+
// chain) is an environment note. Two reviewers read the collapsed word as a second failure.
|
|
257
|
+
info(`canonical: ${canonicalLabel(state.isCanonical)} · owner ${state.owner ?? '—'}`);
|
|
258
|
+
const storage = resolveBackend(storageOptions());
|
|
259
|
+
const result = await verifyProject(state, storage);
|
|
260
|
+
const tokens = result.tokens ?? [];
|
|
261
|
+
let anyCheck = false;
|
|
262
|
+
let allGood = true;
|
|
263
|
+
for (const t of tokens) {
|
|
264
|
+
const tok = state.tokens.find((s) => s.tokenId === t.tokenId);
|
|
265
|
+
info(`token #${t.tokenId}: ${tok?.minted ? 'minted' : 'not yet minted'}`);
|
|
266
|
+
for (const ch of t.checks) {
|
|
267
|
+
verifyReport.contentChecks.push({
|
|
268
|
+
tokenId: t.tokenId,
|
|
269
|
+
minted: !!tok?.minted,
|
|
270
|
+
kind: ch.kind,
|
|
271
|
+
// tri-state again: true (re-hashed, matched) · false (MISMATCH) · null (pointer-only,
|
|
272
|
+
// nothing local to recompute). Collapsing null into false would report a normal state as a
|
|
273
|
+
// failure, which is the bug this command exists to not have.
|
|
274
|
+
verified: ch.verified,
|
|
275
|
+
});
|
|
276
|
+
anyCheck = true;
|
|
277
|
+
if (ch.verified === true)
|
|
278
|
+
ok(`${ch.kind} matches on-chain commitment — content is authentic`);
|
|
279
|
+
else if (ch.verified === false) {
|
|
280
|
+
allGood = false;
|
|
281
|
+
console.log(` ${c.red}✗${c.reset} ${ch.kind} MISMATCH — served bytes don't match the commitment`);
|
|
282
|
+
}
|
|
283
|
+
else
|
|
284
|
+
info(`${ch.kind} — pointer-only, not locally recomputable`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!anyCheck && !isCodeProject(state))
|
|
288
|
+
info('no content commitments on this project');
|
|
289
|
+
verifyReport.contentIntegrity = !anyCheck ? 'no-commitments' : allGood ? 'ok' : 'mismatch';
|
|
290
|
+
verifyReport.ok = !anyCheck || allGood;
|
|
291
|
+
emit(verifyReport);
|
|
292
|
+
// Code projects have no on-chain `image` commitment (the still is rendered off-chain by the
|
|
293
|
+
// effect runner), so the hash checks above are silent for them. Instead report what actually
|
|
294
|
+
// matters after mint: does a REAL thumbnail exist yet, or is the marketplace image still the
|
|
295
|
+
// placeholder? Same seam the /…/image route serves from (currentRenderArtifact @ current
|
|
296
|
+
// inputsHash) — so this answers "did the render land?" with one command, right after token 0.
|
|
297
|
+
let renderGap = false;
|
|
298
|
+
if (isCodeProject(state)) {
|
|
299
|
+
const client = makePublicClient({ chainKey: CHAIN });
|
|
300
|
+
const storageForRender = resolveBackend(storageOptions());
|
|
301
|
+
const minted = state.tokens.filter((t) => t.minted);
|
|
302
|
+
if (minted.length === 0)
|
|
303
|
+
info('no tokens minted yet — mint token #0, then re-run to check its thumbnail.');
|
|
304
|
+
// Live-data posture — the augment hook IS the opt-in: no hook ⇒ zero live reads (pure indexed
|
|
305
|
+
// params); hook set ⇒ the live view reads chain per view, and the STILL snapshots settled state
|
|
306
|
+
// (live data never re-addresses the render — that's the settled/full split).
|
|
307
|
+
info(state.paramHooks?.augmentHook
|
|
308
|
+
? `live data: augment hook ${state.paramHooks.augmentHook} — live view reads chain per view; stills snapshot settled state`
|
|
309
|
+
: 'live data: none (settled params only — the live view makes zero extra chain reads)');
|
|
310
|
+
// The other two param hooks (if wired) — surfaced here since this is where hooks show. Quiet when
|
|
311
|
+
// unset (the common case). Manage all three with `abx set-param-hooks`.
|
|
312
|
+
if (state.paramHooks?.configureHook || state.paramHooks?.transferHook) {
|
|
313
|
+
const parts = [];
|
|
314
|
+
if (state.paramHooks.configureHook)
|
|
315
|
+
parts.push(`configure ${state.paramHooks.configureHook}`);
|
|
316
|
+
if (state.paramHooks.transferHook)
|
|
317
|
+
parts.push(`transfer ${state.paramHooks.transferHook}`);
|
|
318
|
+
info(`param hooks: ${parts.join(' · ')} ${dim('(configure = write-time veto · transfer = ownership-change lifecycle; manage with `abx set-param-hooks`)')}`);
|
|
319
|
+
}
|
|
320
|
+
// This checks THIS machine's store. A render PUBLISHED to a hosted resolver (locator bridge) lives
|
|
321
|
+
// on that resolver, NOT here — so a "not found" below can be a false negative for a hosted drop.
|
|
322
|
+
// `abx verify <addr> --remote <resolver>` probes what the resolver actually serves (the truthful check).
|
|
323
|
+
if (minted.length)
|
|
324
|
+
info(dim(`render check is against THIS node's store; for a HOSTED drop use \`abx verify ${address} --remote <resolver>\``));
|
|
325
|
+
// ONE line per outcome, not per token. This printed the same full-sentence advisory 32 times on a
|
|
326
|
+
// 32-token project (~4KB of identical text) and pushed the four lines that answer "did my deploy
|
|
327
|
+
// work" off the top of the screen; at a 1000-token supply it is unreadable. The per-token detail
|
|
328
|
+
// that survives is the token LIST, which is the only part that differs.
|
|
329
|
+
const missing = [];
|
|
330
|
+
let present = 0;
|
|
331
|
+
for (const token of minted) {
|
|
332
|
+
const { found } = await currentRenderArtifact(client, state, token, storageForRender, 'image');
|
|
333
|
+
if (found)
|
|
334
|
+
present++;
|
|
335
|
+
else
|
|
336
|
+
missing.push(String(token.tokenId));
|
|
337
|
+
}
|
|
338
|
+
verifyReport.renders = {
|
|
339
|
+
minted: minted.length,
|
|
340
|
+
present,
|
|
341
|
+
missing, // token ids, so a caller can re-render exactly these
|
|
342
|
+
// Named for what it IS: this node's store. A render PUBLISHED to a hosted resolver lives
|
|
343
|
+
// there, not here, so `missing` is a false negative for a hosted drop — hence the scope.
|
|
344
|
+
scope: "this node's store",
|
|
345
|
+
};
|
|
346
|
+
emit(verifyReport);
|
|
347
|
+
if (present)
|
|
348
|
+
ok(`${present}/${minted.length} minted token(s): real render present (in this node's store)`);
|
|
349
|
+
if (missing.length) {
|
|
350
|
+
renderGap = true;
|
|
351
|
+
const ids = missing.length > 12 ? `${missing.slice(0, 12).join(', ')}, …+${missing.length - 12} more` : missing.join(', ');
|
|
352
|
+
console.log(` ${c.orange}⚠${c.reset} ${missing.length}/${minted.length} token(s) have no render in THIS node's store ${dim(`(#${ids})`)}`);
|
|
353
|
+
console.log(` ${dim('published to a hosted resolver? check there:')} ${bold(`abx verify ${address} --remote <resolver>`)}`);
|
|
354
|
+
console.log(` ${dim('else render them:')} ${bold(`abx render ${address}`)} ${dim('(once) ·')} ${bold('abx effects')} ${dim('(continuous)')}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// The on-chain URI lane (a non-zero tokenURIRenderer, or an animation field pointing at the
|
|
358
|
+
// generator): read the generator's honesty surface — onChainStatus (branch · chain-complete ·
|
|
359
|
+
// unresolved refs · URL budget) — then decode tokenURI straight from the contract and report
|
|
360
|
+
// what the animation_url actually is. All eth_calls; no server, no local store involved.
|
|
361
|
+
if (hasOnChainUriLane(state)) {
|
|
362
|
+
const client = makePublicClient({ chainKey: CHAIN });
|
|
363
|
+
try {
|
|
364
|
+
const report = await onChainUriReport(client, state, flags.generator);
|
|
365
|
+
const { status, probe } = report;
|
|
366
|
+
verifyReport.onChainUri = jsonSafe({
|
|
367
|
+
generator: report.generator,
|
|
368
|
+
tokenURIRenderer: state.tokenURIRenderer ?? null,
|
|
369
|
+
branch: status.branchName,
|
|
370
|
+
chainComplete: !!status.chainComplete,
|
|
371
|
+
unresolvedRefs: [...status.unresolvedRefs],
|
|
372
|
+
urlOverBudget: !!status.urlOverBudget,
|
|
373
|
+
probe: probe
|
|
374
|
+
? { tokenId: probe.tokenId, onChainJson: !!probe.onChainJson, animation: probe.animation }
|
|
375
|
+
: null,
|
|
376
|
+
});
|
|
377
|
+
emit(verifyReport);
|
|
378
|
+
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)'}`);
|
|
379
|
+
if (status.branchName === 'template') {
|
|
380
|
+
ok(`generator branch: template — the document assembles from the on-chain script chunks`);
|
|
381
|
+
if (status.chainComplete)
|
|
382
|
+
ok('chain-complete — every dependency resolves to proven on-chain bytes; no server, gateway, or CDN in the graph');
|
|
383
|
+
else
|
|
384
|
+
info('not chain-complete — CDN-served or unresolved dependencies (CDN entries serve fine; on-chain bytes are the durability floor)');
|
|
385
|
+
for (const ref of status.unresolvedRefs) {
|
|
386
|
+
console.log(` ${c.red}✗${c.reset} unresolved dependency ${bold(ref)} — the document carries an <!-- abx:unresolved --> marker; fix the ref or the registry pointer`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else if (status.branchName === 'directory') {
|
|
390
|
+
ok('generator branch: directory — the code field emits a parameterized gateway URL');
|
|
391
|
+
if (status.urlOverBudget)
|
|
392
|
+
console.log(` ${c.orange}⚠${c.reset} the emitted URL exceeds the 8KB budget — gateway front-ends may drop it; prefer template mode, trim the param surface, or carry big values as locator params`);
|
|
393
|
+
else
|
|
394
|
+
info('URL within the 8KB budget');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(` ${c.orange}⚠${c.reset} generator reports NO code (neither script chunks nor a code field) — animation_url degrades to an <!-- abx:no-code --> marker`);
|
|
398
|
+
}
|
|
399
|
+
if (!probe) {
|
|
400
|
+
info('tokenURI probe skipped — no token minted yet (mint token #0, then re-run)');
|
|
401
|
+
}
|
|
402
|
+
else if (!probe.onChainJson) {
|
|
403
|
+
console.log(` ${c.red}✗${c.reset} tokenURI(${probe.tokenId}) is NOT an on-chain data:application/json;base64 URI — got "${probe.uriPrefix}…"`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
ok(`tokenURI(${probe.tokenId}) resolves ON-CHAIN: data:application/json;base64 — decoded from the contract, no server`);
|
|
407
|
+
const a = probe.animation;
|
|
408
|
+
if (a.form === 'data-html') {
|
|
409
|
+
if (a.marker)
|
|
410
|
+
console.log(` ${c.orange}⚠${c.reset} animation_url: data:text/html;base64 (${a.bytes} bytes decoded) but the document carries ${bold(a.marker)} — it degraded honestly instead of reverting`);
|
|
411
|
+
else
|
|
412
|
+
ok(`animation_url: data:text/html;base64 — ${a.bytes} bytes of HTML, decoded (the full document, inline)`);
|
|
413
|
+
}
|
|
414
|
+
else if (a.form === 'url') {
|
|
415
|
+
ok(`animation_url: ${a.url} ${dim('(directory branch — the parameterized gateway URL, landed verbatim)')}`);
|
|
416
|
+
}
|
|
417
|
+
else if (a.form === 'data-other') {
|
|
418
|
+
console.log(` ${c.orange}⚠${c.reset} animation_url is a data: URI but not text/html — got "${a.prefix}…"`);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
console.log(` ${c.orange}⚠${c.reset} no animation_url in the on-chain JSON — the generator field may be missing or unrenderable`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// The READ-side envelope. Params enumerate on-chain, so the write side is unbounded — but
|
|
425
|
+
// tokenURI and tokenData assemble EVERY enumerated param per call, and that is what grows.
|
|
426
|
+
// ~64 keys is the documented design envelope; hard failure only nears at several hundred.
|
|
427
|
+
// Advisory (never a ✗) — a big surface is a legitimate choice, it just isn't free.
|
|
428
|
+
const set = await readSetParamKeys(client, state.address, probe ? BigInt(probe.tokenId) : undefined);
|
|
429
|
+
if (set) {
|
|
430
|
+
const total = new Set([...set.contract, ...set.token]).size;
|
|
431
|
+
if (total > 64) {
|
|
432
|
+
console.log(` ${c.orange}⚠${c.reset} ${total} params enumerate on-chain${probe ? ` for token #${probe.tokenId}` : ''} — tokenURI and tokenData assemble every one of them per call, so both grow with this count. ` +
|
|
433
|
+
dim('The documented design envelope is ~64; past a few hundred a public eth_call can hit its gas cap.'));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
console.log(` ${c.orange}⚠${c.reset} on-chain URI check unavailable: ${e.message}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (renderGap)
|
|
442
|
+
console.log(`\n ${dim('live view animates regardless; the placeholder only affects the static marketplace thumbnail.')}\n`);
|
|
443
|
+
else
|
|
444
|
+
console.log(anyCheck && allGood ? `\n ${g('✓ verified')} — the node serves exactly what the chain commits to.\n` : '\n');
|
|
445
|
+
// A byte-vs-chain mismatch is an integrity FAILURE, so fail the command. Verify's whole job is to
|
|
446
|
+
// answer "is what's served what the chain vouches for" — exiting 0 while printing ✗ meant nothing
|
|
447
|
+
// could gate on it (a script or CI would sail past a corrupted image). Deliberately narrow: a
|
|
448
|
+
// missing render / placeholder is a normal, expected state and still exits 0.
|
|
449
|
+
if (anyCheck && !allGood)
|
|
450
|
+
process.exitCode = 1;
|
|
451
|
+
}
|
|
452
|
+
// `abx verify <addr> --remote <url>` — verify what a HOSTED resolver actually serves (the local
|
|
453
|
+
// `verify` checks THIS machine's store/backend, the wrong store for a hosted drop). Probes the real
|
|
454
|
+
// `/…/image` route, so it accounts for the locator bridge (a 302 to ipfs/ar) exactly as a marketplace
|
|
455
|
+
// sees it — the truthful "did the thumbnail land?" check after a remote render.
|
|
456
|
+
export async function cmdVerifyRemote(address, remote) {
|
|
457
|
+
const base = remote.url.replace(/\/$/, '');
|
|
458
|
+
const chainId = resolveChain(CHAIN).id;
|
|
459
|
+
console.log(bold(`\n verify ${address} ${dim(`(remote → ${base})`)}`));
|
|
460
|
+
// A DOWN endpoint and a wrong-address endpoint are different problems with different fixes, and a
|
|
461
|
+
// bare `fetch failed` says neither. `abx status --remote` already gets this right — match it, or the
|
|
462
|
+
// two commands disagree about the same condition (a reviewer's top misdiagnosis risk).
|
|
463
|
+
let stateRes;
|
|
464
|
+
try {
|
|
465
|
+
stateRes = await fetch(`${base}/api/project/${address}`);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
throw new Error(`verify: nothing responded at ${base}${remote.source === 'named' ? ` (from ABX_REMOTE_${remote.name}_URL)` : ''}. Is it running, and is that the right address?`);
|
|
469
|
+
}
|
|
470
|
+
if (!stateRes.ok) {
|
|
471
|
+
throw new Error(`resolver ${base} doesn't serve ${address} (HTTP ${stateRes.status}) — register it first: abx add ${address} --remote ${remote.name?.toLowerCase() ?? base}`);
|
|
472
|
+
}
|
|
473
|
+
const state = (await stateRes.json());
|
|
474
|
+
info(`serving as "${state.name ?? address}"`);
|
|
475
|
+
// Is the resolver actively WATCHING the chain? Prove it from /api/watch (the meta the watcher
|
|
476
|
+
// stamps each tick) so a hosted operator who can't tail the log still sees liveness — and catches
|
|
477
|
+
// a silently-stalled watcher (pollAt gone stale) instead of assuming auto-updates still work.
|
|
478
|
+
const watch = (await fetch(`${base}/api/watch`)
|
|
479
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
480
|
+
.catch(() => null));
|
|
481
|
+
if (watch?.watching) {
|
|
482
|
+
const heads = Object.entries(watch.chains).map(([ck, s]) => `${ck} @ block ${s.head}`).join(', ');
|
|
483
|
+
const ageS = watch.pollAt ? Math.round((Date.now() - Date.parse(watch.pollAt)) / 1000) : null;
|
|
484
|
+
const stalled = ageS !== null && ageS > Math.max(60, Math.round((Number(watch.intervalMs) / 1000) * 4));
|
|
485
|
+
const detail = `watching ${heads || 'chain'}${ageS !== null ? ` · last poll ${ageS}s ago` : ''}${watch.lastDeltaAt ? ` · last change ${watch.lastDeltaAt}` : ''}`;
|
|
486
|
+
if (stalled)
|
|
487
|
+
console.log(` ${c.orange}⚠${c.reset} ${detail} — watcher looks STALLED; restart \`abx serve\``);
|
|
488
|
+
else
|
|
489
|
+
ok(detail);
|
|
490
|
+
}
|
|
491
|
+
else if (watch) {
|
|
492
|
+
console.log(` ${dim('watcher OFF — changes land only via explicit add/index or a manual render (ABX_WATCH_INTERVAL_MS=0)')}`);
|
|
493
|
+
}
|
|
494
|
+
const minted = (state.tokens ?? []).filter((t) => t.minted);
|
|
495
|
+
if (minted.length === 0) {
|
|
496
|
+
info('no tokens minted yet — mint token #0, then re-run.');
|
|
497
|
+
console.log('');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// Prefer the resolver's effect-status API (the derived 4-state readout: up-to-date · rendering ·
|
|
501
|
+
// failed(error) · stale). Falls back to the raw /image probe for a resolver without the route.
|
|
502
|
+
const statusRes = await fetch(`${base}/api/project/${address}/effects`).catch(() => null);
|
|
503
|
+
if (statusRes?.ok) {
|
|
504
|
+
const report = (await statusRes.json());
|
|
505
|
+
for (const t of report.tokens) {
|
|
506
|
+
const label = `token #${t.tokenId} ${t.effectKey}`;
|
|
507
|
+
if (t.status === 'up-to-date')
|
|
508
|
+
ok(`${label}: up to date (real render at the current state)`);
|
|
509
|
+
else if (t.status === 'rendering')
|
|
510
|
+
info(`${label}: rendering — the effects runner is on it`);
|
|
511
|
+
else if (t.status === 'failed')
|
|
512
|
+
console.log(` ${c.red}✗${c.reset} ${label}: FAILED${t.attempts ? ` after ${t.attempts} attempt(s)` : ''} — ${t.error ?? 'see runner logs'} ${dim(`(fix, then \`abx render ${address} ${t.tokenId} --force --remote ${base}\`)`)}`);
|
|
513
|
+
else
|
|
514
|
+
console.log(` ${c.orange}⚠${c.reset} ${label}: stale — no render at the current state yet (the runner's next notify/sweep picks it up, or \`abx render ${address} --remote ${base}\`)`);
|
|
515
|
+
}
|
|
516
|
+
const { upToDate, stale, rendering, failed } = report.counts;
|
|
517
|
+
// RENDERS ONLY — say so. This report answers "is there a current render for each token", never
|
|
518
|
+
// "do the served bytes match the on-chain commitment"; those are different questions and this
|
|
519
|
+
// command promises the second one too. A project with no renders at all (a 1/1, an image Series)
|
|
520
|
+
// has nothing to be "up to date" ABOUT, so don't print a 0/N fraction — but don't let a green ✓
|
|
521
|
+
// here read as "the image is verified" either. Byte integrity comes from the check below.
|
|
522
|
+
if (report.tokens.length === 0) {
|
|
523
|
+
info(`renders ${dim('none for this project (a static image needs no off-chain render)')}`);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// Lead with the count that carries the polarity: "N of M current" never inverts on a skim the
|
|
527
|
+
// way "0/M up to date" does.
|
|
528
|
+
const summary = `${upToDate} of ${minted.length} token(s) current${rendering ? ` · ${rendering} rendering` : ''}${stale ? ` · ${stale} stale` : ''}${failed ? ` · ${failed} FAILED` : ''}`;
|
|
529
|
+
if (failed || stale)
|
|
530
|
+
console.log(` ${c.orange}⚠${c.reset} renders: ${summary} ${dim('— live view animates regardless; only the static thumbnail is affected.')}`);
|
|
531
|
+
else
|
|
532
|
+
ok(`renders: ${summary}`);
|
|
533
|
+
}
|
|
534
|
+
await reportRemoteByteIntegrity(address, remote, base);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
let gap = false;
|
|
538
|
+
for (const t of minted) {
|
|
539
|
+
const img = await fetch(`${base}/t/${chainId}/${address}/${t.tokenId}/image`, { redirect: 'manual' });
|
|
540
|
+
const loc = img.headers.get('location');
|
|
541
|
+
const ct = img.headers.get('content-type') ?? '';
|
|
542
|
+
if (img.status >= 300 && img.status < 400 && loc)
|
|
543
|
+
ok(`token #${t.tokenId} image: real render — resolver 302s to ${loc}`);
|
|
544
|
+
else if (img.status === 200 && !/svg/i.test(ct))
|
|
545
|
+
ok(`token #${t.tokenId} image: real render present (${ct})`);
|
|
546
|
+
else {
|
|
547
|
+
gap = true;
|
|
548
|
+
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`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
console.log(gap
|
|
552
|
+
? ` ${dim('live view animates regardless; the placeholder only affects the static marketplace thumbnail.')}`
|
|
553
|
+
: ` ${g('✓ thumbnails are real renders')} ${dim('— served straight from the resolver.')}`);
|
|
554
|
+
await reportRemoteByteIntegrity(address, remote, base);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* The half `abx verify --remote` was missing: do the served BYTES still hash to the on-chain
|
|
558
|
+
* commitment? Everything above it checks renders (is a thumbnail current, is it a placeholder) — a
|
|
559
|
+
* different question, and a green ✓ there was reading as "the image is correct" when the bytes could
|
|
560
|
+
* genuinely mismatch.
|
|
561
|
+
*
|
|
562
|
+
* The service is the right place to answer it: it holds both the bytes and the chain, and it already
|
|
563
|
+
* exposes exactly this check (`GET /api/project/:addr/verify`, bearer-gated because it triggers
|
|
564
|
+
* outbound fetches). When we can't reach that — no credential, or an older node — say plainly that
|
|
565
|
+
* byte integrity was NOT checked rather than leaving the ✓ above to imply it was.
|
|
566
|
+
*/
|
|
567
|
+
export async function reportRemoteByteIntegrity(address, remote, base) {
|
|
568
|
+
if (!remote.token) {
|
|
569
|
+
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.`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
let report;
|
|
573
|
+
try {
|
|
574
|
+
const res = await fetch(`${base}/api/project/${address}/verify`, { headers: { authorization: `Bearer ${remote.token}` } });
|
|
575
|
+
if (res.status === 401 || res.status === 403) {
|
|
576
|
+
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).`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (!res.ok) {
|
|
580
|
+
// Spec'd as part of `abx-token-api/v1`, so a conforming service has it — but say it neutrally:
|
|
581
|
+
// this is equally "an older self-hosted node" and "a provider that didn't implement it".
|
|
582
|
+
warn(`byte integrity NOT checked — ${base} serves no /api/project/…/verify route (HTTP ${res.status}). ` +
|
|
583
|
+
`Your own node? Redeploy it (\`abx deploy-resolver\`). A provider's? It's part of abx-token-api/v1 — ask them for it. ` +
|
|
584
|
+
`Meanwhile ${bold(`abx verify ${address}`)} checks the bytes on a node that holds them.`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
report = (await res.json());
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
warn(`byte integrity NOT checked — couldn't reach ${base}'s verify route.`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const checked = (report.tokens ?? []).filter((t) => t.checks.length > 0);
|
|
594
|
+
if (checked.length === 0) {
|
|
595
|
+
info(`bytes ${dim('no on-chain byte commitment to check (this project commits no image hash)')}`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const bad = checked.filter((t) => t.checks.some((k) => !k.verified));
|
|
599
|
+
if (bad.length === 0) {
|
|
600
|
+
ok(`bytes: ${checked.length} token(s) hash-match their on-chain commitment ${dim('(what the service serves IS what the chain vouches for)')}`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const spec = remote.name?.toLowerCase() ?? base;
|
|
604
|
+
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 ` +
|
|
605
|
+
`${dim('(this is what a marketplace shows as a blank or placeholder image)')}. The chain is the truth, so the served copy is the wrong one. Two causes, two fixes:`);
|
|
606
|
+
info(`durable bytes exist (ipfs://, ar://) but weren't bridged → ${bold(`abx add ${address} --remote ${spec}`)} forwards the locator, then re-run this.`);
|
|
607
|
+
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.`);
|
|
608
|
+
process.exitCode = 1; // same rule as the local lane: an integrity mismatch fails the command
|
|
609
|
+
}
|
|
610
|
+
// ── state: a one-glance operational snapshot of a deployed contract, read straight from chain ──
|
|
611
|
+
// Read-only (no tx, no local index). Series-only getters revert on a 1/1, so each read is defensive
|
|
612
|
+
// → the same command works for both. The agent-friendly "what's the state before/after an op" call.
|
|
613
|
+
export const STATE_ABI = [
|
|
614
|
+
{ type: 'function', name: 'owner', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
615
|
+
{ type: 'function', name: 'totalSupply', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
|
|
616
|
+
{ type: 'function', name: 'nextTokenId', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
|
|
617
|
+
{ type: 'function', name: 'maxInvocations', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
|
|
618
|
+
{ type: 'function', name: 'paused', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
|
|
619
|
+
{ type: 'function', name: 'minter', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
620
|
+
{ type: 'function', name: 'primaryPayee', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
621
|
+
{ type: 'function', name: 'tokenURIRenderer', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
622
|
+
{ type: 'function', name: 'royaltyInfo', stateMutability: 'view', inputs: [{ type: 'uint256' }, { type: 'uint256' }], outputs: [{ type: 'address' }, { type: 'uint256' }] },
|
|
623
|
+
// ERC-721C (creator token) — ERC-165 advertises ICreatorToken ONLY when enrolled at deploy.
|
|
624
|
+
{ type: 'function', name: 'supportsInterface', stateMutability: 'view', inputs: [{ type: 'bytes4' }], outputs: [{ type: 'bool' }] },
|
|
625
|
+
{ type: 'function', name: 'getTransferValidator', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
626
|
+
];
|
|
627
|
+
export async function cmdState(address, flags) {
|
|
628
|
+
if (!address || address.startsWith('--')) {
|
|
629
|
+
console.error('usage: abx state <address> — a read-only, on-chain operational snapshot\n');
|
|
630
|
+
process.exitCode = 1;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// `--json` emits the same facts as data (B19: a value a program needs must be obtainable without
|
|
634
|
+
// parsing prose). The human narration below is unchanged and rides stderr under --json.
|
|
635
|
+
return withJson(flags, async (emit) => cmdStateBody(address, flags, emit));
|
|
636
|
+
}
|
|
637
|
+
export async function cmdStateBody(address, flags, emit) {
|
|
638
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
639
|
+
// getter absent (extension not composed) or contract has no code → undefined, never a throw.
|
|
640
|
+
const read = (functionName, args = []) => tryReadContract(publicClient, { address, abi: STATE_ABI, functionName, args });
|
|
641
|
+
const [owner, totalSupply, nextTokenId, maxInvocations, paused, minter, primaryPayee, renderer, royalty, creatorToken, transferValidator] = await Promise.all([
|
|
642
|
+
read('owner'),
|
|
643
|
+
read('totalSupply'),
|
|
644
|
+
read('nextTokenId'),
|
|
645
|
+
read('maxInvocations'),
|
|
646
|
+
read('paused'),
|
|
647
|
+
read('minter'),
|
|
648
|
+
read('primaryPayee'),
|
|
649
|
+
read('tokenURIRenderer'),
|
|
650
|
+
read('royaltyInfo', [0n, 10000n]),
|
|
651
|
+
read('supportsInterface', [CREATOR_TOKEN_INTERFACE_ID]),
|
|
652
|
+
read('getTransferValidator'),
|
|
653
|
+
]);
|
|
654
|
+
if (owner === undefined && totalSupply === undefined) {
|
|
655
|
+
throw new Error(`no ABX contract state at ${address} on ${CHAIN} (no code, or not an ABX token).`);
|
|
656
|
+
}
|
|
657
|
+
const isSeries = maxInvocations !== undefined; // series-only getter
|
|
658
|
+
const addrOrNone = (a) => (a && a !== zeroAddress ? a : dim('none'));
|
|
659
|
+
// The payload mirrors what is printed, with `undefined` (getter absent) kept distinct from a zero
|
|
660
|
+
// address (present and deliberately unset) — collapsing those is what makes a readout lie.
|
|
661
|
+
const zeroToNull = (a) => (a === undefined ? null : a === zeroAddress ? null : a);
|
|
662
|
+
const payload = jsonSafe({
|
|
663
|
+
address,
|
|
664
|
+
chain: CHAIN,
|
|
665
|
+
chainId: resolveChain(CHAIN).id,
|
|
666
|
+
type: isSeries ? 'series' : '1of1',
|
|
667
|
+
owner: owner ?? null,
|
|
668
|
+
totalSupply: totalSupply ?? null,
|
|
669
|
+
nextTokenId: nextTokenId ?? null,
|
|
670
|
+
maxInvocations: maxInvocations ?? null,
|
|
671
|
+
paused: paused ?? null,
|
|
672
|
+
minter: zeroToNull(minter),
|
|
673
|
+
primaryPayee: zeroToNull(primaryPayee),
|
|
674
|
+
tokenURIRenderer: zeroToNull(renderer),
|
|
675
|
+
onChainTokenUri: !!(renderer && renderer !== zeroAddress),
|
|
676
|
+
royalty: royalty ? { receiver: royalty[0], bps: Number(royalty[1]) } : null,
|
|
677
|
+
creatorToken: !!creatorToken,
|
|
678
|
+
transferValidator: creatorToken ? zeroToNull(transferValidator) : null,
|
|
679
|
+
params: { governed: [], contractScopeSet: [] },
|
|
680
|
+
});
|
|
681
|
+
emit(payload);
|
|
682
|
+
console.log(bold(`\n ${isSeries ? 'Series' : '1/1'} state`) + dim(` ${address} · ${CHAIN}`));
|
|
683
|
+
info(`owner ${owner ?? dim('?')}`);
|
|
684
|
+
if (isSeries) {
|
|
685
|
+
// Fully minted (totalSupply == maxInvocations) ⇒ fixed supply exhausted: the collection is
|
|
686
|
+
// COMPLETE and `unpause` is moot (nothing left for anyone to mint). Say so, so a fully-minted
|
|
687
|
+
// paused series doesn't read as "unfinished, go unpause".
|
|
688
|
+
const soldOut = totalSupply !== undefined && maxInvocations !== undefined && totalSupply >= maxInvocations;
|
|
689
|
+
info(`supply ${totalSupply ?? 0n} / ${maxInvocations} minted ${dim(soldOut ? '(complete — fixed supply fully minted)' : `(nextTokenId ${nextTokenId ?? 0n})`)}`);
|
|
690
|
+
info(`paused ${paused ? `${c.orange}true${c.reset} ${dim(soldOut ? '— moot: all tokens minted, nothing left to mint' : '— owner-only minting until `abx unpause`')}` : `false ${dim('— open to minter/public')}`}`);
|
|
691
|
+
info(`minter ${addrOrNone(minter)}${minter && minter !== zeroAddress ? '' : dim(' (owner-only)')}`);
|
|
692
|
+
info(`primary payee ${addrOrNone(primaryPayee)}`);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
info(`supply ${totalSupply ?? 0n} / 1 minted`);
|
|
696
|
+
}
|
|
697
|
+
if (royalty)
|
|
698
|
+
info(`royalty ${Number(royalty[1]) / 100}% → ${royalty[0]}`);
|
|
699
|
+
info(`renderer ${renderer && renderer !== zeroAddress ? `on-chain (${renderer})` : dim('off-chain (stored URI base / override)')}`);
|
|
700
|
+
// ERC-721C: shown ONLY for an enrolled collection — an unenrolled token is a plain ERC-721
|
|
701
|
+
// (indistinguishable from a pre-721C token) and shows nothing new here.
|
|
702
|
+
if (creatorToken) {
|
|
703
|
+
info(`721C validator ${transferValidator && transferValidator !== zeroAddress ? transferValidator : `${c.orange}suspended${c.reset} ${dim('(zero — enforcement off; re-enable with `abx set-transfer-validator`)')}`}`);
|
|
704
|
+
}
|
|
705
|
+
// The PostParam surface, read straight from chain: `paramSchemaKeys()` is the DECLARED (governed)
|
|
706
|
+
// set — append-only, and it includes keys declared but never yet written, which nothing off-chain
|
|
707
|
+
// could otherwise discover — and `contractParamKeys()` is the collection-scope set values. Both
|
|
708
|
+
// are maintained by the contract's own write paths, so there is no list to drift. Read-only,
|
|
709
|
+
// best-effort, and skipped entirely for a project that has none (a 1/1, a legacy pre-enumeration
|
|
710
|
+
// project, or a code project with no params).
|
|
711
|
+
try {
|
|
712
|
+
const keys = (await readParamSchemaKeys(publicClient, address)) ?? [];
|
|
713
|
+
const rows = keys.length
|
|
714
|
+
? await Promise.all(keys.map(async (key) => ({ key, s: await readParamSchema(publicClient, address, key) })))
|
|
715
|
+
: [];
|
|
716
|
+
const governed = rows.filter((r) => r.s.exists);
|
|
717
|
+
const now = Math.floor(Date.now() / 1000);
|
|
718
|
+
payload.params.governed = jsonSafe(governed.map(({ key, s }) => ({
|
|
719
|
+
key,
|
|
720
|
+
// Canonical NAMES, not the raw enum indices — a caller must not have to know the Solidity
|
|
721
|
+
// enum's ordering to read this (the same reason `retired` is a boolean below).
|
|
722
|
+
paramType: PARAM_TYPES[s.paramType] ?? String(s.paramType),
|
|
723
|
+
auth: AUTH_OPTIONS[s.auth] ?? String(s.auth),
|
|
724
|
+
authAddress: s.authAddress === zeroAddress ? null : s.authAddress,
|
|
725
|
+
lockAfter: s.lockAfter,
|
|
726
|
+
// A past lockAfter IS how a param is retired — surfaced as a boolean so a caller doesn't
|
|
727
|
+
// have to re-derive the convention from a timestamp.
|
|
728
|
+
retired: s.lockAfter !== 0 && s.lockAfter < now,
|
|
729
|
+
selectOptions: s.selectOptions,
|
|
730
|
+
})));
|
|
731
|
+
emit(payload);
|
|
732
|
+
if (governed.length) {
|
|
733
|
+
console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/artist set these; the generator injects them`)}`);
|
|
734
|
+
for (const { key, s } of governed) {
|
|
735
|
+
const retired = s.lockAfter !== 0 && s.lockAfter < now;
|
|
736
|
+
const locks = s.lockAfter !== 0 && !retired ? dim(` locks ${new Date(s.lockAfter * 1000).toISOString().slice(0, 10)}`) : '';
|
|
737
|
+
const tag = retired ? ` ${c.orange}retired${c.reset}${dim(' — writes revert')}` : locks;
|
|
738
|
+
console.log(` ${describeSchema({ key, paramType: s.paramType, auth: s.auth, authAddress: s.authAddress, lockAfter: 0, min: s.min, max: s.max, selectOptions: s.selectOptions })}${tag}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Collection-scope values (every token sees these). `seed` is per-token and never enumerated —
|
|
742
|
+
// it is a tokenData coordinate, read directly.
|
|
743
|
+
const set = await readSetParamKeys(publicClient, address);
|
|
744
|
+
if (set?.contract.length) {
|
|
745
|
+
payload.params.contractScopeSet = [...set.contract].sort();
|
|
746
|
+
emit(payload);
|
|
747
|
+
info(`${dim('contract-scope params set:')} ${[...set.contract].sort().join(', ')} ${dim('(apply to every token)')}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
/* best-effort — a non-code project has no params surface to read */
|
|
752
|
+
}
|
|
753
|
+
console.log('');
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* `abx status [address] [--remote [name|url]] [--watch]` — INDEXING status: where a project sits in
|
|
757
|
+
* the lifecycle (`queued | backfilling | live | stale | failed`) and how far behind head it is.
|
|
758
|
+
*
|
|
759
|
+
* One vocabulary for both sides of the membrane, which is the point: bare = this node, `--remote` =
|
|
760
|
+
* ask the service, and the same five words either way. Distinct from `abx state <address>`, which
|
|
761
|
+
* reads the CHAIN (owner, royalty, locks) and knows nothing about who is serving it.
|
|
762
|
+
*/
|
|
763
|
+
export async function cmdStatus(address, flags) {
|
|
764
|
+
// `abx status --remote <name>` has no address — without this guard the flag itself lands in
|
|
765
|
+
// rest[0] and gets sent as the address path segment (the same guard every other command applies).
|
|
766
|
+
if (address?.startsWith('--'))
|
|
767
|
+
address = undefined;
|
|
768
|
+
const remote = remoteFlag(flags);
|
|
769
|
+
if (remote)
|
|
770
|
+
return cmdStatusRemote(address, remote, flags);
|
|
771
|
+
const indexer = localIndexer();
|
|
772
|
+
const regs = indexer.store.listRegistrations();
|
|
773
|
+
if (address) {
|
|
774
|
+
const reg = indexer.store.getRegistration(address);
|
|
775
|
+
if (!reg)
|
|
776
|
+
throw new Error(`${address} isn't tracked by this node. Add it with \`abx add ${address}\`, or ask a service: \`abx status ${address} --remote <name>\``);
|
|
777
|
+
const s = indexer.getProject(address);
|
|
778
|
+
const row = indexer.indexStatus(address);
|
|
779
|
+
const head = indexer.store.getMeta(`watch:${reg.chainKey}:head`);
|
|
780
|
+
// Don't print the address twice when there's no name to lead with.
|
|
781
|
+
console.log(s?.name ? `\n ${bold(s.name)} ${dim(address)}` : `\n ${bold(address)}`);
|
|
782
|
+
statusRow('status', statusLine({
|
|
783
|
+
chainId: resolveChain(reg.chainKey).id,
|
|
784
|
+
address,
|
|
785
|
+
status: row.status,
|
|
786
|
+
fromBlock: reg.fromBlock,
|
|
787
|
+
toBlock: s?.toBlock ?? null,
|
|
788
|
+
headBlock: head,
|
|
789
|
+
eventCount: s?.eventCount ?? 0,
|
|
790
|
+
tokenCount: s?.tokens.length ?? 0,
|
|
791
|
+
mintedCount: 0,
|
|
792
|
+
reconstructedAt: s?.reconstructedAt ?? null,
|
|
793
|
+
...(row.errorClass ? { error: { class: row.errorClass, message: row.errorMessage ?? undefined } } : {}),
|
|
794
|
+
}));
|
|
795
|
+
statusRow('floor', `${reg.fromBlock}${row.attempts ? dim(` attempts ${row.attempts}`) : ''}`);
|
|
796
|
+
statusRow('indexed', s ? `${s.eventCount} events · ${s.tokens.length} token(s) ${dim(`· ${s.reconstructedAt}`)}` : dim('no projection yet'));
|
|
797
|
+
if (!head)
|
|
798
|
+
statusRow('head', dim("unknown — this node isn't watching the chain (ABX_WATCH_INTERVAL_MS=0, or `abx serve` isn't running)"));
|
|
799
|
+
console.log('');
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
console.log(bold(`\n ABX self-host node`));
|
|
803
|
+
info(`chain: ${CHAIN} · factory: ${factoryAddress() ?? 'none'} · storage: ${activeBackendId()} · data: ${indexer.store.path}`);
|
|
804
|
+
if (regs.length === 0) {
|
|
805
|
+
console.log(dim('\n No projects yet. `abx demo` to create one.\n'));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
console.log('');
|
|
809
|
+
for (const reg of regs) {
|
|
810
|
+
const s = indexer.getProject(reg.address);
|
|
811
|
+
const st = indexer.indexStatus(reg.address).status;
|
|
812
|
+
const mark = st === 'failed' || st === 'stale' ? `${c.orange}●${c.reset}` : s ? g('●') : dim('○');
|
|
813
|
+
const tail = s ? `${s.eventCount} events` : dim('(registered, not indexed)');
|
|
814
|
+
console.log(` ${mark} ${s?.name ?? reg.address} ${dim(reg.address)} ${tail} ${statusLabel(st)}`);
|
|
815
|
+
}
|
|
816
|
+
console.log(dim(`\n one project in detail: abx status <address>\n`));
|
|
817
|
+
}
|
|
818
|
+
/** The `--remote` half of {@link cmdStatus}: one project, or the roll-up for every project the token
|
|
819
|
+
* can see. `--watch` tails until everything reaches a terminal state. */
|
|
820
|
+
export async function cmdStatusRemote(address, remote, flags) {
|
|
821
|
+
requireRemoteToken(remote);
|
|
822
|
+
const client = serviceClient(remote);
|
|
823
|
+
const chainId = resolveChain(CHAIN).id;
|
|
824
|
+
const watch = flags.watch !== undefined;
|
|
825
|
+
const spec = remote.name ? remote.name.toLowerCase() : remote.source === 'default' ? '' : remote.url;
|
|
826
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(indexing status as the service reports it)')}`);
|
|
827
|
+
if (address) {
|
|
828
|
+
for (;;) {
|
|
829
|
+
let s;
|
|
830
|
+
try {
|
|
831
|
+
s = await client.projectStatus(chainId, address);
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
throw describeRemoteError(err, remote, 'remote status');
|
|
835
|
+
}
|
|
836
|
+
console.log(`\n ${bold(address)}`);
|
|
837
|
+
statusRow('status', statusLine(s));
|
|
838
|
+
statusRow('floor', `${s.fromBlock}${s.attempts ? dim(` attempts ${s.attempts}`) : ''}`);
|
|
839
|
+
statusRow('indexed', `${s.eventCount} events · ${s.tokenCount} token(s)${s.reconstructedAt ? dim(` · ${s.reconstructedAt}`) : ''}`);
|
|
840
|
+
// Spell out what `watching` MEANS on the line itself — a bare `no` sent a reviewer to a
|
|
841
|
+
// different command's output to find out whether it was a problem (it usually isn't).
|
|
842
|
+
if (s.watcher) {
|
|
843
|
+
statusRow('watching', s.watcher.watching
|
|
844
|
+
? `${g('yes')}${s.watcher.head ? dim(` · head ${s.watcher.head}`) : ''}${dim(' — it tails new blocks, so on-chain changes land on their own')}`
|
|
845
|
+
: `${dim('no')} ${dim('— this service updates on an explicit add/index, not by tailing new blocks (normal for many providers)')}`);
|
|
846
|
+
}
|
|
847
|
+
// Don't leave a creator staring at a red word with no next move. `failed` especially reads as
|
|
848
|
+
// terminal when it isn't — name what it means and the one command that follows it.
|
|
849
|
+
if (!watch && s.status !== 'live') {
|
|
850
|
+
if (s.status === 'failed' && s.error)
|
|
851
|
+
statusRow('what now', indexErrorAction(s.error.class));
|
|
852
|
+
statusRow('follow', dim(`this is not final — ${bold(`abx status ${address} --remote${spec ? ` ${spec}` : ''} --watch`)} tails it until it settles`));
|
|
853
|
+
}
|
|
854
|
+
console.log('');
|
|
855
|
+
if (!watch || s.status === 'live' || s.status === 'failed')
|
|
856
|
+
return;
|
|
857
|
+
await sleep(3000);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (;;) {
|
|
861
|
+
let projects;
|
|
862
|
+
try {
|
|
863
|
+
projects = await client.listProjects();
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
throw describeRemoteError(err, remote, 'remote status');
|
|
867
|
+
}
|
|
868
|
+
if (projects.length === 0) {
|
|
869
|
+
console.log(dim('\n no projects visible to this token\n'));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
console.log('');
|
|
873
|
+
for (const p of projects) {
|
|
874
|
+
console.log(` ${g('●')} ${p.name ?? p.label ?? p.address} ${dim(p.address)} ` +
|
|
875
|
+
`${p.status ? statusLabel(p.status) : dim('status not reported')}` +
|
|
876
|
+
`${p.error ? ` ${c.orange}${p.error.class}${c.reset}` : ''} ${dim(`${p.tokenCount ?? '?'} token(s)`)}`);
|
|
877
|
+
}
|
|
878
|
+
console.log(`\n ${rollUp(projects)}\n`);
|
|
879
|
+
const settled = projects.every((p) => !p.status || p.status === 'live' || p.status === 'failed');
|
|
880
|
+
if (!watch || settled)
|
|
881
|
+
return;
|
|
882
|
+
await sleep(3000);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// ── forget ────────────────────────────────────────────────────────────────--
|
|
886
|
+
// Drop a project this node tracks (registration + projection) — for cleaning up
|
|
887
|
+
// test/junk deploys. On-chain data is untouched; `abx add` can re-register it.
|
|
888
|
+
export async function cmdForget(address, flags) {
|
|
889
|
+
if (!address || address.startsWith('--')) {
|
|
890
|
+
console.error('usage: abx forget <address> [--remote [name|url]]\n');
|
|
891
|
+
process.exitCode = 1;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const remote = remoteFlag(flags);
|
|
895
|
+
if (remote) {
|
|
896
|
+
requireRemoteToken(remote);
|
|
897
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(deregistering on the remote resolver — NOT this machine)')}`);
|
|
898
|
+
let removed;
|
|
899
|
+
try {
|
|
900
|
+
({ removed } = await serviceClient(remote).removeProject(resolveChain(CHAIN).id, address));
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
throw describeRemoteError(err, remote, 'remote forget');
|
|
904
|
+
}
|
|
905
|
+
if (removed)
|
|
906
|
+
ok(`remote resolver forgot ${address} — it will stop serving it. On-chain data is untouched.`);
|
|
907
|
+
else
|
|
908
|
+
console.log(dim(` ${address} wasn't registered on ${remote.url} — nothing to forget.`));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const indexer = localIndexer();
|
|
912
|
+
if (!indexer.store.getRegistration(address)) {
|
|
913
|
+
console.log(dim(` ${address} isn't tracked by this node — nothing to forget.`));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
indexer.store.deregister(address);
|
|
917
|
+
ok(`forgot ${address} — dropped its registration + projection. On-chain data is untouched.`);
|
|
918
|
+
}
|
|
919
|
+
//# sourceMappingURL=project.js.map
|