@artblocks/abx-cli 0.1.0-alpha.7 → 0.1.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +301 -125
- package/dist/main.js.map +1 -1
- package/dist/migrate.js +1 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +17 -25
- package/dist/ownerops.js.map +1 -1
- package/dist/remote.d.ts +63 -53
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +128 -43
- package/dist/remote.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +17 -12
- package/skill/reference/hosting.md +19 -8
- package/skill/reference/operating.md +5 -3
- package/skill/reference/setup.md +7 -1
- package/skill/reference/troubleshooting.md +1 -1
package/dist/main.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* abx preview run a code project on localhost while it's still being made (no chain)
|
|
18
18
|
* abx deploy-code deploy a code project (SeriesCode): --script <file> (on-chain template)
|
|
19
19
|
* or --code-dir <dir> (build directory → ipfs/arweave `code` field)
|
|
20
|
-
* abx add <address> register + index a project (--remote
|
|
21
|
-
* abx
|
|
20
|
+
* abx add <address> register + index a project (--remote <name|url>: on a remote resolver, not this machine)
|
|
21
|
+
* abx remote [<name|url>] inspect a remote service (descriptor · chains · managed rendering · your projects there)
|
|
22
|
+
* abx index [<address>] re-index a project from chain (replay; --remote to nudge a remote resolver)
|
|
22
23
|
* abx verify <address> re-hash served bytes vs the on-chain commitment (no server)
|
|
23
24
|
* abx configure-param <addr> <id> <key> <value> set a governed PostParam (typed encode; any lane)
|
|
24
25
|
* abx set-param-hooks <addr> wire/clear a SeriesCode's configure/augment/transfer param hooks
|
|
@@ -70,22 +71,20 @@ const SCHEMA_CATALOG = 'Types: Bool·Select·Uint256Range·Int256Range·DecimalR
|
|
|
70
71
|
'Select needs options — key:Select[A|B|C]:Auth; a Range takes bounds — key:Uint256Range[0..100]:Auth. ' +
|
|
71
72
|
'A palette collectors set = palette:HexColor:TokenOwner';
|
|
72
73
|
import { uploadAndLocate } from './upload.js';
|
|
73
|
-
import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, DEFAULT_CHAIN_KEY, normalizeAttributes, parseTraitPairs, METADATA_FIELD as F, METADATA_REPRESENTATION as R, } from '@artblocks/abx-sdk';
|
|
74
|
+
import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, resolveRpcUrl, redactRpcUrl, explorerUrl, DEFAULT_CHAIN_KEY, normalizeAttributes, parseTraitPairs, METADATA_FIELD as F, METADATA_REPRESENTATION as R, AbxServiceError, } from '@artblocks/abx-sdk';
|
|
74
75
|
import { SelfHostIndexer, SqliteStore } from '@artblocks/abx-indexer';
|
|
75
76
|
import { artContentHash, currentRenderArtifact, generateArt, resolveBaseUrl, startChainWatcher, startTokenApiServer, verifyProject, watchIntervalMs, DEFAULT_PORT, } from '@artblocks/abx-token-api';
|
|
76
77
|
import { ARWEAVE_FREE_UPLOAD_LIMIT, arweaveAddress, arweaveFunding, contentTypeFromPath, hashContent, resolveBackend, turboBalanceForAddress, turboUploadCostUsd, turboUploadWinc } from '@artblocks/abx-storage';
|
|
77
78
|
import { cmdTransfer, cmdMint, cmdSetMinter, cmdSetMaxInvocations, cmdConfigureParam, cmdSetParamHooks, cmdSetDependency, cmdRemoveLastDependency, cmdSetDependencyRegistry, cmdLockDependencies, cmdSetPrimaryPayee, cmdPause, cmdUnpause, cmdRefresh, cmdSetTokenUri, cmdSetContractUri, cmdSetRoyalty, cmdSetField, cmdAttach, cmdLockField, cmdSetRenderer, cmdLockUri, cmdSetAdmin, cmdMinterConfigure, cmdMinterShow, cmdMinterBuy, computeContentPlan, envStagingSender, laneFromFlags, ONCHAIN_PROJECT_SOFT_LIMIT, parseCompress, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, authorshipContractFields, AUTHORSHIP_DEPLOY_FIELDS, } from './ownerops.js';
|
|
78
79
|
import { openWalletSession, signTx } from './signer.js';
|
|
79
|
-
import {
|
|
80
|
+
import { describeRemoteError, listConfiguredRemotes, misnamedRemoteVars, requireRemoteToken, resolveRemote, serviceClient } from './remote.js';
|
|
80
81
|
import { buildMigrationPlan, repinNodeCustody, verifyParity } from './migrate.js';
|
|
81
82
|
import { activeBackendId, arweaveKeyFilePath, backendResolution, ensureArweaveJwk, factoryAddress, seriesFactoryAddress, fixedPriceMinterAddress, loadArweaveJwk, loopbackBaseUrl, faucetHint, rendererAddress, storageOptions, storageSignerChoice, } from './config.js';
|
|
82
83
|
const CHAIN = process.env.ABX_CHAIN ?? DEFAULT_CHAIN_KEY;
|
|
83
|
-
// Block explorer base
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
const EXPLORER = EXPLORERS[resolveChain(CHAIN).id] ?? 'https://sepolia.etherscan.io';
|
|
84
|
+
// Block explorer base for the tx/address links the CLI prints. Derived from viem's chain metadata via
|
|
85
|
+
// the SDK (see explorerUrl) rather than a local table — this used to be a hand-maintained map, which is
|
|
86
|
+
// the same shape of bug that had the token-api dashboard sending every Base Sepolia link to Etherscan.
|
|
87
|
+
const EXPLORER = explorerUrl(resolveChain(CHAIN).id);
|
|
89
88
|
// Product dimensions — what you can launch. One concrete contract exists today
|
|
90
89
|
// (the 1/1 image); this table is the seam future implementations slot into, so
|
|
91
90
|
// `abx deploy --type <dimension>` is stable while the contracts grow under it.
|
|
@@ -209,6 +208,7 @@ async function main() {
|
|
|
209
208
|
case 'set-admin': return cmdSetAdmin(rest[0], flags);
|
|
210
209
|
case 'forget': return cmdForget(rest[0], flags);
|
|
211
210
|
case 'migrate': return cmdMigrate(rest[0], flags);
|
|
211
|
+
case 'remote': return cmdRemote(rest[0], flags);
|
|
212
212
|
case 'storage': return cmdStorage(rest);
|
|
213
213
|
case 'status': return cmdStatus();
|
|
214
214
|
case 'state': return cmdState(rest[0], flags);
|
|
@@ -225,6 +225,65 @@ async function main() {
|
|
|
225
225
|
process.exit(1);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* A `--dry-run` computes the deterministic deploy address, which is a pure function of
|
|
230
|
+
* (factory, salt, deployer) — so it needs a deployer even though it signs nothing. Resolve it the
|
|
231
|
+
* same way the preview will (`--for`, else an env key) and fail EARLY with the fix if neither
|
|
232
|
+
* exists, rather than after the preview has printed several steps of work.
|
|
233
|
+
*/
|
|
234
|
+
/**
|
|
235
|
+
* A dry run only checks whether a factory address is CONFIGURED, not whether it has code on this
|
|
236
|
+
* chain — and the manifest always has an address, so a chain where the trust anchor isn't deployed
|
|
237
|
+
* (a private/local chain, or a wrong-network RPC) sailed past this and died inside
|
|
238
|
+
* `predictDeterministicAddress` with a raw `returned no data ("0x")` and a list of ABI hypotheses.
|
|
239
|
+
* The real deploy and `abx predict` both explain that case; a preview of the same deploy must too.
|
|
240
|
+
* Returns false when the caller should stop (message already printed).
|
|
241
|
+
*/
|
|
242
|
+
async function previewFactoryLive(client, factory, label) {
|
|
243
|
+
const code = await client.getCode({ address: factory }).catch(() => undefined);
|
|
244
|
+
if (code && code !== '0x')
|
|
245
|
+
return true;
|
|
246
|
+
warn(`the configured ${label} ${factory} has no code on '${CHAIN}' (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}).`);
|
|
247
|
+
info('so this preview can\'t compute the deterministic address. Either point at a chain where the trust anchor is deployed');
|
|
248
|
+
info(`(${bold('ABX_CHAIN=' + DEFAULT_CHAIN_KEY)} is the default and has one), or deploy your own on this chain with ${bold('--bootstrap-factory')}`);
|
|
249
|
+
info(dim('(a private anchor — platforms won\'t recognize its clones, so it\'s for private/sandbox chains).'));
|
|
250
|
+
console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Placeholder-identity guard, shared by ALL THREE deploy commands. `name`/`symbol` are written
|
|
255
|
+
* on-chain as the public collection identity and are effectively permanent, so a real deploy must
|
|
256
|
+
* never bake a tool default silently: warn in a preview, HARD-STOP a real send unless `--yes`.
|
|
257
|
+
*
|
|
258
|
+
* It was duplicated per command, and `deploy-series` simply never got a copy — its default
|
|
259
|
+
* "ABX Series"/"ABXS" went on-chain with at most a warning, while the skill promises the CLI
|
|
260
|
+
* refuses demo defaults. (deploy-code's copy even said "mirror deploy/deploy-series", which made
|
|
261
|
+
* the gap look closed.) One predicate now, like `loopbackBaseUrl()`, so a fourth command can't drift.
|
|
262
|
+
*/
|
|
263
|
+
function assertRealIdentity(flags, o) {
|
|
264
|
+
if (flags.name && flags.symbol)
|
|
265
|
+
return;
|
|
266
|
+
if (!flags.name)
|
|
267
|
+
warn(`no --name → default "${o.name}" would be the on-chain collection name`);
|
|
268
|
+
if (!flags.symbol)
|
|
269
|
+
warn(`no --symbol → default "${o.symbol}" would be the on-chain symbol`);
|
|
270
|
+
if (o.dryRun || flags.yes)
|
|
271
|
+
return; // a preview still runs; --yes is the explicit opt-in
|
|
272
|
+
throw new Error('refusing to write tool placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM ' +
|
|
273
|
+
'(or --yes to accept the defaults). On-chain identity is effectively permanent.');
|
|
274
|
+
}
|
|
275
|
+
function assertPreviewDeployer(flags) {
|
|
276
|
+
if (flags.for)
|
|
277
|
+
return;
|
|
278
|
+
try {
|
|
279
|
+
makeWalletClient({ chainKey: CHAIN });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
throw new Error('dry run needs a deployer address to compute the deterministic deploy address — pass --for 0x.. ' +
|
|
283
|
+
'(a preview signs nothing, so no key is needed). For the REAL deploy with no key in .env, use the ' +
|
|
284
|
+
'wallet lane: --sign --for 0x.. (you approve in your own wallet).');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
228
287
|
// When the user/agent opts in (--yes), lift the getLogs chunk cap so a large
|
|
229
288
|
// reconstruction proceeds despite a range-limited RPC (otherwise it stops early with
|
|
230
289
|
// guidance — see GetLogsScanTooLargeError + the skill's "Choosing an RPC" decision).
|
|
@@ -449,7 +508,7 @@ function refuseMissingFactory(kind, envVar, reason) {
|
|
|
449
508
|
` To deploy your OWN trust anchor instead (private chains, sandboxes — platforms won't recognize its clones), ` +
|
|
450
509
|
`re-run with --bootstrap-factory.`);
|
|
451
510
|
}
|
|
452
|
-
async function ensureFactory(override, allowBootstrap = false) {
|
|
511
|
+
async function ensureFactory(override, allowBootstrap = false, quiet = false) {
|
|
453
512
|
// Resolve the canonical factory from the shipped manifest (flag → env → manifest); on a chain
|
|
454
513
|
// with no entry, deploy a fresh trust anchor and tell the operator how to reuse it.
|
|
455
514
|
const known = factoryAddress(override);
|
|
@@ -459,7 +518,10 @@ async function ensureFactory(override, allowBootstrap = false) {
|
|
|
459
518
|
const code = await publicClient.getCode({ address: known });
|
|
460
519
|
if (code && code !== '0x') {
|
|
461
520
|
if (await isCurrentFactory(publicClient, known)) {
|
|
462
|
-
|
|
521
|
+
// `quiet` suppresses only this happy-path line (the demo resolves the factory without making a
|
|
522
|
+
// teaching moment of it). Bootstrap/mismatch messages below always print — those matter.
|
|
523
|
+
if (!quiet)
|
|
524
|
+
info(`using canonical factory ${EXPLORER}/address/${known}`);
|
|
463
525
|
return known;
|
|
464
526
|
}
|
|
465
527
|
if (!allowBootstrap)
|
|
@@ -1009,23 +1071,20 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1009
1071
|
}
|
|
1010
1072
|
}
|
|
1011
1073
|
const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
|
|
1074
|
+
// A keyless preview needs `--for` (the address is a pure function of factory+salt+deployer). Check
|
|
1075
|
+
// it HERE, before the trust-anchor/content/plan steps print — hitting this after a wall of output
|
|
1076
|
+
// reads as "it half-worked", and a first-timer previewing with no key in .env always hits it.
|
|
1077
|
+
if (dryRun)
|
|
1078
|
+
assertPreviewDeployer(flags);
|
|
1012
1079
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1013
1080
|
// Verify the RPC really is CHAIN before any send (factory/renderer/staging/deploy). A dry run
|
|
1014
1081
|
// sends nothing, but it DOES read the chain (predict address, resolve the factory/renderer), so
|
|
1015
1082
|
// a wrong-network RPC must still be caught with the clear mismatch message rather than failing
|
|
1016
1083
|
// opaquely inside predict; `allowUnreachable` keeps a genuinely offline dry-run previewable.
|
|
1017
1084
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
1018
|
-
//
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (!flags.name)
|
|
1022
|
-
warn(`no --name → default "${name}" would be the on-chain collection name`);
|
|
1023
|
-
if (!flags.symbol)
|
|
1024
|
-
warn(`no --symbol → default "${symbol}" would be the on-chain symbol`);
|
|
1025
|
-
if (!dryRun && !flags.yes) {
|
|
1026
|
-
throw new Error('refusing to write demo placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM (or --yes to accept the defaults).');
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1085
|
+
// `demo` is exempt: its whole job is a zero-argument first token.
|
|
1086
|
+
if (!serveAfter)
|
|
1087
|
+
assertRealIdentity(flags, { name, symbol, dryRun });
|
|
1029
1088
|
// Funding preflight (real deploys): warn now if the signer is unfunded, not at the tx.
|
|
1030
1089
|
if (!dryRun) {
|
|
1031
1090
|
let signer = flags.for;
|
|
@@ -1038,7 +1097,13 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1038
1097
|
if (signer)
|
|
1039
1098
|
await warnUnfunded(publicClient, signer);
|
|
1040
1099
|
}
|
|
1041
|
-
step
|
|
1100
|
+
// `deploy` surfaces the trust anchor as a step; the demo does NOT. It briefly opened on "only this
|
|
1101
|
+
// factory can make a token that IS an ABX token", which is simply false — anything that follows the
|
|
1102
|
+
// protocol's event spine is an ABX token, and the factory is one way to get there, not the
|
|
1103
|
+
// definition of the thing. Rather than restate it more carefully, the demo skips it: a first-timer
|
|
1104
|
+
// does not need a provenance lecture before they have made anything.
|
|
1105
|
+
if (!serveAfter)
|
|
1106
|
+
step('Trust anchor');
|
|
1042
1107
|
let factory;
|
|
1043
1108
|
if (dryRun) {
|
|
1044
1109
|
const existing = factoryAddress(flags.factory);
|
|
@@ -1048,19 +1113,12 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1048
1113
|
return;
|
|
1049
1114
|
}
|
|
1050
1115
|
factory = existing;
|
|
1116
|
+
if (!(await previewFactoryLive(publicClient, factory, 'factory')))
|
|
1117
|
+
return;
|
|
1051
1118
|
info(`would reuse canonical factory ${factory}`);
|
|
1052
1119
|
}
|
|
1053
1120
|
else {
|
|
1054
|
-
factory = await ensureFactory(flags.factory, !!flags['bootstrap-factory']);
|
|
1055
|
-
}
|
|
1056
|
-
// The demo explains what it just did; `deploy` stays terse. The authenticity point is the least
|
|
1057
|
-
// obvious thing about ABX and the easiest to get wrong, so it's worth spelling out — but in plain
|
|
1058
|
-
// language, with the actual term parked in a dim aside for whoever wants to look it up.
|
|
1059
|
-
if (serveAfter) {
|
|
1060
|
-
console.log(` ${g('✦')} anyone can write a contract that ${bold('claims')} to be an ABX token`);
|
|
1061
|
-
console.log(` ${g('✦')} only this one factory can make a token that ${bold('is')} one`);
|
|
1062
|
-
info(dim('so "is this real?" is a yes/no question anyone can ask the chain — not a matter of trusting us'));
|
|
1063
|
-
info(dim(`the factory answers it on-chain: isAbxClone(yourToken) · ${EXPLORER}/address/${factory}`));
|
|
1121
|
+
factory = await ensureFactory(flags.factory, !!flags['bootstrap-factory'], serveAfter);
|
|
1064
1122
|
}
|
|
1065
1123
|
// --onchain-uri: resolve tokenURI/contractURI fully on-chain via the canonical renderer
|
|
1066
1124
|
// (the JSON is assembled from on-chain fields → no resolver needed, ever). The off-chain
|
|
@@ -1437,7 +1495,10 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1437
1495
|
if (serveAfter) {
|
|
1438
1496
|
if (state.eventCount > 0)
|
|
1439
1497
|
ok(`read ${bold(String(state.eventCount))} events straight off ${CHAIN} in ${elapsedMs}ms — no API key, no company's server`);
|
|
1440
|
-
|
|
1498
|
+
// "verified real" overstated it in the same way the removed trust-anchor step did — factory
|
|
1499
|
+
// provenance is a fact about how this contract was made, not a verdict on whether a token counts
|
|
1500
|
+
// as ABX. Report the fact.
|
|
1501
|
+
info(`it says: "${state.name}" · owned by ${state.owner} · ${state.isCanonical ? g('made by the canonical factory') : dim('not factory-made')}`);
|
|
1441
1502
|
info(dim(`optional features switched on: ${state.extensions.map((e) => e.name.replace(/^abx\.extension\./, '')).join(' · ') || 'none'}`));
|
|
1442
1503
|
walkthroughSpine(state);
|
|
1443
1504
|
}
|
|
@@ -1560,21 +1621,15 @@ async function cmdDeploySeries(flags) {
|
|
|
1560
1621
|
throw new Error(`--count ${count} exceeds the ${files.length} media file(s) in ${dirPath}`);
|
|
1561
1622
|
const slots = files.slice(0, count);
|
|
1562
1623
|
const dryRun = !!flags['dry-run'];
|
|
1624
|
+
if (dryRun)
|
|
1625
|
+
assertPreviewDeployer(flags); // fail fast, before the preview does any work (see cmdDeploy)
|
|
1626
|
+
assertRealIdentity(flags, { name, symbol, dryRun });
|
|
1563
1627
|
const lane = laneFromFlags(flags);
|
|
1564
1628
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1565
1629
|
// Catch a wrong-network RPC with the clear mismatch message even on dry-run (which still reads
|
|
1566
1630
|
// the chain to predict the address); tolerate an unreachable RPC so an offline preview still works.
|
|
1567
1631
|
await assertChainId(CHAIN, { allowUnreachable: dryRun });
|
|
1568
|
-
//
|
|
1569
|
-
if (!flags.name || !flags.symbol) {
|
|
1570
|
-
if (!flags.name)
|
|
1571
|
-
warn(`no --name → default "${name}" would be the on-chain collection name`);
|
|
1572
|
-
if (!flags.symbol)
|
|
1573
|
-
warn(`no --symbol → default "${symbol}" would be the on-chain symbol`);
|
|
1574
|
-
if (!dryRun && !flags.yes) {
|
|
1575
|
-
throw new Error('refusing to write demo placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM (or --yes).');
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1632
|
+
// (identity guard already ran above, via the shared assertRealIdentity — before any RPC)
|
|
1578
1633
|
// Mint timing: mint-all → the whole series; mint-count N → the first N; else deferred.
|
|
1579
1634
|
const mintCount = flags['mint-all'] !== undefined ? count : flags['mint-count'] ? Number(flags['mint-count']) : 0;
|
|
1580
1635
|
if (mintCount > count)
|
|
@@ -1618,6 +1673,8 @@ async function cmdDeploySeries(flags) {
|
|
|
1618
1673
|
return;
|
|
1619
1674
|
}
|
|
1620
1675
|
factory = existing;
|
|
1676
|
+
if (!(await previewFactoryLive(publicClient, factory, 'Series factory')))
|
|
1677
|
+
return;
|
|
1621
1678
|
info(`would reuse canonical Series factory ${factory}`);
|
|
1622
1679
|
}
|
|
1623
1680
|
else {
|
|
@@ -2079,18 +2136,11 @@ async function cmdPredict(flags) {
|
|
|
2079
2136
|
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)'));
|
|
2080
2137
|
console.log('');
|
|
2081
2138
|
}
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
//
|
|
2085
|
-
function
|
|
2086
|
-
|
|
2087
|
-
if (v === undefined)
|
|
2088
|
-
return null;
|
|
2089
|
-
const base = v !== 'true' ? v : process.env.ABX_PUBLIC_BASE_URL;
|
|
2090
|
-
if (!base) {
|
|
2091
|
-
throw new Error('`--remote` needs a resolver URL: pass `--remote https://host` or set ABX_PUBLIC_BASE_URL in .env');
|
|
2092
|
-
}
|
|
2093
|
-
return base;
|
|
2139
|
+
// The `--remote <name|url>` target (remote.ts owns the convention): a named remote's
|
|
2140
|
+
// `ABX_REMOTE_<NAME>_URL/_TOKEN`, an ad-hoc URL, or bare `--remote` = the self-host default.
|
|
2141
|
+
// Returns null for a local op (the default).
|
|
2142
|
+
function remoteFlag(flags) {
|
|
2143
|
+
return resolveRemote(flags.remote, flags['remote-token']);
|
|
2094
2144
|
}
|
|
2095
2145
|
/**
|
|
2096
2146
|
* Ensure a resolver admin token exists locally, generating + persisting one to `.env`
|
|
@@ -2123,22 +2173,13 @@ function ensureEffectsToken() {
|
|
|
2123
2173
|
process.env.ABX_EFFECTS_TOKEN = token;
|
|
2124
2174
|
return { token, generated: true };
|
|
2125
2175
|
}
|
|
2126
|
-
/** The shared secret that authorizes remote indexing control (never on-chain signing). */
|
|
2127
|
-
function requireAdminToken() {
|
|
2128
|
-
const t = process.env.ABX_RESOLVER_ADMIN_TOKEN;
|
|
2129
|
-
if (!t) {
|
|
2130
|
-
throw new Error('remote ops need ABX_RESOLVER_ADMIN_TOKEN in your .env — it must match the token set on the resolver ' +
|
|
2131
|
-
'(`abx deploy-resolver` generates one and wires both sides).');
|
|
2132
|
-
}
|
|
2133
|
-
return t;
|
|
2134
|
-
}
|
|
2135
2176
|
// ── add ──────────────────────────────────────────────────────────────────────
|
|
2136
2177
|
// Register + index a project this node didn't deploy. LOCAL by default (this
|
|
2137
2178
|
// machine's store); `--remote [url]` instead tells a HOSTED resolver to index it —
|
|
2138
2179
|
// the bridge a local deploy can't make on its own (separate projection stores).
|
|
2139
2180
|
async function cmdAdd(address, flags) {
|
|
2140
2181
|
if (!address || address.startsWith('--')) {
|
|
2141
|
-
console.error('usage: abx add <address> [--from-block N] [--factory 0x..] [--label "..."] [--remote [url]]\n');
|
|
2182
|
+
console.error('usage: abx add <address> [--from-block N] [--factory 0x..] [--label "..."] [--remote [name|url]]\n');
|
|
2142
2183
|
process.exit(1);
|
|
2143
2184
|
}
|
|
2144
2185
|
// `--attributes` is lane-aware here exactly as at deploy: a PER-TOKEN payload edits a Series'
|
|
@@ -2152,10 +2193,10 @@ async function cmdAdd(address, flags) {
|
|
|
2152
2193
|
if (flags.traits)
|
|
2153
2194
|
flagTraits.push(...parseTraitPairs(flags.traits));
|
|
2154
2195
|
const editedTokenAttributes = perTokenEdit ? parseSeriesTraitsById(attrRaw) : undefined;
|
|
2155
|
-
const remote =
|
|
2196
|
+
const remote = remoteFlag(flags);
|
|
2156
2197
|
if (remote) {
|
|
2157
|
-
|
|
2158
|
-
// Bridge what a
|
|
2198
|
+
requireRemoteToken(remote);
|
|
2199
|
+
// Bridge what a remote resolver can't derive itself: the off-chain traits and the durable
|
|
2159
2200
|
// content locators (ipfs://…). Prefer flags; otherwise forward what the LOCAL deploy stored
|
|
2160
2201
|
// (the local registration), and compute locators from this machine's content index if needed.
|
|
2161
2202
|
const localReg = new SelfHostIndexer().store.getRegistration(address);
|
|
@@ -2164,7 +2205,7 @@ async function cmdAdd(address, flags) {
|
|
|
2164
2205
|
: localReg?.attributes
|
|
2165
2206
|
? normalizeAttributes(JSON.parse(localReg.attributes))
|
|
2166
2207
|
: undefined;
|
|
2167
|
-
// Bridge a Series' per-token off-chain traits to the
|
|
2208
|
+
// Bridge a Series' per-token off-chain traits to the remote resolver (the resolver has no other
|
|
2168
2209
|
// way to derive them — they're operator metadata, not chain state). A fresh per-token `--attributes`
|
|
2169
2210
|
// EDITS them; otherwise forward what the LOCAL deploy stored. Best-effort parse.
|
|
2170
2211
|
let tokenAttributes;
|
|
@@ -2190,6 +2231,7 @@ async function cmdAdd(address, flags) {
|
|
|
2190
2231
|
// (Its ABSENCE here was the bug: a hosted resolver defaulted to genesis and scanned the whole
|
|
2191
2232
|
// chain.) Re-sending the same floor stays incremental server-side, so a nudge ≠ a re-scan.
|
|
2192
2233
|
const body = {
|
|
2234
|
+
chainId: resolveChain(CHAIN).id,
|
|
2193
2235
|
address,
|
|
2194
2236
|
fromBlock: await resolveScanFloor(address, localReg?.fromBlock, flags),
|
|
2195
2237
|
factory: await detectCanonicalFactory(address, flags.factory, localReg?.factory),
|
|
@@ -2201,12 +2243,18 @@ async function cmdAdd(address, flags) {
|
|
|
2201
2243
|
contentLocators: Object.keys(contentLocators).length ? contentLocators : undefined,
|
|
2202
2244
|
full: flags.full ? true : undefined,
|
|
2203
2245
|
};
|
|
2204
|
-
info(`${bold('REMOTE')} → ${remote} ${dim('(registering with the
|
|
2246
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(registering with the remote resolver — NOT this machine)')}`);
|
|
2205
2247
|
if (body.contentLocators)
|
|
2206
2248
|
info(`bridging image locator → ${Object.values(body.contentLocators)[0]} ${dim('(so the resolver points at IPFS, not its own localhost)')}`);
|
|
2207
|
-
|
|
2249
|
+
let r;
|
|
2250
|
+
try {
|
|
2251
|
+
r = await serviceClient(remote).registerProject(body);
|
|
2252
|
+
}
|
|
2253
|
+
catch (err) {
|
|
2254
|
+
throw describeRemoteError(err, remote, 'remote add');
|
|
2255
|
+
}
|
|
2208
2256
|
ok(`remote resolver indexed ${r.project.name ?? address}: ${r.project.eventCount} events ${dim(`(${r.mode}, ${r.elapsedMs}ms)`)}`);
|
|
2209
|
-
info(`it now serves ${remote.
|
|
2257
|
+
info(`it now serves ${remote.url}/t/${body.chainId}/${address.toLowerCase()}/0`);
|
|
2210
2258
|
return;
|
|
2211
2259
|
}
|
|
2212
2260
|
allowLargeScan(flags);
|
|
@@ -2789,17 +2837,7 @@ async function cmdDeployCode(flags) {
|
|
|
2789
2837
|
const dryRun = !!flags['dry-run'];
|
|
2790
2838
|
const name = flags.name ?? 'ABX Code';
|
|
2791
2839
|
const symbol = flags.symbol ?? 'ABXC';
|
|
2792
|
-
|
|
2793
|
-
// on-chain identity. dry-run only warns so a preview still runs without --name/--symbol.
|
|
2794
|
-
if (!flags.name || !flags.symbol) {
|
|
2795
|
-
if (!flags.name)
|
|
2796
|
-
warn(`no --name → default "${name}" would be the on-chain collection name`);
|
|
2797
|
-
if (!flags.symbol)
|
|
2798
|
-
warn(`no --symbol → default "${symbol}" would be the on-chain symbol`);
|
|
2799
|
-
if (!dryRun && !flags.yes) {
|
|
2800
|
-
throw new Error('refusing to write demo placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM (or --yes).');
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2840
|
+
assertRealIdentity(flags, { name, symbol, dryRun });
|
|
2803
2841
|
const maxProvided = flags.max !== undefined;
|
|
2804
2842
|
const max = Number(flags.max ?? 16);
|
|
2805
2843
|
if (!Number.isInteger(max) || max <= 0)
|
|
@@ -3640,7 +3678,7 @@ async function loadEffects() {
|
|
|
3640
3678
|
*/
|
|
3641
3679
|
async function cmdRender(address, tokenIds, flags) {
|
|
3642
3680
|
if (!address || address.startsWith('--')) {
|
|
3643
|
-
console.error('usage: abx render <address> [tokenId…] [--force] [--remote [url]] (missing stills/traits; --force re-renders an existing one — abx render --help)\n');
|
|
3681
|
+
console.error('usage: abx render <address> [tokenId…] [--force] [--remote [name|url]] (missing stills/traits; --force re-renders an existing one — abx render --help)\n');
|
|
3644
3682
|
process.exit(1);
|
|
3645
3683
|
}
|
|
3646
3684
|
// Token ids are decimal (mint order). Keep only those — `parseFlags` leaves `--flag value` pairs
|
|
@@ -3677,12 +3715,12 @@ async function cmdRender(address, tokenIds, flags) {
|
|
|
3677
3715
|
ok(line);
|
|
3678
3716
|
return;
|
|
3679
3717
|
}
|
|
3680
|
-
// --remote publishes each render to the
|
|
3718
|
+
// --remote publishes each render to the remote resolver's control plane (the locator bridge) so a
|
|
3681
3719
|
// laptop render lands on a resolver that doesn't share this disk; republish=true makes a re-run
|
|
3682
3720
|
// restore a resolver that lost its volume without re-rendering. Local (no --remote): shared backend.
|
|
3683
|
-
const remote =
|
|
3684
|
-
const resolverUrl = (remote ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
|
|
3685
|
-
const adminToken = remote ?
|
|
3721
|
+
const remote = remoteFlag(flags);
|
|
3722
|
+
const resolverUrl = (remote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
|
|
3723
|
+
const adminToken = remote ? requireRemoteToken(remote) : undefined;
|
|
3686
3724
|
// Co-located (no --remote): record each declared output into the shared store's artifact
|
|
3687
3725
|
// registry so the local resolver's `artifacts` manifest enumerates it. Remote: the publish
|
|
3688
3726
|
// lane (adminToken) records rows on the hosted resolver instead.
|
|
@@ -3717,8 +3755,11 @@ async function cmdRender(address, tokenIds, flags) {
|
|
|
3717
3755
|
* single time and exits (vs `abx render <addr>` which is the per-project repair lane).
|
|
3718
3756
|
*/
|
|
3719
3757
|
async function cmdEffects(flags) {
|
|
3720
|
-
const
|
|
3721
|
-
const
|
|
3758
|
+
const effectsRemote = remoteFlag(flags);
|
|
3759
|
+
const resolverUrl = (effectsRemote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
|
|
3760
|
+
// Token optional BY DESIGN: no token = the co-located topology (shared store, no publish lane).
|
|
3761
|
+
// A named remote brings its own token; otherwise the self-host env token.
|
|
3762
|
+
const adminToken = effectsRemote?.token ?? process.env.ABX_RESOLVER_ADMIN_TOKEN;
|
|
3722
3763
|
// Co-located (no admin token — we share the resolver's store): record each declared output into
|
|
3723
3764
|
// the shared artifact registry so the resolver's `artifacts` manifest enumerates it. With an
|
|
3724
3765
|
// admin token, the publish lane records rows on the hosted resolver instead.
|
|
@@ -3758,15 +3799,21 @@ async function cmdEffects(flags) {
|
|
|
3758
3799
|
await new Promise(() => { }); // block like `serve`
|
|
3759
3800
|
}
|
|
3760
3801
|
async function cmdIndex(address, flags) {
|
|
3761
|
-
const remote =
|
|
3802
|
+
const remote = remoteFlag(flags);
|
|
3762
3803
|
if (remote) {
|
|
3763
3804
|
if (!address || address.startsWith('--')) {
|
|
3764
|
-
console.error('usage: abx index <address> --remote [url] (re-index one project on a
|
|
3805
|
+
console.error('usage: abx index <address> --remote [name|url] (re-index one project on a remote resolver)\n');
|
|
3765
3806
|
process.exit(1);
|
|
3766
3807
|
}
|
|
3767
|
-
|
|
3768
|
-
info(`${bold('REMOTE')} → ${remote} ${dim('(re-indexing on the
|
|
3769
|
-
|
|
3808
|
+
requireRemoteToken(remote);
|
|
3809
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(re-indexing on the remote resolver — the post-deploy nudge)')}`);
|
|
3810
|
+
let r;
|
|
3811
|
+
try {
|
|
3812
|
+
r = await serviceClient(remote).registerProject({ chainId: resolveChain(CHAIN).id, address, full: flags.full ? true : undefined });
|
|
3813
|
+
}
|
|
3814
|
+
catch (err) {
|
|
3815
|
+
throw describeRemoteError(err, remote, 'remote index');
|
|
3816
|
+
}
|
|
3770
3817
|
ok(`remote resolver re-indexed ${r.project.name ?? address}: ${r.project.eventCount} events ${dim(`(${r.mode}, ${r.elapsedMs}ms)`)}`);
|
|
3771
3818
|
return;
|
|
3772
3819
|
}
|
|
@@ -3797,9 +3844,9 @@ async function cmdVerify(address, flags) {
|
|
|
3797
3844
|
process.exit(1);
|
|
3798
3845
|
}
|
|
3799
3846
|
allowLargeScan(flags);
|
|
3800
|
-
const remote =
|
|
3847
|
+
const remote = remoteFlag(flags);
|
|
3801
3848
|
if (remote)
|
|
3802
|
-
return cmdVerifyRemote(address, remote);
|
|
3849
|
+
return cmdVerifyRemote(address, remote.url);
|
|
3803
3850
|
const indexer = new SelfHostIndexer();
|
|
3804
3851
|
let state = indexer.getProject(address);
|
|
3805
3852
|
if (indexer.store.getRegistration(address)) {
|
|
@@ -4071,7 +4118,12 @@ async function cmdTokenUri(address, flags) {
|
|
|
4071
4118
|
catch {
|
|
4072
4119
|
const code = await publicClient.getCode({ address }).catch(() => undefined);
|
|
4073
4120
|
if (!code || code === '0x') {
|
|
4074
|
-
|
|
4121
|
+
// Name the endpoint we actually asked. "No contract here" is indistinguishable from "you're
|
|
4122
|
+
// pointed at the wrong node", and a chain KEY doesn't disambiguate that — two endpoints can
|
|
4123
|
+
// both claim `sepolia` (a fork, a stale duplicate .env line) and only one has your contract.
|
|
4124
|
+
console.error(`abx tokenuri: no contract at ${address} on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}) — ` +
|
|
4125
|
+
`double-check the address, and that this endpoint is the network you deployed to. ` +
|
|
4126
|
+
`If you JUST deployed, give the tx a block or two to mine.\n`);
|
|
4075
4127
|
}
|
|
4076
4128
|
else {
|
|
4077
4129
|
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`);
|
|
@@ -4414,15 +4466,24 @@ async function cmdStatus() {
|
|
|
4414
4466
|
// test/junk deploys. On-chain data is untouched; `abx add` can re-register it.
|
|
4415
4467
|
async function cmdForget(address, flags) {
|
|
4416
4468
|
if (!address || address.startsWith('--')) {
|
|
4417
|
-
console.error('usage: abx forget <address> [--remote [url]]\n');
|
|
4469
|
+
console.error('usage: abx forget <address> [--remote [name|url]]\n');
|
|
4418
4470
|
process.exit(1);
|
|
4419
4471
|
}
|
|
4420
|
-
const remote =
|
|
4472
|
+
const remote = remoteFlag(flags);
|
|
4421
4473
|
if (remote) {
|
|
4422
|
-
|
|
4423
|
-
info(`${bold('REMOTE')} → ${remote} ${dim('(deregistering on the
|
|
4424
|
-
|
|
4425
|
-
|
|
4474
|
+
requireRemoteToken(remote);
|
|
4475
|
+
info(`${bold('REMOTE')} → ${remote.url} ${dim('(deregistering on the remote resolver — NOT this machine)')}`);
|
|
4476
|
+
let removed;
|
|
4477
|
+
try {
|
|
4478
|
+
({ removed } = await serviceClient(remote).removeProject(resolveChain(CHAIN).id, address));
|
|
4479
|
+
}
|
|
4480
|
+
catch (err) {
|
|
4481
|
+
throw describeRemoteError(err, remote, 'remote forget');
|
|
4482
|
+
}
|
|
4483
|
+
if (removed)
|
|
4484
|
+
ok(`remote resolver forgot ${address} — it will stop serving it. On-chain data is untouched.`);
|
|
4485
|
+
else
|
|
4486
|
+
console.log(dim(` ${address} wasn't registered on ${remote.url} — nothing to forget.`));
|
|
4426
4487
|
return;
|
|
4427
4488
|
}
|
|
4428
4489
|
const indexer = new SelfHostIndexer();
|
|
@@ -4433,6 +4494,94 @@ async function cmdForget(address, flags) {
|
|
|
4433
4494
|
indexer.store.deregister(address);
|
|
4434
4495
|
ok(`forgot ${address} — dropped its registration + projection. On-chain data is untouched.`);
|
|
4435
4496
|
}
|
|
4497
|
+
// ── remote ────────────────────────────────────────────────────────────────--
|
|
4498
|
+
// Inspect a remote service. Bare `abx remote` lists the named remotes configured in .env plus the
|
|
4499
|
+
// self-host default pair (URLs + whether a token is set — never the secret itself). With a target,
|
|
4500
|
+
// fetches its PUBLIC service descriptor (what it serves: interfaces, chains, auth, managed
|
|
4501
|
+
// rendering) and — when a token resolves — lists the projects visible to that token, which makes
|
|
4502
|
+
// this the one-command "is my provider key valid?" check. Read-only; registers nothing.
|
|
4503
|
+
async function cmdRemote(spec, flags) {
|
|
4504
|
+
if (!spec || spec.startsWith('--')) {
|
|
4505
|
+
const remotes = listConfiguredRemotes();
|
|
4506
|
+
console.log(`\n ${bold('named remotes')} ${dim('(ABX_REMOTE_<NAME>_URL/_TOKEN in .env — inspect one: abx remote <name>)')}`);
|
|
4507
|
+
if (remotes.length === 0)
|
|
4508
|
+
console.log(dim(' none configured'));
|
|
4509
|
+
for (const r of remotes) {
|
|
4510
|
+
console.log(` ${g('●')} ${r.name.toLowerCase()} ${dim(r.url)} ${r.hasToken ? g('token set') : dim('no token')}`);
|
|
4511
|
+
}
|
|
4512
|
+
// A near-miss var reads as "no token" while the value is sitting in .env under the wrong name.
|
|
4513
|
+
for (const bad of misnamedRemoteVars()) {
|
|
4514
|
+
warn(`${bad.key} isn't a recognized remote var — the convention is ${bold(bad.suggestion)} (only _URL and _TOKEN are read).`);
|
|
4515
|
+
}
|
|
4516
|
+
const def = process.env.ABX_PUBLIC_BASE_URL ?? process.env.ABX_RESOLVER_URL;
|
|
4517
|
+
console.log(`\n ${bold('self-host default')} ${dim('(bare --remote)')}`);
|
|
4518
|
+
console.log(def
|
|
4519
|
+
? ` ${g('●')} ${def} ${process.env.ABX_RESOLVER_ADMIN_TOKEN ? g('token set') : dim('no ABX_RESOLVER_ADMIN_TOKEN')}`
|
|
4520
|
+
: dim(' none (set ABX_PUBLIC_BASE_URL in .env)'));
|
|
4521
|
+
console.log('');
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
const target = resolveRemote(spec, flags['remote-token']);
|
|
4525
|
+
if (!target)
|
|
4526
|
+
return;
|
|
4527
|
+
const client = serviceClient(target);
|
|
4528
|
+
console.log(`\n ${bold(target.name ? `remote ${target.name.toLowerCase()}` : 'remote')} ${dim(`→ ${target.url}`)}`);
|
|
4529
|
+
let d;
|
|
4530
|
+
try {
|
|
4531
|
+
d = await client.descriptor();
|
|
4532
|
+
}
|
|
4533
|
+
catch (err) {
|
|
4534
|
+
// Two very different situations, and the fix differs — so don't nest the raw client error
|
|
4535
|
+
// (it repeats the URL and leaks `GET`/`fetch failed` at a creator).
|
|
4536
|
+
const status = err instanceof AbxServiceError ? err.status : -1;
|
|
4537
|
+
if (status === 0) {
|
|
4538
|
+
throw new Error(`nothing responded at ${target.url} — check the address. A provider gives you an https:// base ` +
|
|
4539
|
+
`(e.g. https://meta.provider.xyz); if it's your own node, is it running?`);
|
|
4540
|
+
}
|
|
4541
|
+
throw new Error(`${target.url} answered, but serves no ABX service descriptor at /.well-known/abx-service. ` +
|
|
4542
|
+
`That's either an older self-hosted node (fine if it's yours — the remote commands still work against it) ` +
|
|
4543
|
+
`or not an ABX service at all. Verify the URL before registering anything with it.`);
|
|
4544
|
+
}
|
|
4545
|
+
info(`service ${d.service?.name ?? '—'} ${dim(d.service?.version ?? '')}`);
|
|
4546
|
+
info(`serves ${(d.interfaces ?? []).join(' · ') || '—'}`);
|
|
4547
|
+
const chainId = resolveChain(CHAIN).id;
|
|
4548
|
+
const coversChain = (d.chains ?? []).includes(chainId);
|
|
4549
|
+
info(`chains ${(d.chains ?? []).join(', ') || '—'} ${coversChain ? g(`✓ covers ${CHAIN} (${chainId})`) : `${c.orange}⚠${c.reset} does NOT cover ${CHAIN} (${chainId}) — registrations will be refused`}`);
|
|
4550
|
+
if (d.render?.attached) {
|
|
4551
|
+
const outputs = d.render.effects?.flatMap((e) => e.outputs.map((o) => `${e.key}/${o.key}`)).join(', ');
|
|
4552
|
+
info(`rendering managed behind this service${outputs ? ` (${outputs})` : d.render.effects === null ? dim(' (attached — runner unverified right now)') : ''} — code drops need no effects runner here`);
|
|
4553
|
+
}
|
|
4554
|
+
if (d.auth) {
|
|
4555
|
+
info(`auth bearer${d.auth.signupUrl ? ` · get a key: ${d.auth.signupUrl}` : ''}${d.auth.docsUrl ? ` · docs: ${d.auth.docsUrl}` : ''}`);
|
|
4556
|
+
}
|
|
4557
|
+
else {
|
|
4558
|
+
info(`auth none advertised ${dim('(control plane disabled on this node)')}`);
|
|
4559
|
+
}
|
|
4560
|
+
if (!target.token) {
|
|
4561
|
+
// Same near-miss check the register path does — this is where someone lands FIRST when their
|
|
4562
|
+
// credential is set under a name the CLI doesn't read, so the hint has to be here too.
|
|
4563
|
+
const nearMiss = misnamedRemoteVars().find((v) => v.suggestion === target.tokenVar);
|
|
4564
|
+
if (nearMiss)
|
|
4565
|
+
warn(`${nearMiss.key} is set but is NOT read — the convention is ${bold(target.tokenVar)} (only _URL and _TOKEN). Rename it and re-run.`);
|
|
4566
|
+
else
|
|
4567
|
+
info(dim(`no token resolved (set ${target.tokenVar} or pass --remote-token) — descriptor only; can't list your projects.`));
|
|
4568
|
+
console.log('');
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
try {
|
|
4572
|
+
const projects = await client.listProjects();
|
|
4573
|
+
ok(`token accepted — ${projects.length} project(s) visible to it`);
|
|
4574
|
+
for (const p of projects.slice(0, 10)) {
|
|
4575
|
+
console.log(` ${g('●')} ${p.name ?? p.label ?? p.address} ${dim(`${p.address} · ${p.tokenCount ?? '?'} token(s)`)}`);
|
|
4576
|
+
}
|
|
4577
|
+
if (projects.length > 10)
|
|
4578
|
+
console.log(dim(` … and ${projects.length - 10} more`));
|
|
4579
|
+
}
|
|
4580
|
+
catch (err) {
|
|
4581
|
+
throw describeRemoteError(err, target, 'remote list');
|
|
4582
|
+
}
|
|
4583
|
+
console.log('');
|
|
4584
|
+
}
|
|
4436
4585
|
// ── migrate ───────────────────────────────────────────────────────────────--
|
|
4437
4586
|
// Move a contract's OFF-CHAIN operator state from one resolver to another (e.g. fly.io →
|
|
4438
4587
|
// a droplet). The destination replays all ON-CHAIN state from chain itself; this bridges
|
|
@@ -4441,13 +4590,17 @@ async function cmdForget(address, flags) {
|
|
|
4441
4590
|
// and write through the admin control plane. It does NOT cut over — after a clean migration the
|
|
4442
4591
|
// operator re-points DNS (custom domain) or the on-chain base URI (provider endpoint).
|
|
4443
4592
|
async function cmdMigrate(address, flags) {
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4593
|
+
// Both sides accept a named remote or a URL. Only the DESTINATION needs a credential — the
|
|
4594
|
+
// source is read via its PUBLIC api (the exit ramp works with zero provider cooperation).
|
|
4595
|
+
const fromTarget = typeof flags.from === 'string' ? resolveRemote(flags.from) : null;
|
|
4596
|
+
const toTarget = typeof flags.to === 'string' ? resolveRemote(flags.to, flags['remote-token']) : null;
|
|
4597
|
+
if (!address || address.startsWith('--') || !fromTarget || !toTarget) {
|
|
4598
|
+
console.error('usage: abx migrate <address> --from <source-resolver name|url> --to <dest-resolver name|url> [--from-block N]\n');
|
|
4448
4599
|
process.exit(1);
|
|
4449
4600
|
}
|
|
4450
|
-
|
|
4601
|
+
requireRemoteToken(toTarget);
|
|
4602
|
+
const from = fromTarget.url;
|
|
4603
|
+
const to = toTarget.url;
|
|
4451
4604
|
const chainId = resolveChain(CHAIN).id;
|
|
4452
4605
|
allowLargeScan(flags);
|
|
4453
4606
|
console.log(bold(`\n abx migrate ${dim('— port off-chain state between resolvers (no cutover)')}`));
|
|
@@ -4504,6 +4657,7 @@ async function cmdMigrate(address, flags) {
|
|
|
4504
4657
|
step('Populate the destination');
|
|
4505
4658
|
const locCount = Object.keys(plan.contentLocators).length;
|
|
4506
4659
|
const body = {
|
|
4660
|
+
chainId,
|
|
4507
4661
|
address,
|
|
4508
4662
|
// The deploy block (locally known); if somehow absent, omit it so the destination derives it
|
|
4509
4663
|
// rather than scanning from genesis (never bake a from-0 floor into a fresh resolver).
|
|
@@ -4516,8 +4670,14 @@ async function cmdMigrate(address, flags) {
|
|
|
4516
4670
|
contentLocators: locCount ? plan.contentLocators : undefined,
|
|
4517
4671
|
full: true, // first registration on the destination — replay from the deploy block
|
|
4518
4672
|
};
|
|
4519
|
-
info(`${bold('REMOTE')} → ${to} ${dim('(
|
|
4520
|
-
|
|
4673
|
+
info(`${bold('REMOTE')} → ${to} ${dim('(control plane — chain replay + off-chain enrichment)')}`);
|
|
4674
|
+
let r;
|
|
4675
|
+
try {
|
|
4676
|
+
r = await serviceClient(toTarget).registerProject(body);
|
|
4677
|
+
}
|
|
4678
|
+
catch (err) {
|
|
4679
|
+
throw describeRemoteError(err, toTarget, 'migrate destination');
|
|
4680
|
+
}
|
|
4521
4681
|
ok(`destination indexed ${r.project.name ?? address}: ${r.project.eventCount} events ${dim(`(${r.mode}, ${r.elapsedMs}ms)`)}`);
|
|
4522
4682
|
// 5) Parity check — does the destination now serve the same metadata as the source? Sample a
|
|
4523
4683
|
// token we did NOT re-pin (a re-pinned image is durable-locator-on-dest vs old-host-on-source
|
|
@@ -4853,7 +5013,8 @@ async function cmdDeployResolver(flags) {
|
|
|
4853
5013
|
const url = art.baseUrl;
|
|
4854
5014
|
info(`abx deploy --image <art> --name … --public-base-url ${url} (or export ABX_PUBLIC_BASE_URL=${url})`);
|
|
4855
5015
|
info(`the contract derives ${url}/t/${resolveChain(CHAIN).id}/{address}/{tokenId} from that base.`);
|
|
4856
|
-
info(`${bold('then')} abx add <clone> --remote ${dim('# tell the
|
|
5016
|
+
info(`${bold('then')} abx add <clone> --remote ${dim('# tell the remote resolver to index it — a LOCAL deploy does NOT')}`);
|
|
5017
|
+
info(dim(`prefer addressing it by name? add ABX_REMOTE_<NAME>_URL=${url} (+ ABX_REMOTE_<NAME>_TOKEN=<the same token>) to .env → abx add <clone> --remote <name>`));
|
|
4857
5018
|
console.log('');
|
|
4858
5019
|
}
|
|
4859
5020
|
// ── deploy-effects: scaffold the render runner (the resolver's browser-bearing companion) ─────
|
|
@@ -5152,8 +5313,9 @@ const COMMAND_HELP = {
|
|
|
5152
5313
|
No tokenId → sweeps all minted tokens; pass ids (${g('0 1 2')}) to target specific tokens.
|
|
5153
5314
|
${g('--force')} RE-RENDER even when the still already exists — the fix for a bad / blank / timed-out capture
|
|
5154
5315
|
(the art is otherwise deterministic, so a plain render idempotent-skips an existing still). Overwrites it (+ republishes on --remote).
|
|
5155
|
-
${g('--remote [url]')}
|
|
5156
|
-
Idempotent; a re-run restores a resolver that lost its volume. Needs
|
|
5316
|
+
${g('--remote [name|url]')} publish each render to a REMOTE resolver (the locator bridge: upload to ${bold('ABX_STORAGE_BACKEND')}, POST /v1/effect-artifacts).
|
|
5317
|
+
Idempotent; a re-run restores a resolver that lost its volume. Needs its token (a named remote's
|
|
5318
|
+
${g('ABX_REMOTE_<NAME>_TOKEN')}, else ${g('ABX_RESOLVER_ADMIN_TOKEN')}).
|
|
5157
5319
|
--effects-url <url> enqueue on a running effect-runner service instead of rendering inline (else ${g('ABX_EFFECTS_URL')})
|
|
5158
5320
|
${dim('Inline (no --remote) renders on THIS machine (needs `npx playwright install chromium` once), pointing Chromium at the live')}
|
|
5159
5321
|
${dim(`view of ${g('ABX_RESOLVER_URL')} (else your configured base / local ${g('abx serve')}), and stores to the resolved backend.`)}
|
|
@@ -5169,8 +5331,8 @@ const COMMAND_HELP = {
|
|
|
5169
5331
|
--port <n> HTTP port (default ${g('ABX_EFFECTS_PORT')} / 8788) — ${g('POST /notify')} enqueues (watcher lane) · ${g('POST /run')} sweeps synchronously (command lane)
|
|
5170
5332
|
--interval-ms <n> the SAFETY-FLOOR sweep (default ${g('ABX_EFFECTS_INTERVAL_MS')} / 300000) — catches a missed notify / cold start; the watcher is the trigger
|
|
5171
5333
|
--concurrency <n> parallel renders while draining (default ${g('ABX_EFFECTS_CONCURRENCY')} / 1 — Chromium is heavy; raise deliberately)
|
|
5172
|
-
${dim('Co-located with a local `abx serve` (same store) → no tokens needed. Against a
|
|
5173
|
-
${dim('
|
|
5334
|
+
${dim('Co-located with a local `abx serve` (same store) → no tokens needed. Against a REMOTE resolver: --remote <name|url> (its')}
|
|
5335
|
+
${dim('token publishes renders + reports status), or ABX_RESOLVER_URL + ABX_RESOLVER_ADMIN_TOKEN. PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
|
|
5174
5336
|
${dim('To HOST the runner (fly/docker), use `abx deploy-effects`.')}`,
|
|
5175
5337
|
'configure-param': `
|
|
5176
5338
|
${bold('abx configure-param')} <address> <tokenId|-> <key> <value> ${dim('— set a PostParam (typed, canonical encode). Sends a tx.')}
|
|
@@ -5181,7 +5343,7 @@ const COMMAND_HELP = {
|
|
|
5181
5343
|
like ${g('params.keys')} / ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
|
|
5182
5344
|
After a token write, a project whose ${g('params.keys')} doesn't list the key gets a one-line fix suggestion.
|
|
5183
5345
|
--file <path> read the value from a file (String / Bytes payloads)
|
|
5184
|
-
${g('--remote [url]')}
|
|
5346
|
+
${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
|
|
5185
5347
|
the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
|
|
5186
5348
|
resolver running the chain watcher (the ${g('abx serve')} default) sees the change on its next poll (~12s)
|
|
5187
5349
|
and auto-re-renders on its own. Keep --remote for a watcher-disabled resolver or when seconds matter.
|
|
@@ -5365,13 +5527,16 @@ const COMMAND_HELP = {
|
|
|
5365
5527
|
${bold('abx add')} <address> ${dim('— register + index a project. Also edits off-chain display metadata + traits.')}
|
|
5366
5528
|
--from-block <n> --factory 0x.. --label "<s>" --description "<s>" --external-url <url> [--full] [--yes]
|
|
5367
5529
|
--traits "K=V; K2=V2" / --attributes <file.json> set the off-chain operator traits (on-chain attributes always win)
|
|
5368
|
-
${g('--remote [url]')}
|
|
5530
|
+
${g('--remote [name|url]')} target a REMOTE resolver instead of this machine — bridges the image locator (ipfs://…) + traits to it.
|
|
5531
|
+
A ${bold('name')} reads ${g('ABX_REMOTE_<NAME>_URL')} + ${g('ABX_REMOTE_<NAME>_TOKEN')} from .env (a managed provider's API key);
|
|
5532
|
+
a URL (or bare --remote = ${g('ABX_PUBLIC_BASE_URL')}) uses ${g('ABX_RESOLVER_ADMIN_TOKEN')}. Inspect first: ${g('abx remote <name>')}
|
|
5533
|
+
--remote-token <t> override the token for this invocation (--token means a token ID elsewhere, hence the name)`,
|
|
5369
5534
|
index: `
|
|
5370
5535
|
${bold('abx index')} [<address>] ${dim('— re-index from chain (read-only). Incremental by default.')}
|
|
5371
5536
|
${g('--full')} force a full replay from the deploy block (the durability proof) --yes allow a very large scan`,
|
|
5372
5537
|
verify: `
|
|
5373
5538
|
${bold('abx verify')} <address> ${dim('— re-hash the served bytes against the on-chain commitment (read-only; no server).')}
|
|
5374
|
-
${g('--remote [url]')}
|
|
5539
|
+
${g('--remote [name|url]')} verify what a REMOTE resolver actually serves (else ABX_PUBLIC_BASE_URL) — probes its \`/image\` (302→locator
|
|
5375
5540
|
or 200 bytes), so it accounts for a render PUBLISHED to that resolver. ${bold('Use this for a code project whose')}
|
|
5376
5541
|
${bold('renders were published to a hosted resolver')} — a plain \`abx verify\` only checks THIS machine's store and will
|
|
5377
5542
|
report a false placeholder for a render that lives on the resolver.
|
|
@@ -5381,11 +5546,21 @@ const COMMAND_HELP = {
|
|
|
5381
5546
|
${bold('abx tokenuri')} <address> [--token <id>] ${dim('— read tokenURI(id) straight from the contract on-chain + decode the JSON (read-only; no server).')}
|
|
5382
5547
|
${dim('The proof a fully on-chain token self-resolves: any RPC returns the renderer-assembled metadata. Default token 0.')}`,
|
|
5383
5548
|
forget: `
|
|
5384
|
-
${bold('abx forget')} <address> ${dim('— drop a project’s local registration + projection. On-chain data is untouched.')}
|
|
5549
|
+
${bold('abx forget')} <address> ${dim('— drop a project’s local registration + projection. On-chain data is untouched.')}
|
|
5550
|
+
${g('--remote [name|url]')} deregister on a REMOTE resolver instead (it stops serving the project; re-add any time)`,
|
|
5551
|
+
remote: `
|
|
5552
|
+
${bold('abx remote')} [<name|url>] ${dim('— inspect a remote service (read-only; registers nothing).')}
|
|
5553
|
+
Bare: list the named remotes in .env (${g('ABX_REMOTE_<NAME>_URL')} / ${g('_TOKEN')} — token shown as set/unset, never printed)
|
|
5554
|
+
plus the self-host default (bare --remote = ${g('ABX_PUBLIC_BASE_URL')} + ${g('ABX_RESOLVER_ADMIN_TOKEN')}).
|
|
5555
|
+
With a target: fetch its PUBLIC ${g('/.well-known/abx-service')} descriptor — what it serves (interfaces), which chains
|
|
5556
|
+
(flags a mismatch with your ${g('ABX_CHAIN')}), whether ${bold('rendering is managed')} behind it (code drops then need no effects
|
|
5557
|
+
runner), and where a human gets an API key (${g('auth.signupUrl')}). With a token: lists the projects visible to it —
|
|
5558
|
+
${bold('the one-command "is my provider key valid?" check')} (401 = fix the key · 403 = provider-side scoping, not a typo).`,
|
|
5385
5559
|
migrate: `
|
|
5386
5560
|
${bold('abx migrate')} <address> ${dim('— move a contract\'s OFF-CHAIN state to another resolver (read-only on both; no cutover).')}
|
|
5387
|
-
--from <url>
|
|
5388
|
-
--to <url>
|
|
5561
|
+
--from <name|url> the SOURCE resolver (currently serving the contract) — read via its PUBLIC api; no source credential needed
|
|
5562
|
+
--to <name|url> the DESTINATION resolver (its control plane) — the ONE credential migrate needs: a named remote's
|
|
5563
|
+
${g('ABX_REMOTE_<NAME>_TOKEN')}, else ${g('ABX_RESOLVER_ADMIN_TOKEN')} (your own node's, from ${g('deploy-resolver')}), else --remote-token
|
|
5389
5564
|
--from-block <n> chain scan floor for the local read (default: the deploy block, discovered on-chain — never genesis) --yes allow a very large scan
|
|
5390
5565
|
--backend <id> durable custody for re-pinning source-only images (ipfs · arweave); else config/env
|
|
5391
5566
|
${dim('replays on-chain state on the dest from chain, then bridges description / external_url / off-chain')}
|
|
@@ -5435,7 +5610,8 @@ function help() {
|
|
|
5435
5610
|
--minter 0x.. · --primary-payee 0x.. · --unpaused · ${g('--dry-run')} · see ${g('abx help deploy-series')}
|
|
5436
5611
|
${g('abx predict')} pre-compute a deploy address flags: [--salt 0x..] [--for 0x..] [--factory 0x..]
|
|
5437
5612
|
${g('abx add')} <address> register + index a project this node didn't deploy
|
|
5438
|
-
flags: --from-block --factory --label
|
|
5613
|
+
flags: --from-block --factory --label · ${g('--remote <name|url>')} registers on a REMOTE resolver instead
|
|
5614
|
+
${g('abx remote')} [<name|url>] inspect a remote service: its descriptor (chains · managed rendering · where to get a key) + your projects there
|
|
5439
5615
|
${g('abx index')} [<address>] re-index from chain (incremental by default; ${g('--full')} forces a replay from deploy)
|
|
5440
5616
|
${g('abx verify')} <addr> re-hash served bytes vs the on-chain commitment (no server needed)
|
|
5441
5617
|
${g('abx tokenuri')} <addr> read tokenURI(0) on-chain + decode the JSON (proof a self-resolving token works)
|