@artblocks/abx-cli 0.1.0-alpha.16 → 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 +385 -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/jsonout.d.ts +37 -0
- package/dist/jsonout.d.ts.map +1 -0
- package/dist/jsonout.js +68 -0
- package/dist/jsonout.js.map +1 -0
- package/dist/main.js +133 -6223
- 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 +37 -44
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +274 -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 +30 -0
- package/dist/served.d.ts.map +1 -0
- package/dist/served.js +112 -0
- package/dist/served.js.map +1 -0
- 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 +9 -13
- package/skill/reference/code-projects.md +7 -6
- package/skill/reference/hosting.md +6 -6
- package/skill/reference/operating.md +2 -5
- package/skill/reference/setup.md +4 -3
- package/skill/reference/troubleshooting.md +14 -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/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,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only commands: `preview` (the studio lane — no chain), `inspect` (static analysis of a
|
|
3
|
+
* generative script + lane recommendation), `render` (effect-runner repair lane), `tokenuri` /
|
|
4
|
+
* `contracturi` (read directly from the contract, decode, optionally follow + report what's
|
|
5
|
+
* served), and `tokens` (every token's owner/seed/params, from chain alone).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { basename, resolve as resolvePath } from 'node:path';
|
|
9
|
+
import { SqliteStore } from '@artblocks/abx-indexer';
|
|
10
|
+
import { analyzeScript, listTokens, makePublicClient, oneOfOneImageAbi, recommendLane, redactRpcUrl, resolveRpcUrl, } from '@artblocks/abx-sdk';
|
|
11
|
+
import { resolveBackend } from '@artblocks/abx-storage';
|
|
12
|
+
import { resolveBaseUrl } from '@artblocks/abx-token-api';
|
|
13
|
+
import { CHAIN, storageOptions, storageOverrides } from '../config.js';
|
|
14
|
+
import { positionalArgs, warnStrayFlags } from '../flags.js';
|
|
15
|
+
import { assertPortFree, bold, c, dim, g, info, loadEffects, ok, requirePublishableBackend, step, warn } from '../output.js';
|
|
16
|
+
import { DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS, parsePreviewParams, previewConfigFromFlags, previewDepTags, shootPreview, startPreviewServer, } from '../preview.js';
|
|
17
|
+
import { remoteFlag, requireRemoteToken } from '../remote.js';
|
|
18
|
+
import { SCHEMA_CATALOG, describeSchema } from '../schema.js';
|
|
19
|
+
import { decodeOnChainJson, fetchServedTokenUri, prettyBody, servedOk } from '../served.js';
|
|
20
|
+
/**
|
|
21
|
+
* `abx preview` — serve the program on localhost, live, for as long as the work is being made.
|
|
22
|
+
*
|
|
23
|
+
* The studio lane, and deliberately the FIRST thing to reach for on a code project: it renders the
|
|
24
|
+
* same document the generator serves (real `abx.js`, real tokenData shape, real dependency tags)
|
|
25
|
+
* with a synthetic seed, so a creator can refresh for new seeds, drive their PostParams from real
|
|
26
|
+
* inputs, and watch an animated piece actually move — none of which a still-image sweep can show.
|
|
27
|
+
* No chain, no key, no deploy. `--shoot` renders the same document headlessly for an agent that
|
|
28
|
+
* can't open a browser.
|
|
29
|
+
*/
|
|
30
|
+
export async function cmdPreview(flags) {
|
|
31
|
+
warnStrayFlags(flags, PREVIEW_FLAGS, 'preview');
|
|
32
|
+
const cfg = previewConfigFromFlags(flags);
|
|
33
|
+
const shootDir = flags.shoot && flags.shoot !== 'true' ? String(flags.shoot) : flags.shoot === 'true' ? 'abx-preview' : undefined;
|
|
34
|
+
const count = Math.min(Math.max(Number(flags.count ?? 9) || 9, 1), 64);
|
|
35
|
+
console.log(bold(`\n ABX Self-Host Toolkit — preview\n ${dim('the program, running locally — no chain, no deploy')}`));
|
|
36
|
+
const { notes } = previewDepTags(cfg.deps);
|
|
37
|
+
step('Program');
|
|
38
|
+
info(cfg.source.kind === 'dir' ? `directory build ${cfg.source.path}/ (its own index.html + abx.js)` : `script ${cfg.source.path} ${dim('(re-read from disk on every render)')}`);
|
|
39
|
+
if (cfg.schemas.length)
|
|
40
|
+
info(`params: ${cfg.schemas.map(describeSchema).join(' · ')}`);
|
|
41
|
+
else
|
|
42
|
+
info(dim('params: none declared — add --schema key:Type:Auth to drive them from the studio'));
|
|
43
|
+
for (const n of notes)
|
|
44
|
+
info(`dep ${n}`);
|
|
45
|
+
// A raw on-chain dependency can't be fetched without a chain, so the preview would render a
|
|
46
|
+
// sketch missing its runtime and look broken for the wrong reason. Say so rather than let them
|
|
47
|
+
// debug their own art.
|
|
48
|
+
if (cfg.deps.some((d) => d.display.startsWith('0x'))) {
|
|
49
|
+
warn('an on-chain data-contract dep is NOT loaded in preview — the sketch will run without it here. Use a name@version ref to preview against the CDN copy.');
|
|
50
|
+
}
|
|
51
|
+
// `--shoot` takes an ephemeral port (0), so only the studio lane can collide.
|
|
52
|
+
const previewPort = shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT);
|
|
53
|
+
if (previewPort !== 0)
|
|
54
|
+
await assertPortFree(previewPort, 'preview');
|
|
55
|
+
const server = await startPreviewServer(cfg, previewPort);
|
|
56
|
+
if (shootDir) {
|
|
57
|
+
step(`Render ${count} seeds headlessly`);
|
|
58
|
+
try {
|
|
59
|
+
const shotParams = parsePreviewParams(flags.param);
|
|
60
|
+
if (Object.keys(shotParams).length) {
|
|
61
|
+
info(`param overrides applied to every frame: ${Object.entries(shotParams).map(([k, v]) => `${k}=${v || dim('(unset)')}`).join(' · ')}`);
|
|
62
|
+
}
|
|
63
|
+
const shots = await shootPreview(server.url, shootDir, count, {
|
|
64
|
+
width: Number(flags.width ?? 1000) || 1000,
|
|
65
|
+
timeoutMs: Number(flags['timeout-ms'] ?? 10_000) || 10_000,
|
|
66
|
+
params: shotParams,
|
|
67
|
+
});
|
|
68
|
+
ok(`${shots.length} frames → ${shootDir}/ ${dim('(traits in traits.json)')}`);
|
|
69
|
+
for (const s of shots) {
|
|
70
|
+
const t = s.traits && Object.keys(s.traits).length
|
|
71
|
+
? Object.entries(s.traits).map(([k, v]) => `${k} ${String(v)}`).join(' · ')
|
|
72
|
+
: s.timedOut
|
|
73
|
+
? `${c.orange}timed out — traits unknown${c.reset}`
|
|
74
|
+
: `${c.orange}no traits reported${c.reset}`;
|
|
75
|
+
console.log(` ${dim(s.seed.slice(0, 10) + '…')} ${t}${s.done || s.timedOut ? '' : dim(' (no abx.done())')}`);
|
|
76
|
+
}
|
|
77
|
+
const timedOut = shots.filter((s) => s.timedOut).length;
|
|
78
|
+
const silent = shots.filter((s) => !s.traits || !Object.keys(s.traits).length).length;
|
|
79
|
+
// A timeout is a measurement failure, not a finding about the program — so it must never be
|
|
80
|
+
// reported as one. The old code printed the "NO frame reported traits" alarm whenever traits
|
|
81
|
+
// came back empty for any reason, which condemned a CORRECT program on a loaded machine and
|
|
82
|
+
// inverted the one line agents are told to trust.
|
|
83
|
+
if (timedOut) {
|
|
84
|
+
warn(`${timedOut} of ${shots.length} frame(s) hit the ${Math.round((Number(flags['timeout-ms'] ?? 10_000) || 10_000) / 1000)}s wait with nothing reported — ` +
|
|
85
|
+
`that is a TIMEOUT, not a verdict on the program. Re-run with ${bold('--timeout-ms 30000')} (or on an idle machine) before believing anything about its traits.`);
|
|
86
|
+
}
|
|
87
|
+
if (silent === shots.length && !timedOut) {
|
|
88
|
+
warn('NO frame reported traits — `abx.traits({…})` is the only thing that becomes marketplace `attributes`. Verify with `abx inspect`.');
|
|
89
|
+
}
|
|
90
|
+
else if (silent < shots.length && shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
|
|
91
|
+
// Only meaningful when traits DID come back: identical values across seeds is the signature
|
|
92
|
+
// of a sketch that never reads `abx.tokenData.seed` (prototyped on Math.random()), which
|
|
93
|
+
// deploys as N visually identical tokens. Skipped when nothing reported at all — the
|
|
94
|
+
// warning above already covers that, and firing both reads as noise.
|
|
95
|
+
warn('every seed produced identical traits — check the sketch actually reads `abx.tokenData.seed` (the silent "all tokens the same" failure).');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await server.close();
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
step('Studio');
|
|
104
|
+
console.log(`\n ${g('●')} ${bold('preview')} ${server.url}`);
|
|
105
|
+
console.log(` ${dim('studio ')}${server.url} ${dim('seed + params + live traits')}`);
|
|
106
|
+
console.log(` ${dim('grid ')}${server.url}/grid ${dim('9 seeds at once, all live')}`);
|
|
107
|
+
console.log(` ${dim('bare view ')}${server.url}/view ${dim('the generator document itself')}`);
|
|
108
|
+
console.log(`\n ${dim('Edit the program and refresh — it is re-read from disk. Ctrl-C to stop.')}`);
|
|
109
|
+
console.log(` ${dim('This is a preview: `abx inspect` is still the wiring check, and a testnet deploy is the faithful end-to-end.')}\n`);
|
|
110
|
+
await new Promise(() => { }); // block like `serve` — the creator drives it
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `abx inspect <script.js>` — read a generative script and, WITHOUT executing it, report what it
|
|
114
|
+
* needs (traits + their on-chain reproducibility, dependency hints, size → assembled-document size →
|
|
115
|
+
* single-`tokenURI`-eth_call viability) and RECOMMEND a lane. The decision-tree entry point: run it
|
|
116
|
+
* before choosing `--onchain-uri` vs a resolver vs directory mode, so the choice is derived, not guessed.
|
|
117
|
+
*/
|
|
118
|
+
export async function cmdInspect(path, flags) {
|
|
119
|
+
if (!path || path.startsWith('--')) {
|
|
120
|
+
console.error('usage: abx inspect <script.js> [--dep <name@version>[,…]] (static analysis + a lane recommendation — abx inspect --help)\n');
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// `abx inspect` analyzes a JS generative SCRIPT. A creator on the in-chain Solidity lane naturally
|
|
125
|
+
// tries `abx inspect <renderer address>` — redirect them instead of crashing with a raw ENOENT.
|
|
126
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(path)) {
|
|
127
|
+
console.error(`abx inspect analyzes a JS generative SCRIPT file, not an address. For a Solidity renderer (an in-chain SVG/traits contract) there's nothing to statically analyze here — deploy it, then pass its address to ${bold('abx deploy-code --image-renderer 0x..')} / ${bold('--attributes-renderer 0x..')} (deploy-code verifies the address has code).\n`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const abs = resolvePath(path);
|
|
132
|
+
if (!existsSync(abs)) {
|
|
133
|
+
console.error(`abx inspect: no such file '${path}'. It takes a JS generative script (e.g. sketch.js). For a Solidity renderer, pass its DEPLOYED address to ${bold('abx deploy-code --image-renderer/--attributes-renderer')} instead.\n`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const source = readFileSync(abs, 'utf8');
|
|
138
|
+
const declaredDeps = String(flags.dep ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
139
|
+
const a = analyzeScript(source, declaredDeps);
|
|
140
|
+
console.log(bold(`\n abx inspect — ${basename(path)} ${dim('(static analysis; the script is never executed)')}`));
|
|
141
|
+
step('Program');
|
|
142
|
+
info(`size: ${a.bytes} bytes → ${a.estChunks} on-chain chunk(s)`);
|
|
143
|
+
info(`libraries: ${a.depHints.length
|
|
144
|
+
? a.depHints.join(', ') + (declaredDeps.length ? '' : dim(' — declare on-chain with --dep <name@version>'))
|
|
145
|
+
: 'none detected'}`);
|
|
146
|
+
step('Runtime data contract');
|
|
147
|
+
if (a.runtime.readsTokenData) {
|
|
148
|
+
ok(`reads state via ${bold('abx.tokenData')}${a.runtime.reportsTraits ? ` and reports traits via ${bold('abx.traits({…})')}` : ''} — the recognized accessor(s).`);
|
|
149
|
+
if (!a.runtime.reportsTraits)
|
|
150
|
+
info(`no ${bold('abx.traits({…})')} call — fine if this piece has no marketplace traits; add one if you want filterable traits (see below).`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// The silent-breaker: a fresh sketch that invents its own global deploys + renders without error
|
|
154
|
+
// but never receives the seed/params. This is the #1 authoring trap the skill's contract prevents.
|
|
155
|
+
warn(`this script never reads the abx token data via a recognized accessor (${bold('abx.tokenData')} or ${bold('window.abxTokenData')}).`);
|
|
156
|
+
if (a.runtime.wrongGlobal)
|
|
157
|
+
warn(` found instead: ${bold(a.runtime.wrongGlobal)}.`);
|
|
158
|
+
info(`the mint ${bold('seed')} and collector ${bold('params')} will NOT reach it → every token renders IDENTICALLY, and traits stay empty. Read state the one supported way: ${bold('var td = (window.abx && abx.tokenData) || {}')} then ${bold('td.seed')} / ${bold('td.<paramKey>')} (flat), report with ${bold('abx.traits({…})')}. Contract + a worked example: the skill's "Authoring the program" section.`);
|
|
159
|
+
}
|
|
160
|
+
step('Traits');
|
|
161
|
+
if (a.traits.present) {
|
|
162
|
+
info(`reports ${a.traits.keys.length}: ${a.traits.keys.join(', ')}`);
|
|
163
|
+
const line = `on-chain reproducibility: ${bold(a.feasibility.verdict)} — ${a.feasibility.reason}`;
|
|
164
|
+
if (a.feasibility.verdict === 'exact-likely')
|
|
165
|
+
ok(line);
|
|
166
|
+
else if (a.feasibility.verdict === 'infeasible')
|
|
167
|
+
warn(line);
|
|
168
|
+
else
|
|
169
|
+
info(line);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
warn(a.feasibility.reason);
|
|
173
|
+
}
|
|
174
|
+
step('PostParams (collector-set inputs)');
|
|
175
|
+
if (a.paramHints.length) {
|
|
176
|
+
warn(`the script READS ${a.paramHints.length} param(s): ${bold(a.paramHints.join(', '))}`);
|
|
177
|
+
info(`declare EACH at deploy or it's silently dropped (render sees undefined → its default): ${bold(`--schema ${a.paramHints.map((k) => `${k}:<Type>:<Auth>`).join(',')}`)} ${dim(SCHEMA_CATALOG)}`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// "none detected" is a HINT, not proof — the scan only sees params read as `abx.tokenData.<key>`
|
|
181
|
+
// (flat) or destructured from it. A creator who reads them a different way (a nested `.params`,
|
|
182
|
+
// an invented global) gets a false "none" and drops the --schema. Say so, don't reassure.
|
|
183
|
+
info(`none detected reading ${bold('abx.tokenData.<key>')} (flat) or destructuring from it. If you INTENDED a collector param (e.g. a palette) but see this, you're likely reading it a different way — read it as ${bold('abx.tokenData.<key>')} and re-run, or declare it explicitly with ${bold('--schema')} or it's dropped at render.`);
|
|
184
|
+
}
|
|
185
|
+
step('On-chain document — RPC viability');
|
|
186
|
+
info(`estimated assembled document: ~${Math.round(a.doc.estBytes / 1000)}KB` +
|
|
187
|
+
`${a.doc.unknownDepSizes ? ' + undeclared/unknown dep bytes' : ''} ` +
|
|
188
|
+
dim(`(script + runtime${a.doc.deps.length ? ' + ' + a.doc.deps.join('+') : ''})`));
|
|
189
|
+
if (a.doc.fitsSingleCall)
|
|
190
|
+
ok('fits a single tokenURI eth_call — the template (fully-on-chain) branch is viable.');
|
|
191
|
+
else
|
|
192
|
+
warn('may exceed a single tokenURI eth_call — prefer directory mode, a CDN dep, or the generator piecewise getters.');
|
|
193
|
+
step('Recommended lane');
|
|
194
|
+
console.log(` ${bold(recommendLane(a))}`);
|
|
195
|
+
console.log(` ${dim('trait feasibility deep-dive: docs/research/onchain-traits-feasibility.md · all lanes: abx deploy-code --help')}\n`);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* `abx render <address> [tokenId…]` — the ops/repair lane of the effect runner
|
|
199
|
+
* (`specs/protocol/effects.md → The reference runner`). Same code path as the deployed
|
|
200
|
+
* service, so a hand-fix and the daemon can't diverge: with a runner up
|
|
201
|
+
* (ABX_EFFECTS_URL / --effects-url) this enqueues via its `POST /run`; otherwise it
|
|
202
|
+
* runs the sweep inline (which needs Playwright installed locally — the error says how).
|
|
203
|
+
* Idempotent either way: only missing artifacts at the CURRENT inputsHash render.
|
|
204
|
+
*/
|
|
205
|
+
export async function cmdRender(address, tokenIds, flags) {
|
|
206
|
+
if (!address || address.startsWith('--')) {
|
|
207
|
+
console.error('usage: abx render <address> [tokenId…] [--force] [--remote [name|url]] (missing stills/traits; --force re-renders an existing one — abx render --help)\n');
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Token ids are decimal (mint order). Keep only those — `parseFlags` leaves `--flag value` pairs
|
|
212
|
+
// in `rest`, so a flag's VALUE (e.g. the `--remote <url>`) would otherwise be read as a bogus id
|
|
213
|
+
// and make the sweep match nothing (a silent `ran=0`).
|
|
214
|
+
const ids = tokenIds.filter((t) => /^\d+$/.test(t));
|
|
215
|
+
// --force re-renders even when the still already exists at the current inputsHash — the repair
|
|
216
|
+
// lane for a bad/blank/timed-out capture (the art is otherwise deterministic, so a plain render
|
|
217
|
+
// idempotent-skips). Overwrites the artifact (and republishes it on --remote).
|
|
218
|
+
const force = !!flags.force;
|
|
219
|
+
const runnerUrl = flags['effects-url'] ?? process.env.ABX_EFFECTS_URL;
|
|
220
|
+
if (runnerUrl) {
|
|
221
|
+
let res;
|
|
222
|
+
try {
|
|
223
|
+
res = await fetch(`${runnerUrl.replace(/\/$/, '')}/run`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'content-type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ address, tokenIds: ids.length ? ids : undefined, force }),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
// A bare "fetch failed" (ECONNREFUSED) here means nothing is LISTENING at the configured
|
|
231
|
+
// effects URL — name it and give the two ways out, rather than a contentless error.
|
|
232
|
+
throw new Error(`no effect runner reachable at ${runnerUrl} (${err.message}). ` +
|
|
233
|
+
`Start one there with \`abx effects\`, or unset ABX_EFFECTS_URL (and drop --effects-url) to render inline on this machine.`);
|
|
234
|
+
}
|
|
235
|
+
if (!res.ok)
|
|
236
|
+
throw new Error(`effect runner at ${runnerUrl} replied ${res.status} ${(await res.text().catch(() => '')).slice(0, 200)}`);
|
|
237
|
+
const stats = (await res.json());
|
|
238
|
+
const line = `runner: ran=${stats.ran} skipped=${stats.skipped} failed=${stats.failed} ${dim(`(${runnerUrl})`)}`;
|
|
239
|
+
if (stats.failed)
|
|
240
|
+
warn(`${line}\n ${dim(stats.errors?.[0] ?? 'see runner logs')}`);
|
|
241
|
+
else
|
|
242
|
+
ok(line);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// --remote publishes each render to the remote resolver's control plane (the locator bridge) so a
|
|
246
|
+
// laptop render lands on a resolver that doesn't share this disk; republish=true makes a re-run
|
|
247
|
+
// restore a resolver that lost its volume without re-rendering. Local (no --remote): shared backend.
|
|
248
|
+
const remote = remoteFlag(flags);
|
|
249
|
+
const resolverUrl = (remote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
|
|
250
|
+
const adminToken = remote ? requireRemoteToken(remote) : undefined;
|
|
251
|
+
// Refuse the combination that can't work before launching Chromium (a laptop render against a
|
|
252
|
+
// hosted resolver, on a backend with no public URL) — the render would end in a 400 either way.
|
|
253
|
+
if (remote)
|
|
254
|
+
requirePublishableBackend(flags, 'abx render --remote');
|
|
255
|
+
// Co-located (no --remote): record each declared output into the shared store's artifact
|
|
256
|
+
// registry so the local resolver's `artifacts` manifest enumerates it. Remote: the publish
|
|
257
|
+
// lane (adminToken) records rows on the hosted resolver instead.
|
|
258
|
+
const localStore = remote ? null : new SqliteStore();
|
|
259
|
+
const { EffectRunner, renderEffect } = await loadEffects();
|
|
260
|
+
const runner = new EffectRunner({
|
|
261
|
+
resolverUrl,
|
|
262
|
+
client: makePublicClient({ chainKey: CHAIN }),
|
|
263
|
+
storage: resolveBackend(storageOptions(storageOverrides(flags))),
|
|
264
|
+
effects: [renderEffect()],
|
|
265
|
+
environmentId: process.env.ABX_ENVIRONMENT_ID ?? 'web:any',
|
|
266
|
+
adminToken,
|
|
267
|
+
republish: remote ? true : undefined,
|
|
268
|
+
recordArtifact: localStore ? (row) => localStore.putEffectArtifact(row) : undefined,
|
|
269
|
+
});
|
|
270
|
+
const stats = await runner.sweepProject(address, ids.length ? ids : undefined, { force });
|
|
271
|
+
const where = remote ? `published → ${resolverUrl}` : `resolver ${resolverUrl}`;
|
|
272
|
+
const forceNote = force ? ' (forced re-render)' : '';
|
|
273
|
+
const summary = `inline: ran=${stats.ran} skipped=${stats.skipped} failed=${stats.failed}${forceNote} ${dim(`(${where})`)}`;
|
|
274
|
+
if (stats.failed)
|
|
275
|
+
warn(`${summary}\n ${dim(stats.errors[0] ?? 'see error above')}`);
|
|
276
|
+
else
|
|
277
|
+
ok(summary);
|
|
278
|
+
if (stats.ran)
|
|
279
|
+
noteArweavePropagation(flags);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* After an Arweave publish, say that a fresh 404 at the gateway is PROPAGATION, not a failed render.
|
|
283
|
+
*
|
|
284
|
+
* `arweave.net` lags Turbo uploads by minutes: a tester found 32/32 of their renders 404ing there
|
|
285
|
+
* while Turbo reported CONFIRMED and 22/32 already served fine from other ar.io gateways. The bytes
|
|
286
|
+
* were never in doubt — only the gateway was behind. Unexplained, a "broken" thumbnail on a fresh
|
|
287
|
+
* drop reads as a failed render, and the natural next move is `abx render --force` on all of them:
|
|
288
|
+
* a full re-upload that fixes nothing.
|
|
289
|
+
*
|
|
290
|
+
* Note this is advisory only, and deliberately so — the locator is baked into what the resolver
|
|
291
|
+
* registers at publish time, so it cannot be repaired by a redirect later. Choosing the gateway is
|
|
292
|
+
* the operator's call (`ABX_ARWEAVE_GATEWAY`), which is why this names it.
|
|
293
|
+
*/
|
|
294
|
+
export function noteArweavePropagation(flags) {
|
|
295
|
+
const opts = storageOptions(storageOverrides(flags));
|
|
296
|
+
if (resolveBackend(opts).id !== 'arweave')
|
|
297
|
+
return;
|
|
298
|
+
const gateway = opts.arweave?.gateway ?? 'https://arweave.net';
|
|
299
|
+
if (!/(^|\/\/)([^/]*\.)?arweave\.net/.test(gateway))
|
|
300
|
+
return; // a gateway they chose — don't lecture
|
|
301
|
+
info(`${dim('arweave: the locator points at')} ${gateway}${dim(', which can 404 for several minutes after upload while it catches up. That is PROPAGATION, not a failed render — the bytes are already confirmed. Do NOT re-run with')} ${bold('--force')}${dim('; the URL starts working on its own.')}`);
|
|
302
|
+
info(` ${dim('to bake a different gateway into the locator instead (it is fixed at publish time):')} ${bold('ABX_ARWEAVE_GATEWAY=https://<gateway>')} ${dim('before you render.')}`);
|
|
303
|
+
// The note is advisory; this is the answer. Naming it here is the difference between "wait and
|
|
304
|
+
// hope" and a check a script can gate on — which is what both reporters ended up building.
|
|
305
|
+
info(` ${dim('to KNOW rather than wait:')} ${bold('abx storage status <locator>')} ${dim('— ready vs propagating vs unreachable, exits non-zero until it serves.')}`);
|
|
306
|
+
}
|
|
307
|
+
export async function cmdTokenUri(address, flags, extra = []) {
|
|
308
|
+
if (!address || address.startsWith('--')) {
|
|
309
|
+
console.error('usage: abx tokenuri <address> [--token <id>] [--fetch] [--json]\n');
|
|
310
|
+
process.exitCode = 1;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
314
|
+
console.error(`abx tokenuri: '${address}' isn't a 0x contract address.\n`);
|
|
315
|
+
process.exitCode = 1;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// `abx tokenuri <addr> 0` silently ignored the `0` and printed token 0 — a COINCIDENTALLY correct
|
|
319
|
+
// answer, which is the dangerous kind: `… <addr> 7` would have printed token 0 just as confidently
|
|
320
|
+
// and exited 0. The token id is a flag here, so name it rather than guessing at intent.
|
|
321
|
+
const strayPositionals = positionalArgs(extra);
|
|
322
|
+
if (strayPositionals.length) {
|
|
323
|
+
const first = strayPositionals[0];
|
|
324
|
+
const looksLikeTokenId = /^\d+$/.test(first);
|
|
325
|
+
console.error(`abx tokenuri: unexpected extra argument '${first}'.` +
|
|
326
|
+
(looksLikeTokenId
|
|
327
|
+
? ` The token id is a flag — did you mean:\n abx tokenuri ${address} --token ${first}\n`
|
|
328
|
+
: `\n usage: abx tokenuri <address> [--token <id>] [--fetch] [--json]\n`));
|
|
329
|
+
process.exitCode = 1;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const tokenId = BigInt(flags.token ?? '0');
|
|
333
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
334
|
+
// A creator verifying their work often runs this on a wrong / not-yet-mined address — turn viem's
|
|
335
|
+
// raw revert (with its viem.sh docs link) into a black-box-clean, actionable message.
|
|
336
|
+
let uri;
|
|
337
|
+
try {
|
|
338
|
+
uri = (await publicClient.readContract({
|
|
339
|
+
address,
|
|
340
|
+
abi: oneOfOneImageAbi,
|
|
341
|
+
functionName: 'tokenURI',
|
|
342
|
+
args: [tokenId],
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
const code = await publicClient.getCode({ address }).catch(() => undefined);
|
|
347
|
+
if (!code || code === '0x') {
|
|
348
|
+
// Name the endpoint we actually asked. "No contract here" is indistinguishable from "you're
|
|
349
|
+
// pointed at the wrong node", and a chain KEY doesn't disambiguate that — two endpoints can
|
|
350
|
+
// both claim `sepolia` (a fork, a stale duplicate .env line) and only one has your contract.
|
|
351
|
+
console.error(`abx tokenuri: no contract at ${address} on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}) — ` +
|
|
352
|
+
`double-check the address, and that this endpoint is the network you deployed to. ` +
|
|
353
|
+
`If you JUST deployed, give the tx a block or two to mine.\n`);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
console.error(`abx tokenuri: ${address} didn't return a tokenURI for token ${tokenId} — it may not be an ABX/ERC-721 token, token ${tokenId} may be unminted (try --token <id>), or — on a large on-chain tokenURI — an unauthenticated RPC read hit its gas cap (try a wallet-connected / high-gas RPC).\n`);
|
|
357
|
+
}
|
|
358
|
+
process.exitCode = 1;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// --fetch: FOLLOW the URL the contract commits to and print what is actually served (backlog B14).
|
|
362
|
+
// The gap this closes: `tokenuri` read the chain, `verify` re-hashed bytes, `status` reported the
|
|
363
|
+
// lifecycle — none of them showed the served JSON body, so an agent in a cold sweep fell back to raw
|
|
364
|
+
// `curl`. It also makes a provider mismatch self-evident without a warning that cries wolf: if you
|
|
365
|
+
// registered with one provider while a different base is baked on-chain, the fetch shows you what
|
|
366
|
+
// the BAKED base returns (often a 404, or another project's document entirely).
|
|
367
|
+
const served = flags.fetch !== undefined ? await fetchServedTokenUri(uri) : null;
|
|
368
|
+
// --json: the verbatim document, nothing else on stdout — no banner, no ANSI, no truncation, so
|
|
369
|
+
// `abx tokenuri <addr> --json | jq` is a supported read path rather than something to regex.
|
|
370
|
+
if (flags.json !== undefined) {
|
|
371
|
+
const onChain = decodeOnChainJson(uri, true);
|
|
372
|
+
if (served === null) {
|
|
373
|
+
process.stdout.write((onChain ?? uri) + '\n');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// With --fetch the payload has to say WHICH answer is which: a marketplace reads the served body,
|
|
377
|
+
// and conflating it with the on-chain URI is the confusion the flag exists to remove.
|
|
378
|
+
process.stdout.write(JSON.stringify({
|
|
379
|
+
tokenId: tokenId.toString(),
|
|
380
|
+
contract: address,
|
|
381
|
+
chain: CHAIN,
|
|
382
|
+
tokenURI: uri,
|
|
383
|
+
onChain: onChain !== null,
|
|
384
|
+
served,
|
|
385
|
+
}, null, 2) + '\n');
|
|
386
|
+
if (served.status !== null && (served.status < 200 || served.status >= 300))
|
|
387
|
+
process.exitCode = 1;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
console.log(`\n ${bold(`tokenURI(${tokenId})`)} ${dim(`— read directly from ${address} on ${CHAIN}, no server`)}`);
|
|
391
|
+
const json = decodeOnChainJson(uri);
|
|
392
|
+
if (json) {
|
|
393
|
+
info('decoded on-chain JSON:');
|
|
394
|
+
console.log(json.split('\n').map((l) => ' ' + l).join('\n') + '\n');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(` ${uri.slice(0, 240)}${uri.length > 240 ? dim(`… (${uri.length} chars)`) : ''}\n`);
|
|
398
|
+
}
|
|
399
|
+
if (served)
|
|
400
|
+
reportServedTokenUri(served, address);
|
|
401
|
+
}
|
|
402
|
+
/** The human readout for `--fetch`. */
|
|
403
|
+
export function reportServedTokenUri(served, address) {
|
|
404
|
+
if (served.skipped) {
|
|
405
|
+
info(`--fetch: ${served.skipped}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
console.log(` ${bold('served')} ${dim(`— GET ${served.url}`)}`);
|
|
409
|
+
if (served.status === null) {
|
|
410
|
+
warn(`the baked URL did not respond: ${served.error}. That host is what a marketplace will ask, so this is what they see too.`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const line = `HTTP ${served.status}${served.contentType ? dim(` · ${served.contentType.split(';')[0]}`) : ''}`;
|
|
414
|
+
if (servedOk(served))
|
|
415
|
+
ok(line);
|
|
416
|
+
else
|
|
417
|
+
console.log(` ${c.orange}\u26a0${c.reset} ${line}`);
|
|
418
|
+
const pretty = prettyBody(served.body);
|
|
419
|
+
const clipped = pretty.length > 1200 ? `${pretty.slice(0, 1200)}\n \u2026 (${pretty.length} chars \u2014 --json for the full body)` : pretty;
|
|
420
|
+
console.log(clipped.split('\n').map((l) => ' ' + l).join('\n'));
|
|
421
|
+
if (!servedOk(served)) {
|
|
422
|
+
// The mismatch case, stated where it is actionable rather than as a standing warning that would
|
|
423
|
+
// cry wolf on the common custom-domain setup.
|
|
424
|
+
console.log(`\n ${dim('the contract commits to that URL, so a non-2xx is about the SERVICE, never a mistyped path:')} ` +
|
|
425
|
+
`${dim('the project may not be registered there, the service may serve a different chain, or the base baked on-chain may point at a provider you never registered with.')}\n` +
|
|
426
|
+
` ${dim('check the lifecycle:')} ${bold(`abx status ${address} --remote <name|url>`)}`);
|
|
427
|
+
}
|
|
428
|
+
console.log('');
|
|
429
|
+
}
|
|
430
|
+
// ── contracturi ──────────────────────────────────────────────────────────────
|
|
431
|
+
/**
|
|
432
|
+
* `abx contracturi <address>` — the collection-level counterpart of `tokenuri`: read
|
|
433
|
+
* `contractURI()` (ERC-7572) STRAIGHT FROM THE CONTRACT, then FOLLOW it and decode the JSON.
|
|
434
|
+
*
|
|
435
|
+
* Why this exists, and why it follows the URL: the resolver's route grammar is committed
|
|
436
|
+
* on-chain at deploy (`contractURIBase` = `<baseUrl>/c`), so the contract — not a doc, not a
|
|
437
|
+
* service descriptor — is the source of truth for where a collection's metadata lives. Without
|
|
438
|
+
* this command the only way to look was to hand-build the URL from memory of the grammar, and a
|
|
439
|
+
* guessed path that 404s reads exactly like a broken service. Ask the chain instead.
|
|
440
|
+
*/
|
|
441
|
+
export async function cmdContractUri(address, _flags) {
|
|
442
|
+
if (!address || address.startsWith('--')) {
|
|
443
|
+
console.error('usage: abx contracturi <address>\n');
|
|
444
|
+
process.exitCode = 1;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
448
|
+
console.error(`abx contracturi: '${address}' isn't a 0x contract address.\n`);
|
|
449
|
+
process.exitCode = 1;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
453
|
+
let uri;
|
|
454
|
+
try {
|
|
455
|
+
uri = (await publicClient.readContract({
|
|
456
|
+
address,
|
|
457
|
+
abi: oneOfOneImageAbi,
|
|
458
|
+
functionName: 'contractURI',
|
|
459
|
+
args: [],
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
const code = await publicClient.getCode({ address }).catch(() => undefined);
|
|
464
|
+
if (!code || code === '0x') {
|
|
465
|
+
// Name the endpoint we actually asked — "no contract here" and "you're pointed at the wrong
|
|
466
|
+
// node" are indistinguishable otherwise (same reasoning as `tokenuri`).
|
|
467
|
+
console.error(`abx contracturi: no contract at ${address} on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}) — ` +
|
|
468
|
+
`double-check the address, and that this endpoint is the network you deployed to. ` +
|
|
469
|
+
`If you JUST deployed, give the tx a block or two to mine.\n`);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
console.error(`abx contracturi: ${address} didn't return a contractURI — it may not be an ABX/ERC-7572 contract, or ` +
|
|
473
|
+
`— on a large on-chain contractURI — an unauthenticated RPC read hit its gas cap (try a wallet-connected / high-gas RPC).\n`);
|
|
474
|
+
}
|
|
475
|
+
process.exitCode = 1;
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
console.log(`\n ${bold('contractURI()')} ${dim(`— read directly from ${address} on ${CHAIN}`)}`);
|
|
479
|
+
if (!uri) {
|
|
480
|
+
console.error(`\n ${bold('empty')} — this contract has no contractURI set: no collection-level metadata to resolve. ` +
|
|
481
|
+
`Set one with ${bold(`abx set-contract-uri ${address} --uri <base>`)}, or point it at the canonical renderer for the on-chain lane.\n`);
|
|
482
|
+
process.exitCode = 1;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const onChain = decodeOnChainJson(uri);
|
|
486
|
+
if (onChain) {
|
|
487
|
+
info('resolution: ON-CHAIN (data: URI from the renderer — no server in the path)');
|
|
488
|
+
console.log(onChain.split('\n').map((l) => ' ' + l).join('\n') + '\n');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
console.log(` ${dim('resolves to')} ${uri}`);
|
|
492
|
+
if (!/^https?:\/\//i.test(uri)) {
|
|
493
|
+
// ipfs:// / ar:// — a locator, not something we can fetch without choosing a gateway. Print it
|
|
494
|
+
// rather than silently picking one; the creator's gateway choice is theirs.
|
|
495
|
+
info(`not an http(s) URL — a ${uri.split(':')[0]}: locator needs a gateway to fetch. Nothing more to read from here.`);
|
|
496
|
+
console.log('');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
let body;
|
|
500
|
+
try {
|
|
501
|
+
const res = await fetch(uri, { headers: { accept: 'application/json' } });
|
|
502
|
+
body = await res.text();
|
|
503
|
+
if (!res.ok) {
|
|
504
|
+
// The URL came FROM THE CHAIN, so a bad status here is genuinely about the service (or the
|
|
505
|
+
// contract pointing somewhere stale) — never a mistyped path. Say which, so nobody re-guesses.
|
|
506
|
+
console.error(`\n ${bold(`HTTP ${res.status}`)} from the contract's own contractURI — the URL is correct by construction (it came from ` +
|
|
507
|
+
`${address} on-chain), so this is the SERVICE, not the path. Likely: the project isn't registered on that resolver ` +
|
|
508
|
+
`(${bold('abx add ' + address + ' --remote')}), the node serves a different chain, or it's down. ` +
|
|
509
|
+
`Response: ${body.slice(0, 200)}\n`);
|
|
510
|
+
process.exitCode = 1;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
console.error(`\n couldn't reach ${uri} — ${e.message}. The URL is what the contract commits to, so check that the ` +
|
|
516
|
+
`host is up and publicly reachable (a localhost base URL resolves for no one but this machine).\n`);
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
info('resolution: OFF-CHAIN (fetched from the URL the contract commits to)');
|
|
521
|
+
try {
|
|
522
|
+
console.log(JSON.stringify(JSON.parse(body), null, 2)
|
|
523
|
+
.split('\n')
|
|
524
|
+
.map((l) => ' ' + l)
|
|
525
|
+
.join('\n') + '\n');
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
console.log(` ${dim('(not JSON)')} ${body.slice(0, 400)}\n`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// ── status ────────────────────────────────────────────────────────────────--
|
|
532
|
+
// ── tokens: every token's seed + params, read straight from chain ────────────────────────────
|
|
533
|
+
// For a generative collection the seed list IS the collection — it's the first thing you want after
|
|
534
|
+
// a drop ("what did the seeds actually deal?") and, before this existed, the one thing no command
|
|
535
|
+
// could answer. An agent's workaround was: start `abx serve`, GET the project API, base64-decode
|
|
536
|
+
// each tokenURI, base64-decode the animation_url inside it, then regex `0x[0-9a-f]{64}` out of the
|
|
537
|
+
// resulting HTML — 32 times. Every input to that was already a plain contract read.
|
|
538
|
+
export const TOKENS_FLAGS = new Set(['json', 'limit', 'from']);
|
|
539
|
+
/** An owner address abbreviated for a human table. Abbreviating for eyes is fine; abbreviating for a
|
|
540
|
+
* PROGRAM is not — which is why every value here is also reachable verbatim via `--json`. */
|
|
541
|
+
export function shortAddr(a) {
|
|
542
|
+
return `${a.slice(0, 6)}…${a.slice(-4)}`;
|
|
543
|
+
}
|
|
544
|
+
export async function cmdTokens(address, flags) {
|
|
545
|
+
if (!address || address.startsWith('--')) {
|
|
546
|
+
console.error('usage: abx tokens <address> [--json] [--from <id>] [--limit <n>] — every token\'s owner, seed, and params, from chain\n');
|
|
547
|
+
process.exitCode = 1;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
551
|
+
console.error(`abx tokens: '${address}' isn't a 0x contract address.\n`);
|
|
552
|
+
process.exitCode = 1;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
warnStrayFlags(flags, TOKENS_FLAGS, 'tokens');
|
|
556
|
+
const jsonMode = flags.json !== undefined;
|
|
557
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
558
|
+
const code = await publicClient.getCode({ address }).catch(() => undefined);
|
|
559
|
+
if (!code || code === '0x') {
|
|
560
|
+
// Name the endpoint we actually asked — "no contract here" and "wrong node" are otherwise
|
|
561
|
+
// indistinguishable (same reasoning as `tokenuri` / `contracturi`).
|
|
562
|
+
console.error(`abx tokens: no contract at ${address} on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}) — ` +
|
|
563
|
+
`double-check the address, and that this endpoint is the network you deployed to. ` +
|
|
564
|
+
`If you JUST deployed, give the tx a block or two to mine.\n`);
|
|
565
|
+
process.exitCode = 1;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const listing = await listTokens(publicClient, address, {
|
|
569
|
+
from: flags.from === undefined ? undefined : Number(flags.from),
|
|
570
|
+
limit: flags.limit === undefined ? undefined : Number(flags.limit),
|
|
571
|
+
// Progress on stderr only — stdout must stay pure JSON in --json mode (the ANSI-in-a-locator
|
|
572
|
+
// lesson: a program's channel is not a place to narrate).
|
|
573
|
+
onProgress: (done, total) => total > 50 && done % 25 === 0 ? process.stderr.write(` reading ${done}/${total}…\r`) : undefined,
|
|
574
|
+
});
|
|
575
|
+
if (listing.tokens.length > 50)
|
|
576
|
+
process.stderr.write(' \r');
|
|
577
|
+
if (jsonMode) {
|
|
578
|
+
process.stdout.write(JSON.stringify(listing, null, 2) + '\n');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const supply = listing.totalSupply ?? listing.tokens.length;
|
|
582
|
+
const cap = listing.maxInvocations !== null && listing.maxInvocations > 0 ? ` of ${listing.maxInvocations} max` : '';
|
|
583
|
+
console.log(`\n ${bold(`tokens ${address}`)} ${dim(`— ${supply} minted${cap} on ${CHAIN}`)}`);
|
|
584
|
+
info('read straight from the contract — no indexer, no resolver, no server in the loop');
|
|
585
|
+
if (!listing.hasParams) {
|
|
586
|
+
info('this token type has no params extension — so no seeds or params to list (that is correct, not a failed read). Owners below.');
|
|
587
|
+
}
|
|
588
|
+
else if (!listing.hasParamEnumeration) {
|
|
589
|
+
warn('this contract predates on-chain param enumeration — seeds still read, but params cannot be listed. Redeploy on a current implementation to enumerate them.');
|
|
590
|
+
}
|
|
591
|
+
const contractKeys = Object.keys(listing.contractParams);
|
|
592
|
+
if (contractKeys.length) {
|
|
593
|
+
info(`contract-scope params ${dim('(every token inherits these; a token-scope value of the same key wins)')}`);
|
|
594
|
+
for (const k of contractKeys.sort())
|
|
595
|
+
console.log(` ${g(k)}=${listing.contractParams[k]}`);
|
|
596
|
+
}
|
|
597
|
+
if (!listing.tokens.length) {
|
|
598
|
+
console.log(`\n ${dim('no tokens in range — nothing minted yet, or --from is past the mint frontier.')}\n`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const anySeed = listing.tokens.some((t) => t.seed !== null);
|
|
602
|
+
console.log(`\n ${dim('id'.padStart(4))} ${anySeed ? dim('seed'.padEnd(66)) + ' ' : ''}${dim('owner')}`);
|
|
603
|
+
for (const t of listing.tokens) {
|
|
604
|
+
// A seed is the whole reason this command exists, so it prints in FULL. The owner truncates —
|
|
605
|
+
// an address stays recognizable abbreviated, and `--json` carries both verbatim either way.
|
|
606
|
+
const seedCell = anySeed ? `${t.seed ?? dim('—'.padEnd(66))} ` : '';
|
|
607
|
+
const owner = t.owner ? shortAddr(t.owner) : dim('— unminted or burned');
|
|
608
|
+
console.log(` ${bold(('#' + t.tokenId).padStart(4))} ${seedCell}${owner}`);
|
|
609
|
+
const keys = Object.keys(t.params).sort();
|
|
610
|
+
if (keys.length)
|
|
611
|
+
console.log(` ${keys.map((k) => `${g(k)}=${t.params[k]}`).join(dim(' · '))}`);
|
|
612
|
+
}
|
|
613
|
+
// The token-scope param SPREAD — what collectors actually chose, which is the question a param
|
|
614
|
+
// dump is usually a step toward. Deliberately NOT called a trait spread: traits come from running
|
|
615
|
+
// the script against the seed (that's `abx render`), not from params.
|
|
616
|
+
const spread = new Map();
|
|
617
|
+
for (const t of listing.tokens) {
|
|
618
|
+
for (const [k, v] of Object.entries(t.params)) {
|
|
619
|
+
const counts = spread.get(k) ?? new Map();
|
|
620
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
621
|
+
spread.set(k, counts);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (spread.size && listing.tokens.length > 1) {
|
|
625
|
+
console.log(`\n ${bold('token-scope param spread')} ${dim('(what has actually been set — not the trait spread, which comes from running the script)')}`);
|
|
626
|
+
for (const key of [...spread.keys()].sort()) {
|
|
627
|
+
const counts = [...spread.get(key).entries()].sort((a, b) => b[1] - a[1]);
|
|
628
|
+
const shown = counts.slice(0, 8).map(([v, n]) => `${v} ${dim('×' + n)}`).join(dim(' · '));
|
|
629
|
+
const rest = counts.length > 8 ? dim(` …+${counts.length - 8} more values`) : '';
|
|
630
|
+
console.log(` ${g(key)}: ${shown}${rest}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
console.log(`\n ${dim('every value above verbatim + machine-readable:')} ${bold(`abx tokens ${address} --json`)}\n`);
|
|
634
|
+
}
|
|
635
|
+
//# sourceMappingURL=reads.js.map
|