@artblocks/abx-cli 0.1.0-alpha.11 → 0.1.0-alpha.13
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/gas.d.ts +63 -0
- package/dist/gas.d.ts.map +1 -0
- package/dist/gas.js +76 -0
- package/dist/gas.js.map +1 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +121 -15
- package/dist/inspect.js.map +1 -1
- package/dist/main.js +349 -49
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +22 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +163 -5
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +6 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +59 -23
- package/dist/preview.js.map +1 -1
- package/dist/schema.d.ts +12 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +62 -7
- package/dist/schema.js.map +1 -1
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +38 -2
- package/dist/signer.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +27 -38
- package/skill/reference/code-projects.md +37 -2
- package/skill/reference/decisions.md +61 -0
- package/skill/reference/hosting.md +4 -4
package/dist/main.js
CHANGED
|
@@ -55,15 +55,16 @@ import { effectsArtifact, resolverArtifact } from './provision.js';
|
|
|
55
55
|
import { mintPageArtifact, PUBLIC_RPC } from './mintpage.js';
|
|
56
56
|
import { formatEther, toHex, zeroAddress } from 'viem';
|
|
57
57
|
import { encodeFunctionData } from 'viem';
|
|
58
|
-
import { DEP_RESOLUTION, deploySeedSource, deploySeriesCodeFactory, prepareCodeSetup, prepareDeploySeriesCode, resolveGenerator, resolveSeedSource, resolveSeriesCodeFactory, seriesCodeAbi, seriesCodeFactoryAbi, } from '@artblocks/abx-sdk';
|
|
58
|
+
import { DEP_RESOLUTION, deploySeedSource, deploySeriesCodeFactory, prepareCodeSetup, prepareDeploySeriesCode, resolveGenerator, resolveSeedSource, resolveSeriesCodeFactory, seriesCodeAbi, readParamSchema, seriesCodeFactoryAbi, } from '@artblocks/abx-sdk';
|
|
59
59
|
import { checkRegistryDeps, dependencySetupCalls, parseDepFlag, resolveDepRegistryPointer } from './deps.js';
|
|
60
|
-
import { composeParamsKeys, expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport } from './onchain-uri.js';
|
|
60
|
+
import { composeParamsKeys, expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport, readParamsKeys } from './onchain-uri.js';
|
|
61
61
|
import { parseFlags, unknownFlags } from './flags.js';
|
|
62
62
|
import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, readSkillVersion, SKILL_DIR_NAME, } from './update-check.js';
|
|
63
63
|
import { analyzeScript, recommendLane } from './inspect.js';
|
|
64
64
|
import { previewConfigFromFlags, previewDepTags, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
65
65
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
66
66
|
import { declinesSkillInstall } from './prompt.js';
|
|
67
|
+
import { pinGas, waitForCodeAt } from './gas.js';
|
|
67
68
|
import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
|
|
68
69
|
/** The `--schema` type/auth/format catalog, shown wherever the CLI nudges `--schema`. Kept accurate
|
|
69
70
|
* to the on-chain enums (PARAM_TYPES / AUTH_OPTIONS) + the Select-options / Range-bounds format —
|
|
@@ -73,15 +74,38 @@ const SCHEMA_CATALOG = 'Types: Bool·Select·Uint256Range·Int256Range·DecimalR
|
|
|
73
74
|
'Select needs options — key:Select[A|B|C]:Auth; a Range takes bounds — key:Uint256Range[0..100]:Auth. ' +
|
|
74
75
|
'A palette collectors set = palette:HexColor:TokenOwner';
|
|
75
76
|
import { uploadAndLocate } from './upload.js';
|
|
76
|
-
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, indexProgress, isAccepted, AbxIndexTimeoutError, AbxServiceError, } from '@artblocks/abx-sdk';
|
|
77
|
+
import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, resolveRpcUrl, redactRpcUrl, explorerUrl, DEFAULT_CHAIN_KEY, KNOWN_CHAIN_KEYS, normalizeAttributes, parseTraitPairs, METADATA_FIELD as F, METADATA_REPRESENTATION as R, indexProgress, isAccepted, AbxIndexTimeoutError, AbxServiceError, } from '@artblocks/abx-sdk';
|
|
77
78
|
import { SelfHostIndexer, SqliteStore } from '@artblocks/abx-indexer';
|
|
78
79
|
import { artContentHash, currentRenderArtifact, generateArt, resolveBaseUrl, startChainWatcher, startTokenApiServer, verifyProject, watchIntervalMs, DEFAULT_PORT, } from '@artblocks/abx-token-api';
|
|
79
80
|
import { ARWEAVE_FREE_UPLOAD_LIMIT, arweaveAddress, arweaveFunding, contentTypeFromPath, hashContent, resolveBackend, turboBalanceForAddress, turboUploadCostUsd, turboUploadWinc } from '@artblocks/abx-storage';
|
|
80
|
-
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';
|
|
81
|
+
import { cmdTransfer, cmdMint, cmdSetMinter, cmdSetMaxInvocations, cmdConfigureParam, cmdSetSchema, cmdRetireParam, 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';
|
|
81
82
|
import { openWalletSession, signTx } from './signer.js';
|
|
82
83
|
import { describeRemoteError, listConfiguredRemotes, misnamedRemoteVars, requireRemoteToken, resolveRemote, serviceClient, tokenSourceLabel } from './remote.js';
|
|
83
84
|
import { buildMigrationPlan, repinNodeCustody, verifyParity } from './migrate.js';
|
|
84
85
|
import { activeBackendId, arweaveKeyFilePath, backendResolution, ensureArweaveJwk, factoryAddress, seriesFactoryAddress, fixedPriceMinterAddress, loadArweaveJwk, loopbackBaseUrl, faucetHint, rendererAddress, storageOptions, storageSignerChoice, } from './config.js';
|
|
86
|
+
/**
|
|
87
|
+
* Validate `ABX_CHAIN` at module load, BEFORE anything derives from it.
|
|
88
|
+
*
|
|
89
|
+
* This has to run here, not inside `main()`. Chain-derived constants are evaluated during module
|
|
90
|
+
* evaluation (the `EXPLORER` line right below, and — until they were made lazy — several in the
|
|
91
|
+
* token-api package the CLI imports), so an unknown value threw before `main()` ever ran. The result
|
|
92
|
+
* was a raw Node stack trace quoting an internal source path, for EVERY command, including `doctor`
|
|
93
|
+
* — whose entire job is telling you what is wrong with your environment. A cold agent hit exactly
|
|
94
|
+
* this and reported the tool as broken. A typo should get an answer, not a crash.
|
|
95
|
+
*/
|
|
96
|
+
function assertKnownChainEnv() {
|
|
97
|
+
const key = process.env.ABX_CHAIN;
|
|
98
|
+
if (!key || KNOWN_CHAIN_KEYS.includes(key))
|
|
99
|
+
return;
|
|
100
|
+
const mainnetish = /^(mainnet|ethereum|homestead|base|eth|1|8453)$/i.test(key.trim());
|
|
101
|
+
process.stderr.write(`\n\u001b[31m\u2717\u001b[0m ABX_CHAIN="${key}" is not a chain this toolkit ships. Known: ${KNOWN_CHAIN_KEYS.join(', ')}.\n` +
|
|
102
|
+
(mainnetish
|
|
103
|
+
? ` Mainnet is not supported yet \u2014 ABX is testnet-only today. Use base-sepolia (the default) or sepolia;\n` +
|
|
104
|
+
` a testnet launch exercises the real thing end to end, just without real money.\n\n`
|
|
105
|
+
: ` Unset it to use the default (${DEFAULT_CHAIN_KEY}), or set one of the above.\n\n`));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
assertKnownChainEnv();
|
|
85
109
|
const CHAIN = process.env.ABX_CHAIN ?? DEFAULT_CHAIN_KEY;
|
|
86
110
|
// Block explorer base for the tx/address links the CLI prints. Derived from viem's chain metadata via
|
|
87
111
|
// the SDK (see explorerUrl) rather than a local table — this used to be a hand-maintained map, which is
|
|
@@ -186,6 +210,8 @@ async function main() {
|
|
|
186
210
|
case 'set-minter': return cmdSetMinter(rest[0], flags);
|
|
187
211
|
case 'set-max-invocations': return cmdSetMaxInvocations(rest[0], flags);
|
|
188
212
|
case 'configure-param': return cmdConfigureParam(rest[0], rest.slice(1), flags);
|
|
213
|
+
case 'set-schema': return cmdSetSchema(rest[0], flags);
|
|
214
|
+
case 'retire-param': return cmdRetireParam(rest[0], rest.slice(1), flags);
|
|
189
215
|
case 'set-param-hooks': return cmdSetParamHooks(rest[0], flags);
|
|
190
216
|
case 'set-dependency': return cmdSetDependency(rest[0], rest.slice(1), flags);
|
|
191
217
|
case 'remove-last-dependency': return cmdRemoveLastDependency(rest[0], flags);
|
|
@@ -708,7 +734,51 @@ const imageUrlTemplateField = (template) => ({
|
|
|
708
734
|
representation: encodeTag(R.urlTemplate),
|
|
709
735
|
value: toHex(template),
|
|
710
736
|
});
|
|
737
|
+
/** Backends that can hand out a publicly-readable URL for uploaded bytes — the ones that make
|
|
738
|
+
* "image off-chain, JSON on-chain, no server" possible. `fs` serves only from this machine. */
|
|
739
|
+
const DIRECT_URL_BACKENDS = new Set(['ipfs', 'arweave', 'cloud']);
|
|
711
740
|
const looksLikeSvg = (s) => /^\s*<(\?xml|svg)/i.test(s);
|
|
741
|
+
/**
|
|
742
|
+
* Does the IMAGE itself end up on-chain for this deploy? Two ways it can:
|
|
743
|
+
* • `--onchain-image` — the bytes are staged in the chunk store and read back via a `reader` field.
|
|
744
|
+
* • `--onchain-uri` with an SVG — v1 inlines SVG directly as a data: URI (raster cannot be).
|
|
745
|
+
* Anything else falls through to keccak256 custody, and the on-chain renderer serves its PLACEHOLDER
|
|
746
|
+
* image. This distinction is the difference between a token that is durable and one that only looks
|
|
747
|
+
* durable, so it must be computed from the actual file — never inferred from the flag alone (the
|
|
748
|
+
* success banner used to claim "fully on-chain, no hosting needed" for a keccak-anchored raster).
|
|
749
|
+
*/
|
|
750
|
+
/**
|
|
751
|
+
* Will `tokenURI` resolve to the creator's REAL image, with nothing to keep running?
|
|
752
|
+
*
|
|
753
|
+
* Three ways yes, and they are not the same promise: the bytes are on-chain
|
|
754
|
+
* ({@link imageEndsUpOnChain}), or they sit at a durable public URL that the on-chain JSON points at
|
|
755
|
+
* (a direct-URL backend — the "no server" pattern). Only `fs` custody under `--onchain-uri` fails,
|
|
756
|
+
* because the renderer then holds a hash and nothing can serve it. Keep this separate from
|
|
757
|
+
* "fully on-chain": conflating them is what let a keccak-anchored raster ship under a permanence
|
|
758
|
+
* banner it did not earn.
|
|
759
|
+
*/
|
|
760
|
+
function imageResolvesWithoutServer(flags, onchainImage) {
|
|
761
|
+
if (imageEndsUpOnChain(flags, onchainImage))
|
|
762
|
+
return true;
|
|
763
|
+
return !!flags.image && DIRECT_URL_BACKENDS.has(backendResolution(storageOverrides(flags)).backend);
|
|
764
|
+
}
|
|
765
|
+
function imageEndsUpOnChain(flags, onchainImage) {
|
|
766
|
+
if (onchainImage)
|
|
767
|
+
return true;
|
|
768
|
+
const image = typeof flags.image === 'string' ? flags.image : undefined;
|
|
769
|
+
if (!image)
|
|
770
|
+
return false;
|
|
771
|
+
try {
|
|
772
|
+
const p = resolvePath(image);
|
|
773
|
+
if (!statSync(p).isFile())
|
|
774
|
+
return false;
|
|
775
|
+
// Only the head matters — looksLikeSvg tests the leading tag.
|
|
776
|
+
return looksLikeSvg(readFileSync(p).subarray(0, 256).toString('utf8'));
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
712
782
|
async function prepareContent(imagePath, clone, overrides, store = true, // false for --dry-run: compute the hash but don't custody bytes
|
|
713
783
|
onChain = false, // true (--onchain-uri): put the image ON-CHAIN (inline SVG) so it self-resolves
|
|
714
784
|
remoteEth, // wallet-lane Turbo uploads: sign+pay with the connected browser wallet
|
|
@@ -730,9 +800,8 @@ walletAddr) {
|
|
|
730
800
|
const path = resolvePath(imagePath);
|
|
731
801
|
const bytes = new Uint8Array(readFileSync(path));
|
|
732
802
|
const contentType = contentTypeFromPath(path);
|
|
733
|
-
// On-chain mode can inline SVG directly (the renderer emits it as a data: URI).
|
|
734
|
-
|
|
735
|
-
// custody + keccak, and the on-chain renderer will show its placeholder for the image.
|
|
803
|
+
// On-chain mode can inline SVG directly (the renderer emits it as a data: URI).
|
|
804
|
+
let onchainUrlLane = false;
|
|
736
805
|
if (onChain) {
|
|
737
806
|
const text = Buffer.from(bytes).toString('utf8');
|
|
738
807
|
if (looksLikeSvg(text)) {
|
|
@@ -741,7 +810,18 @@ walletAddr) {
|
|
|
741
810
|
contentNote: `content: ${basename(path)} (${bytes.length} bytes SVG) INLINE on-chain`,
|
|
742
811
|
};
|
|
743
812
|
}
|
|
744
|
-
|
|
813
|
+
// A raster can't be inlined in v1. That used to mean "fall through to keccak custody and let the
|
|
814
|
+
// renderer show a placeholder" — which quietly made the documented pattern-2 recipe (image
|
|
815
|
+
// off-chain, JSON on-chain, no server) impossible for a 1/1 even though `deploy-series` did it
|
|
816
|
+
// fine. Now the 1/1 takes the same route the Series takes: upload to a durable, publicly-readable
|
|
817
|
+
// backend and bake the resulting URL on-chain as the image field, so `tokenURI` resolves from the
|
|
818
|
+
// chain to a real image with nothing to keep running. Only a backend that can hand out a public
|
|
819
|
+
// read URL qualifies; `fs` (this machine only) still falls through to custody.
|
|
820
|
+
onchainUrlLane = DIRECT_URL_BACKENDS.has(backendResolution(overrides).backend);
|
|
821
|
+
if (!onchainUrlLane) {
|
|
822
|
+
warn(`--onchain-uri can't inline ${contentType} on-chain (v1 inlines SVG only), and '${backendResolution(overrides).backend}' can't serve a public URL — ` +
|
|
823
|
+
`storing in custody, so the on-chain image will be a placeholder. Add ${bold('--backend arweave')} (or ipfs/cloud) to bake a real image URL, or ${bold('--onchain-image')} to put the bytes themselves on-chain.`);
|
|
824
|
+
}
|
|
745
825
|
}
|
|
746
826
|
const hash = hashContent(bytes);
|
|
747
827
|
const opts = storageOptions(overrides); // flags override config/env
|
|
@@ -750,6 +830,13 @@ walletAddr) {
|
|
|
750
830
|
opts.arweave = { ...opts.arweave, remoteEth, jwk: undefined, ethSignerKey: undefined };
|
|
751
831
|
noteArweavePlan(opts, bytes.length); // free-vs-credit readout (Turbo) — shown for dry-run too
|
|
752
832
|
if (!store) {
|
|
833
|
+
const backendId = backendResolution(overrides).backend;
|
|
834
|
+
if (onchainUrlLane) {
|
|
835
|
+
return {
|
|
836
|
+
tokenFields: [imageUrlField(`<${backendId}-url>`)],
|
|
837
|
+
contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) → would upload to ${backendId}; its public URL becomes the ON-CHAIN image field (no server)`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
753
840
|
return {
|
|
754
841
|
tokenFields: [imageKeccakField(hash)],
|
|
755
842
|
contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) · image keccak256 ${hash} (would be stored on deploy)`,
|
|
@@ -761,6 +848,26 @@ walletAddr) {
|
|
|
761
848
|
if (!opts.arweave?.remoteEth)
|
|
762
849
|
await assertTurboFundsForUpload(opts, [bytes.length], walletAddr);
|
|
763
850
|
const backend = resolveBackend(opts);
|
|
851
|
+
if (onchainUrlLane) {
|
|
852
|
+
// One file, uploaded as a one-entry directory — the same call `deploy-series` uses for its
|
|
853
|
+
// uniform-extension folder, so both commands produce the same URL shape from the same code path.
|
|
854
|
+
// Named `0.<ext>` because a 1/1 is token 0. Falls back to custody if the backend turns out not to
|
|
855
|
+
// support directories, rather than silently producing a token whose image never resolves.
|
|
856
|
+
const ext = extname(path).toLowerCase() || '.bin';
|
|
857
|
+
if (backend.putDirectory) {
|
|
858
|
+
const { base } = await backend.putDirectory([{ name: `0${ext}`, bytes, contentType }]);
|
|
859
|
+
const url = `${base}/0${ext}`;
|
|
860
|
+
// Keep the bytes under their hash too: `abx verify` can then still prove the served image
|
|
861
|
+
// matches what was uploaded, even though the on-chain field addresses it by URL.
|
|
862
|
+
await backend.put(hash, { bytes, contentType });
|
|
863
|
+
ok(`image → ${url} ${dim(`(${backend.id}; baked on-chain, no server)`)}`);
|
|
864
|
+
return {
|
|
865
|
+
tokenFields: [imageUrlField(url)],
|
|
866
|
+
contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) on ${backend.id} · ON-CHAIN image url ${url}`,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
warn(`'${backend.id}' cannot upload a directory, so no public image URL could be baked — falling back to custody (the on-chain image will be a placeholder).`);
|
|
870
|
+
}
|
|
764
871
|
await backend.put(hash, { bytes, contentType });
|
|
765
872
|
return {
|
|
766
873
|
tokenFields: [imageKeccakField(hash)],
|
|
@@ -1222,6 +1329,23 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1222
1329
|
if (!hasPublicUrl)
|
|
1223
1330
|
info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
|
|
1224
1331
|
}
|
|
1332
|
+
// `--onchain-uri` puts the tokenURI JSON on-chain; it does NOT put the IMAGE on-chain. Without
|
|
1333
|
+
// `--onchain-image` the image is committed as a keccak256 anchor, which the on-chain renderer
|
|
1334
|
+
// cannot serve bytes for — so `tokenURI` returns a PLACEHOLDER image. Say that here, before the
|
|
1335
|
+
// spend (this runs in `--dry-run` too), because the success banner used to claim "fully on-chain,
|
|
1336
|
+
// no hosting needed" for exactly this config and a creator would believe they had permanence they
|
|
1337
|
+
// don't. The code lane's dry run already warns about this case; the 1/1 lane shipped it silently.
|
|
1338
|
+
if (onChainUri && !imageResolvesWithoutServer(flags, onchainImage)) {
|
|
1339
|
+
warn(`${bold('--onchain-uri puts the metadata JSON on-chain, not the image.')} With '${backendResolution(storageOverrides(flags)).backend}' custody the image is only ` +
|
|
1340
|
+
`a keccak256 anchor, so ${bold('tokenURI')} will serve a PLACEHOLDER — nothing can fetch the bytes.`);
|
|
1341
|
+
info(`two ways to get a real image with no server: ${bold('--backend arweave')} (or ipfs/cloud) uploads it and bakes the URL on-chain, or ${bold('--onchain-image --compress fastlz')} puts the bytes themselves on-chain (best under ~24KB).`);
|
|
1342
|
+
}
|
|
1343
|
+
else if (onChainUri && !imageEndsUpOnChain(flags, onchainImage)) {
|
|
1344
|
+
// Resolves, durably, with no server — but the IMAGE is not on-chain, and that distinction is
|
|
1345
|
+
// exactly what a creator asking for permanence is buying. Say which one they are getting.
|
|
1346
|
+
info(`${bold('image off-chain, JSON on-chain')} — the image uploads to ${bold(backendResolution(storageOverrides(flags)).backend)} and its URL is baked into the on-chain metadata. ` +
|
|
1347
|
+
`No server to run; permanence is the backend's (Arweave is pay-once-forever, IPFS lasts as long as it stays pinned). For the image itself on-chain, use ${bold('--onchain-image')}.`);
|
|
1348
|
+
}
|
|
1225
1349
|
// IPFS/Arweave custody serves the image from the gateway URL. A LOCAL gateway produces an image
|
|
1226
1350
|
// URL only this machine can load — the image equivalent of the localhost-tokenURI footgun above.
|
|
1227
1351
|
if (!onChainUri && !onchainImage) {
|
|
@@ -1524,7 +1648,23 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1524
1648
|
if (!serveAfter) {
|
|
1525
1649
|
if (onChainUri) {
|
|
1526
1650
|
// Fully on-chain → it self-resolves. Prove it from the chain, no server/localhost.
|
|
1527
|
-
|
|
1651
|
+
// BUT only claim "fully on-chain" when the IMAGE is on-chain too. With `--onchain-uri` alone
|
|
1652
|
+
// the image is a keccak256 anchor the renderer can't serve, so tokenURI yields a placeholder —
|
|
1653
|
+
// and this banner previously said "no server or hosting needed" over exactly that, which is a
|
|
1654
|
+
// promise of permanence the token does not have. Truthful, per-config wording instead.
|
|
1655
|
+
if (imageEndsUpOnChain(flags, onchainImage)) {
|
|
1656
|
+
console.log(`\n${g('Done — fully on-chain.')} ${dim('The token self-resolves; no server or hosting needed.')}`);
|
|
1657
|
+
}
|
|
1658
|
+
else if (imageResolvesWithoutServer(flags, onchainImage)) {
|
|
1659
|
+
const bk = backendResolution(storageOverrides(flags)).backend;
|
|
1660
|
+
console.log(`\n${g('Done — metadata on-chain, image on ' + bk + '.')} ${dim('No server to run: the on-chain JSON points straight at the image.')}`);
|
|
1661
|
+
console.log(` ${dim(`the image is addressed by URL rather than stored on-chain — its permanence is ${bk}'s (Arweave: paid once, kept forever · IPFS: kept while pinned).`)}`);
|
|
1662
|
+
}
|
|
1663
|
+
else {
|
|
1664
|
+
console.log(`\n${g('Done — metadata on-chain.')} ${dim('tokenURI/contractURI resolve from the chain with no server.')}`);
|
|
1665
|
+
console.log(` ${c.orange}The IMAGE is not on-chain${c.reset} ${dim('— it is a keccak256 anchor, so tokenURI serves a placeholder image.')}`);
|
|
1666
|
+
console.log(` ${dim(`prove it: ${bold(`abx tokenuri ${clone}`)} reports the image field's provenance. To fix it, redeploy with ${bold('--backend arweave')} (URL baked on-chain) or ${bold('--onchain-image')} (bytes on-chain).`)}`);
|
|
1667
|
+
}
|
|
1528
1668
|
console.log(` ${bold(`abx tokenuri ${clone}`)} ${dim('# read tokenURI(0) straight from the contract + decode the JSON')}`);
|
|
1529
1669
|
if (noMint)
|
|
1530
1670
|
console.log(` ${bold(`abx mint ${clone}`)} ${dim('# issue token #0')}`);
|
|
@@ -2799,14 +2939,25 @@ async function cmdPreview(flags) {
|
|
|
2799
2939
|
for (const s of shots) {
|
|
2800
2940
|
const t = s.traits && Object.keys(s.traits).length
|
|
2801
2941
|
? Object.entries(s.traits).map(([k, v]) => `${k} ${String(v)}`).join(' · ')
|
|
2802
|
-
:
|
|
2803
|
-
|
|
2942
|
+
: s.timedOut
|
|
2943
|
+
? `${c.orange}timed out — traits unknown${c.reset}`
|
|
2944
|
+
: `${c.orange}no traits reported${c.reset}`;
|
|
2945
|
+
console.log(` ${dim(s.seed.slice(0, 10) + '…')} ${t}${s.done || s.timedOut ? '' : dim(' (no abx.done())')}`);
|
|
2804
2946
|
}
|
|
2947
|
+
const timedOut = shots.filter((s) => s.timedOut).length;
|
|
2805
2948
|
const silent = shots.filter((s) => !s.traits || !Object.keys(s.traits).length).length;
|
|
2806
|
-
|
|
2949
|
+
// A timeout is a measurement failure, not a finding about the program — so it must never be
|
|
2950
|
+
// reported as one. The old code printed the "NO frame reported traits" alarm whenever traits
|
|
2951
|
+
// came back empty for any reason, which condemned a CORRECT program on a loaded machine and
|
|
2952
|
+
// inverted the one line agents are told to trust.
|
|
2953
|
+
if (timedOut) {
|
|
2954
|
+
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 — ` +
|
|
2955
|
+
`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.`);
|
|
2956
|
+
}
|
|
2957
|
+
if (silent === shots.length && !timedOut) {
|
|
2807
2958
|
warn('NO frame reported traits — `abx.traits({…})` is the only thing that becomes marketplace `attributes`. Verify with `abx inspect`.');
|
|
2808
2959
|
}
|
|
2809
|
-
else if (shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
|
|
2960
|
+
else if (silent < shots.length && shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
|
|
2810
2961
|
// Only meaningful when traits DID come back: identical values across seeds is the signature
|
|
2811
2962
|
// of a sketch that never reads `abx.tokenData.seed` (prototyped on Math.random()), which
|
|
2812
2963
|
// deploys as N visually identical tokens. Skipped when nothing reported at all — the
|
|
@@ -3613,8 +3764,15 @@ async function cmdDeployCode(flags) {
|
|
|
3613
3764
|
// has nothing to run — point at the from-chain check instead.
|
|
3614
3765
|
if (hasImageRenderer && !hasProgram)
|
|
3615
3766
|
info(` ${g('nothing to run')} — verify from chain: ${bold('abx tokenuri ' + (predicted ?? '<address>'))} ${dim('(decodes name + on-chain SVG + traits)')}`);
|
|
3616
|
-
else
|
|
3617
|
-
|
|
3767
|
+
else {
|
|
3768
|
+
const remoteRender = !(onChainUri && hasImageBase);
|
|
3769
|
+
info(` ${dim('stand up the runner:')} ${bold('abx deploy-effects --resolver-url ' + baseUrl)} · one-shot: ${bold('abx render ' + (predicted ?? '<address>') + (remoteRender ? ' --remote ' + baseUrl : ''))} · verify: ${bold('abx verify ' + (predicted ?? '<address>'))}`);
|
|
3770
|
+
// Rendering against a resolver you don't share a disk with means YOU hold the bytes and it
|
|
3771
|
+
// holds the URL — so say now which backend that needs, rather than letting the render fail.
|
|
3772
|
+
if (remoteRender && !resolveBackend(storageOptions(storageOverrides(flags))).locator) {
|
|
3773
|
+
info(` ${dim('that render publishes a URL, so it needs a backend that can name one:')} ${bold('--backend cloud')} ${dim('(S3/R2 + public base) ·')} ${bold('ipfs')} ${dim('·')} ${bold('arweave')} ${dim('— or render co-located with the resolver (`abx effects` on its host).')}`);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3618
3776
|
if (dirUpload)
|
|
3619
3777
|
await noteStorageReadiness(storageOptions(storageOverrides(flags)), dirUpload.sizes);
|
|
3620
3778
|
if (deployer && salt && !parseSaltFlag(flags.salt)) {
|
|
@@ -3675,7 +3833,7 @@ async function cmdDeployCode(flags) {
|
|
|
3675
3833
|
});
|
|
3676
3834
|
const setupCalls = (owner) => [
|
|
3677
3835
|
...scriptChunks.map((chunk, i) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] })),
|
|
3678
|
-
...schemas.map(({ key, paramType, auth, min, max, selectOptions }) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth,
|
|
3836
|
+
...schemas.map(({ key, paramType, auth, authAddress, lockAfter, min, max, selectOptions }) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth, authAddress, lockAfter, min, max, selectOptions] })),
|
|
3679
3837
|
...dependencySetupCalls(deps, depRegistry), // setDependency legs in index order (+ the soft registry pointer)
|
|
3680
3838
|
...(onchainUriLegs?.calls ?? []), // program lane only: animation_url field · params.keys · the URI renderers (before the mints)
|
|
3681
3839
|
].concat(Array.from({ length: setupMintCount }, () => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'mint', args: [owner] }))); // folded mints ride init, not here
|
|
@@ -3686,7 +3844,7 @@ async function cmdDeployCode(flags) {
|
|
|
3686
3844
|
const calls = setupCalls(owner);
|
|
3687
3845
|
const txs = [prepareDeploySeriesCode({ factory, params: initParamsFor(owner), salt, chainId: cid, clone })];
|
|
3688
3846
|
if (calls.length)
|
|
3689
|
-
txs.push(prepareCodeSetup({ contract: clone, calls, chainId: cid, chunkCount: scriptChunks.length, schemaKeys: schemas.map((x) => x.key), deps: deps.map((d) => d.display), onchainUri: onChainUri && hasProgram }));
|
|
3847
|
+
txs.push(prepareCodeSetup({ contract: clone, calls, chainId: cid, chunkCount: scriptChunks.length, chunkBytes: scriptChunks.map((h) => (h.length - 2) / 2), schemaKeys: schemas.map((x) => x.key), deps: deps.map((d) => d.display), onchainUri: onChainUri && hasProgram }));
|
|
3690
3848
|
return { clone, txs };
|
|
3691
3849
|
};
|
|
3692
3850
|
let clone;
|
|
@@ -3738,8 +3896,19 @@ async function cmdDeployCode(flags) {
|
|
|
3738
3896
|
// the mempool rejects the duplicate as "replacement transaction underpriced" and the setup leg is
|
|
3739
3897
|
// lost (leaving a deployed-but-unconfigured clone). Fetch once, then increment per send.
|
|
3740
3898
|
let nonce = await publicClient.getTransactionCount({ address: account.address, blockTag: 'pending' });
|
|
3741
|
-
for (const tx of txs) {
|
|
3742
|
-
|
|
3899
|
+
for (const [i, tx] of txs.entries()) {
|
|
3900
|
+
// The SAME read-after-write lag the nonce is pinned for, one field over. Every tx after the
|
|
3901
|
+
// first targets the clone the FIRST tx created; if the node serving `eth_estimateGas` hasn't
|
|
3902
|
+
// seen that block yet, the target looks codeless and the estimate comes back as the CALLDATA
|
|
3903
|
+
// cost alone (~200k). That cannot fund the CREATE inside `SSTORE2.write` (~200 gas per stored
|
|
3904
|
+
// byte), so the CREATE returns 0 and the tx reverts `DeploymentFailed()` — which is what a
|
|
3905
|
+
// reporter hit on Base Sepolia on every `deploy-code` attempt, in both lanes: 201,616 sent
|
|
3906
|
+
// where 941,331 was needed. Wait for the code to be visible, then PIN the gas ourselves
|
|
3907
|
+
// (never re-read at send time), flooring it at what the payload provably costs.
|
|
3908
|
+
if (i > 0 && tx.to)
|
|
3909
|
+
await waitForCodeAt(publicClient, tx.to);
|
|
3910
|
+
const gas = await pinGas(publicClient, { from: account.address, to: tx.to, data: tx.data, gasFloor: tx.gasFloor });
|
|
3911
|
+
const hash = await wallet.sendTransaction({ to: tx.to, data: tx.data, account, chain: wallet.chain, nonce, gas });
|
|
3743
3912
|
nonce += 1;
|
|
3744
3913
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
3745
3914
|
if (receipt.status !== 'success')
|
|
@@ -3892,6 +4061,10 @@ async function cmdRender(address, tokenIds, flags) {
|
|
|
3892
4061
|
const remote = remoteFlag(flags);
|
|
3893
4062
|
const resolverUrl = (remote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
|
|
3894
4063
|
const adminToken = remote ? requireRemoteToken(remote) : undefined;
|
|
4064
|
+
// Refuse the combination that can't work before launching Chromium (a laptop render against a
|
|
4065
|
+
// hosted resolver, on a backend with no public URL) — the render would end in a 400 either way.
|
|
4066
|
+
if (remote)
|
|
4067
|
+
requirePublishableBackend(flags, 'abx render --remote');
|
|
3895
4068
|
// Co-located (no --remote): record each declared output into the shared store's artifact
|
|
3896
4069
|
// registry so the local resolver's `artifacts` manifest enumerates it. Remote: the publish
|
|
3897
4070
|
// lane (adminToken) records rows on the hosted resolver instead.
|
|
@@ -3916,6 +4089,41 @@ async function cmdRender(address, tokenIds, flags) {
|
|
|
3916
4089
|
else
|
|
3917
4090
|
ok(summary);
|
|
3918
4091
|
}
|
|
4092
|
+
/**
|
|
4093
|
+
* The publish topology's one hard prerequisite, checked BEFORE any capture.
|
|
4094
|
+
*
|
|
4095
|
+
* A resolver that doesn't share this machine's disk serves referenced output (the still, a video, a
|
|
4096
|
+
* model) by **redirect** — so it needs a URL, and it refuses the bytes (`specs/protocol/effects.md →
|
|
4097
|
+
* Bound vs referenced`). A backend that can't name a locator therefore has no publish lane at all,
|
|
4098
|
+
* and every render against one would be work spent to earn a 400.
|
|
4099
|
+
*
|
|
4100
|
+
* So this refuses up front and names the ways out, rather than warning and letting the render run.
|
|
4101
|
+
* It deliberately does NOT pick a backend for the operator: which store holds their art — and which
|
|
4102
|
+
* gateway serves it — is theirs to decide. The three that work are peers, not a ranking: `https://`
|
|
4103
|
+
* from S3/R2, an IPFS gateway, and Arweave are all fine, because derived output is re-creatable and
|
|
4104
|
+
* the only real requirement is that a third party can fetch it.
|
|
4105
|
+
*/
|
|
4106
|
+
function requirePublishableBackend(flags, what) {
|
|
4107
|
+
const backend = resolveBackend(storageOptions(storageOverrides(flags)));
|
|
4108
|
+
// `cloud` is the one backend that HAS a locator method and can still return null: the URL is
|
|
4109
|
+
// `<publicBase>/<key>`, so without a public base it can name nothing. Catch that here too — it
|
|
4110
|
+
// would otherwise be the same failure one render later.
|
|
4111
|
+
if (backend.id === 'cloud' && !backend.publicBase) {
|
|
4112
|
+
throw new Error(`${what} hands the resolver a URL for each render, but the 'cloud' backend has no public read base — ` +
|
|
4113
|
+
`set ${bold('ABX_S3_PUBLIC_BASE')} to the bucket's public URL (an R2/CloudFront/S3-website base), ` +
|
|
4114
|
+
`or use ${bold('--backend ipfs')} / ${bold('--backend arweave')}, or render co-located with the resolver.`);
|
|
4115
|
+
}
|
|
4116
|
+
if (backend.locator)
|
|
4117
|
+
return;
|
|
4118
|
+
const { backend: name, source } = backendResolution(storageOverrides(flags));
|
|
4119
|
+
throw new Error(`${what} publishes each render to a resolver that doesn't share this machine's disk, but the '${name}' backend ` +
|
|
4120
|
+
`(${source === 'default' ? 'the default' : `from ${source}`}) can't produce a URL for what it stores — so the resolver would have nothing to serve.\n` +
|
|
4121
|
+
` Point it at a backend that can (equal options — pick on cost/ops, not durability dogma):\n` +
|
|
4122
|
+
` ${bold('--backend cloud')} S3 / R2 / B2 with a public base (ABX_S3_* + ABX_S3_PUBLIC_BASE)\n` +
|
|
4123
|
+
` ${bold('--backend ipfs')} Pinata or your own Kubo + a public gateway (PINATA_JWT / ABX_IPFS_*)\n` +
|
|
4124
|
+
` ${bold('--backend arweave')} pay-once permanent (uploads under 100KiB are free, no setup)\n` +
|
|
4125
|
+
` …or run co-located instead: ${bold('abx effects')} on the same host as ${bold('abx serve')}, sharing one backend and no remote token.`);
|
|
4126
|
+
}
|
|
3919
4127
|
/**
|
|
3920
4128
|
* `abx effects` — run the reference effect runner LOCALLY, in-process (the local counterpart to
|
|
3921
4129
|
* `abx deploy-effects`, which scaffolds a HOSTED runner). This is what makes a param change / new
|
|
@@ -3935,6 +4143,8 @@ async function cmdEffects(flags) {
|
|
|
3935
4143
|
// the shared artifact registry so the resolver's `artifacts` manifest enumerates it. With an
|
|
3936
4144
|
// admin token, the publish lane records rows on the hosted resolver instead.
|
|
3937
4145
|
const localStore = adminToken ? null : new SqliteStore();
|
|
4146
|
+
if (adminToken)
|
|
4147
|
+
requirePublishableBackend(flags, 'abx effects against a remote resolver');
|
|
3938
4148
|
const { EffectRunner, renderEffect } = await loadEffects();
|
|
3939
4149
|
const runner = new EffectRunner({
|
|
3940
4150
|
resolverUrl,
|
|
@@ -4795,6 +5005,36 @@ async function cmdState(address, flags) {
|
|
|
4795
5005
|
if (royalty)
|
|
4796
5006
|
info(`royalty ${Number(royalty[1]) / 100}% → ${royalty[0]}`);
|
|
4797
5007
|
info(`renderer ${renderer && renderer !== zeroAddress ? `on-chain (${renderer})` : dim('off-chain (stored URI base / override)')}`);
|
|
5008
|
+
// The governed PostParam surface. There is no on-chain enumeration of schema keys, so the source
|
|
5009
|
+
// is the project's own `params.keys` list — which is also what the on-chain generator reads, so
|
|
5010
|
+
// anything missing from it is invisible to renders anyway. Read-only, best-effort, and skipped
|
|
5011
|
+
// entirely for a project that has none (a 1/1, or a code project with no params).
|
|
5012
|
+
try {
|
|
5013
|
+
const csv = await readParamsKeys(publicClient, address);
|
|
5014
|
+
const keys = (csv ?? '').split(',').map((k) => k.trim()).filter((k) => k && k !== 'params.keys');
|
|
5015
|
+
if (keys.length) {
|
|
5016
|
+
const rows = await Promise.all(keys.map(async (key) => ({ key, s: await readParamSchema(publicClient, address, key) })));
|
|
5017
|
+
const governed = rows.filter((r) => r.s.exists);
|
|
5018
|
+
const now = Math.floor(Date.now() / 1000);
|
|
5019
|
+
if (governed.length) {
|
|
5020
|
+
console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/artist set these; the generator injects them`)}`);
|
|
5021
|
+
for (const { key, s } of governed) {
|
|
5022
|
+
const retired = s.lockAfter !== 0 && s.lockAfter < now;
|
|
5023
|
+
const locks = s.lockAfter !== 0 && !retired ? dim(` locks ${new Date(s.lockAfter * 1000).toISOString().slice(0, 10)}`) : '';
|
|
5024
|
+
const tag = retired ? ` ${c.orange}retired${c.reset}${dim(' — writes revert')}` : locks;
|
|
5025
|
+
console.log(` ${describeSchema({ key, paramType: s.paramType, auth: s.auth, authAddress: s.authAddress, lockAfter: 0, min: s.min, max: s.max, selectOptions: s.selectOptions })}${tag}`);
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
// Listed but ungoverned: the generator will inject the key with whatever raw value is set (or
|
|
5029
|
+
// nothing). Not an error — say it plainly rather than leaving a silent discrepancy.
|
|
5030
|
+
const ungoverned = rows.filter((r) => !r.s.exists).map((r) => r.key);
|
|
5031
|
+
if (ungoverned.length)
|
|
5032
|
+
info(`${dim('also in params.keys, no schema:')} ${ungoverned.join(', ')}`);
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
catch {
|
|
5036
|
+
/* best-effort — a non-code project has no params surface to read */
|
|
5037
|
+
}
|
|
4798
5038
|
console.log('');
|
|
4799
5039
|
}
|
|
4800
5040
|
/**
|
|
@@ -5320,10 +5560,16 @@ async function cmdDoctor(flags) {
|
|
|
5320
5560
|
try {
|
|
5321
5561
|
const backend = resolveBackend(storageOptions());
|
|
5322
5562
|
const h = await backend.health?.();
|
|
5563
|
+
// Whether this backend can name a public URL for what it stores decides one thing operators hit
|
|
5564
|
+
// later: a REMOTE effects runner needs it (the resolver serves referenced output by redirect, so
|
|
5565
|
+
// it takes a URL and refuses bytes). Co-located rendering doesn't care — hence a note, not a fail.
|
|
5566
|
+
const publishable = backend.locator
|
|
5567
|
+
? 'can publish to a remote resolver'
|
|
5568
|
+
: 'local-only — a remote effects runner needs cloud/ipfs/arweave (co-located rendering is fine)';
|
|
5323
5569
|
if (h)
|
|
5324
|
-
check('storage', h.ok, `${backend.id} · ${h.detail ?? ''}`);
|
|
5570
|
+
check('storage', h.ok, `${backend.id} · ${h.detail ?? ''} · ${publishable}`);
|
|
5325
5571
|
else
|
|
5326
|
-
check('storage', true, `${backend.id} · configured`);
|
|
5572
|
+
check('storage', true, `${backend.id} · configured · ${publishable}`);
|
|
5327
5573
|
}
|
|
5328
5574
|
catch (err) {
|
|
5329
5575
|
check('storage', false, `${backendId} · ${err.message}`);
|
|
@@ -5532,13 +5778,19 @@ async function cmdDeployEffects(flags) {
|
|
|
5532
5778
|
const intervalMs = flags['interval-ms'] ? Number(flags['interval-ms']) : undefined;
|
|
5533
5779
|
const environmentId = flags['env-id'] ?? process.env.ABX_ENVIRONMENT_ID;
|
|
5534
5780
|
const storageBackend = process.env.ABX_STORAGE_BACKEND;
|
|
5535
|
-
// A HOSTED runner
|
|
5536
|
-
// (
|
|
5537
|
-
// which
|
|
5538
|
-
//
|
|
5781
|
+
// A HOSTED runner holds its own render bytes and hands the resolver a URL — that's the whole
|
|
5782
|
+
// topology (`specs/protocol/effects.md → Bound vs referenced`). The default `fs` writes to the
|
|
5783
|
+
// runner CONTAINER's disk, which nothing else can reach, so there is no URL to publish: the runner
|
|
5784
|
+
// now REFUSES to start on that config. Scaffolding it anyway would just deploy a container that
|
|
5785
|
+
// exits, so this is a hard stop rather than the warning it used to be.
|
|
5539
5786
|
if (!storageBackend || storageBackend === 'fs') {
|
|
5540
|
-
|
|
5541
|
-
`
|
|
5787
|
+
throw new Error(`ABX_STORAGE_BACKEND is ${storageBackend ? "'fs'" : 'unset (defaults to fs)'} — a hosted runner can't serve renders off its own container disk, ` +
|
|
5788
|
+
`and it refuses to start without a backend that can name a public URL for what it stores.\n` +
|
|
5789
|
+
` Set ABX_STORAGE_BACKEND to one of (equal options — pick on cost/ops):\n` +
|
|
5790
|
+
` ${bold('cloud')} (alias s3) S3 / R2 / B2 with ABX_S3_* + a public base\n` +
|
|
5791
|
+
` ${bold('ipfs')} Pinata or your own Kubo + a public gateway\n` +
|
|
5792
|
+
` ${bold('arweave')} pay-once permanent (<100KiB uploads are free)\n` +
|
|
5793
|
+
` …then re-run. (Rendering on the resolver's own host instead? Use ${bold('abx effects')} co-located — no publish lane, no locator needed.)`);
|
|
5542
5794
|
}
|
|
5543
5795
|
const repoRoot = findRepoRoot();
|
|
5544
5796
|
if (!repoRoot) {
|
|
@@ -5560,10 +5812,7 @@ async function cmdDeployEffects(flags) {
|
|
|
5560
5812
|
const outDir = resolvePath(joinPath(dir, 'effects'));
|
|
5561
5813
|
console.log(bold(`\n ABX Self-Host Toolkit — provision effects runner (fly)`));
|
|
5562
5814
|
info(`resolver: ${bold(cleanResolver)} ${dim('(reads token state + publishes renders here)')}`);
|
|
5563
|
-
info(`storage home: ${bold(storageBackend
|
|
5564
|
-
if (!storageBackend || storageBackend === 'fs') {
|
|
5565
|
-
warn('storage backend is fs (LOCAL) — a HOSTED runner needs a PUBLIC home (ipfs/arweave/s3) so the resolver can serve the render. Set ABX_STORAGE_BACKEND before deploying, or the runner will push raw bytes to the resolver.');
|
|
5566
|
-
}
|
|
5815
|
+
info(`storage home: ${bold(storageBackend)} ${dim('— the runner HOLDS the render bytes here and publishes their URL; the resolver redirects to it and never proxies')}`);
|
|
5567
5816
|
step('Write the effects artifact (self-contained — Dockerfile.effects + fly.toml + vendored source)');
|
|
5568
5817
|
mkdirSync(outDir, { recursive: true });
|
|
5569
5818
|
for (const f of art.files) {
|
|
@@ -5772,7 +6021,10 @@ const COMMAND_HELP = {
|
|
|
5772
6021
|
(no --public-base-url needed); the ${g('params.keys')} contract param is written automatically from the schema
|
|
5773
6022
|
flags. The marketplace still is rendered off-chain by the effect runner ${bold('for a JS program')} (--script/--code-dir) — but with ${g('--image-renderer')} the image is computed ON-CHAIN too (nothing to render). Verify with ${g('abx verify <addr>')}.
|
|
5774
6023
|
(--generator 0x.. / --renderer 0x.. override the canonical singletons; ABX_GENERATOR / ABX_RENDERER via env.)
|
|
5775
|
-
--schema key:Type:Auth,… governed PostParams (e.g. ${g('palette:HexColor:TokenOwner')}) —
|
|
6024
|
+
--schema key:Type:Auth,… governed PostParams (e.g. ${g('palette:HexColor:TokenOwner')}) — set values later with ${g('abx configure-param')},
|
|
6025
|
+
change the SCHEMA later with ${g('abx set-schema')}. An Address leg names its writer inline —
|
|
6026
|
+
${g('key:Type:Address(0x…)')} (a CONTRACT may hold it: the controller pattern for open participation).
|
|
6027
|
+
Append ${g(':lock=<when>')} (ISO date, unix seconds, or ${g('now')}) to freeze the value after that time.
|
|
5776
6028
|
${dim('repeatable OR comma-separable (like --dep): ')}${g('--schema a --schema b')}${dim(' ≡ ')}${g('--schema a,b')}
|
|
5777
6029
|
Type ∈ Bool·Select·Uint256Range·Int256Range·DecimalRange·HexColor·Timestamp·String·Bytes; Auth ∈ Artist·TokenOwner·Address (+ Or-combos).
|
|
5778
6030
|
${bold('a Select declares its options')}: ${g('mood:Select[Calm|Wild|Chaotic]:TokenOwner')}; ${bold('a Range takes bounds')}: ${g('density:Uint256Range[0..100]:TokenOwner')} (Int/Decimal/Timestamp too; bounds optional)
|
|
@@ -5797,6 +6049,7 @@ const COMMAND_HELP = {
|
|
|
5797
6049
|
--backend ipfs|arweave directory-mode custody for the build upload ${g('--unpaused')} · --minter 0x.. · --primary-payee 0x..
|
|
5798
6050
|
${dim('a fixed-price PRIMARY SALE is set up AFTER deploy, not by these flags: ')}${g('abx minter configure <addr> --price <eth>')}${dim(' → ')}${g('set-minter')}${dim(' → ')}${g('set-primary-payee')}${dim(' → ')}${g('unpause')}${dim(' (see `abx minter --help`). --minter/--primary-payee here only pre-authorize an already-known minter.')}
|
|
5799
6051
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} ${dim('(needs --for 0x..)')} ${g('--bootstrap-factory')} ${dim('(private chains/sandboxes only)')}
|
|
6052
|
+
${g('--yes')} accept inferred --name/--symbol defaults (the real send otherwise REFUSES to bake placeholders)
|
|
5800
6053
|
${g('--dry-run')} preview (address · resolver base · schema · mint plan · tx count) — nothing sent, no bytes pinned
|
|
5801
6054
|
${g('--confirm')} interactive y/N gate before the real send
|
|
5802
6055
|
${bold('After deploy — render the marketplace still')} ${dim('(JS lanes only — --script/--code-dir; with --image-renderer the image is on-chain, nothing to render)')}:
|
|
@@ -5807,8 +6060,12 @@ const COMMAND_HELP = {
|
|
|
5807
6060
|
No tokenId → sweeps all minted tokens; pass ids (${g('0 1 2')}) to target specific tokens.
|
|
5808
6061
|
${g('--force')} RE-RENDER even when the still already exists — the fix for a bad / blank / timed-out capture
|
|
5809
6062
|
(the art is otherwise deterministic, so a plain render idempotent-skips an existing still). Overwrites it (+ republishes on --remote).
|
|
5810
|
-
${g('--remote [name|url]')}
|
|
5811
|
-
|
|
6063
|
+
${g('--remote [name|url]')} register each render with a REMOTE resolver: the bytes go to ${bold('ABX_STORAGE_BACKEND')} (YOU hold them), and
|
|
6064
|
+
POST /v1/effect-artifacts hands the resolver the URL — it redirects there and never proxies. Traits are the
|
|
6065
|
+
exception: they stitch into the token JSON, so their content (≤64KB) goes to the resolver itself.
|
|
6066
|
+
${bold('Requires a backend that can name a public URL')} — ${g('--backend cloud')} (S3/R2 + public base), ${g('ipfs')}, or ${g('arweave')};
|
|
6067
|
+
equal options, pick on cost/ops. The default ${g('fs')} is refused up front rather than after the render.
|
|
6068
|
+
Idempotent; a re-run re-registers rows on a resolver that lost them. Needs its token (a named remote's
|
|
5812
6069
|
${g('ABX_REMOTE_<NAME>_TOKEN')}, else ${g('ABX_RESOLVER_ADMIN_TOKEN')}).
|
|
5813
6070
|
--effects-url <url> enqueue on a running effect-runner service instead of rendering inline (else ${g('ABX_EFFECTS_URL')})
|
|
5814
6071
|
${dim('Inline (no --remote) renders on THIS machine (needs `npx playwright install chromium` once), pointing Chromium at the live')}
|
|
@@ -5825,9 +6082,30 @@ const COMMAND_HELP = {
|
|
|
5825
6082
|
--port <n> HTTP port (default ${g('ABX_EFFECTS_PORT')} / 8788) — ${g('POST /notify')} enqueues (watcher lane) · ${g('POST /run')} sweeps synchronously (command lane)
|
|
5826
6083
|
--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
|
|
5827
6084
|
--concurrency <n> parallel renders while draining (default ${g('ABX_EFFECTS_CONCURRENCY')} / 1 — Chromium is heavy; raise deliberately)
|
|
5828
|
-
${dim('Co-located with a local `abx serve` (same store) → no tokens needed
|
|
5829
|
-
${dim('token
|
|
6085
|
+
${dim('Co-located with a local `abx serve` (same store) → no tokens needed, any backend works (fs included).')}
|
|
6086
|
+
${dim('Against a REMOTE resolver: --remote <name|url> (its token registers renders + reports status), or ABX_RESOLVER_URL +')}
|
|
6087
|
+
${dim('ABX_RESOLVER_ADMIN_TOKEN — and a backend that can name a public URL (cloud/ipfs/arweave), since the resolver takes the')}
|
|
6088
|
+
${dim('URL and refuses the bytes. PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
|
|
5830
6089
|
${dim('To HOST the runner (fly/docker), use `abx deploy-effects`.')}`,
|
|
6090
|
+
'set-schema': `
|
|
6091
|
+
${bold('abx set-schema')} <address> ${g('--schema key:Type:Auth[:lock=<when>]')} ${dim('— attach or replace ONE PostParam schema. Owner-only. Sends a tx.')}
|
|
6092
|
+
A project's param surface is NOT frozen at deploy: ${g('setParamSchema')} is owner-gated with no deploy-time restriction,
|
|
6093
|
+
so you can add a key a piece turned out to need without redeploying (and losing the address, mints, and collectors).
|
|
6094
|
+
${bold('This is a full-row upsert, not a patch.')} Replacing an existing schema rewrites every field — restate anything you
|
|
6095
|
+
want to keep, including an existing ${g('lock=')}. The contract does ${bold('not')} re-validate values already stored under the key,
|
|
6096
|
+
so narrowing a bound, dropping a Select option, or changing the type strands them; that is refused unless you pass
|
|
6097
|
+
${g('--force')}. A key can never be un-governed — to decommission one use ${g('abx retire-param')}.
|
|
6098
|
+
${g('--force')} apply a change that could strand already-stored values
|
|
6099
|
+
${g('--dry-run')} print the exact tx and send nothing`,
|
|
6100
|
+
'retire-param': `
|
|
6101
|
+
${bold('abx retire-param')} <address> <key> ${dim('— permanently stop further writes to a PostParam. Owner-only. Sends a tx.')}
|
|
6102
|
+
Sets the schema's ${g('lockAfter')} into the past, so every later ${g('configure-param')} reverts ${g('ParamLockExpired')}. This is the
|
|
6103
|
+
closest the protocol comes to deleting a parameter, and it is ${bold('irreversible')}.
|
|
6104
|
+
Reads the current schema and changes ONLY the lock, so type/auth/bounds/options are carried forward untouched.
|
|
6105
|
+
What it does NOT do: remove the key (a governed key stays governed forever) or erase a value already stored — that
|
|
6106
|
+
value keeps serving in token data. A value written under a TokenOwner/Address leg came from a collector, and the
|
|
6107
|
+
artist deliberately cannot delete it.
|
|
6108
|
+
${g('--dry-run')} print the exact tx and send nothing`,
|
|
5831
6109
|
'configure-param': `
|
|
5832
6110
|
${bold('abx configure-param')} <address> <tokenId|-> <key> <value> ${dim('— set a PostParam (typed, canonical encode). Sends a tx.')}
|
|
5833
6111
|
Reads the on-chain schema for <key> and canonically encodes <value> (${g('#rrggbb')} for HexColor, fixed-decimal ranges,
|
|
@@ -6142,6 +6420,8 @@ function help() {
|
|
|
6142
6420
|
--dep-registry 0x.. · --description "<s>" · --external-url <url> · ${g('--image-base <url>')} (off-chain thumbnails at a deterministic /{id} URL) ·
|
|
6143
6421
|
${g('--attributes-renderer 0x..')} (traits on-chain) · --max N · ${g('--mint-all')}|${g('--mint-count')} N · --backend ipfs|arweave · ${g('--dry-run')}
|
|
6144
6422
|
${g('abx configure-param')} <addr> <id> <key> <value> set a governed PostParam (typed encode; a change re-addresses the render)
|
|
6423
|
+
${g('abx set-schema')} <addr> --schema key:Type:Auth[:lock=<when>] attach or replace ONE key's schema, any time after deploy
|
|
6424
|
+
${g('abx retire-param')} <addr> <key> stop all further writes to a param, permanently (the closest thing to removing one)
|
|
6145
6425
|
${g('abx set-dependency')} <addr> <index> <ref> declare/replace a code dependency (name@version via the registry pointer, or 0x.. on-chain)
|
|
6146
6426
|
${g('abx remove-last-dependency')} <addr> · ${g('abx set-dependency-registry')} <addr> <0x..|none> · ${g('abx lock-dependencies')} <addr> operate/freeze the set
|
|
6147
6427
|
${g('abx render')} <addr> [id…] render the still + traits ONCE (the repair lane) [${g('--force')} re-render] [${g('--remote')} [url] publish to a hosted resolver]
|
|
@@ -6215,18 +6495,25 @@ function parseSaltFlag(raw) {
|
|
|
6215
6495
|
/** Locate the skill folder: the bundled copy beside the compiled CLI (published), else the
|
|
6216
6496
|
* canonical repo copy (dev). Returns null if neither is present. */
|
|
6217
6497
|
function resolveBundledSkill() {
|
|
6218
|
-
|
|
6219
|
-
const pkgDir = resolvePath(
|
|
6498
|
+
const here = fileURLToPath(import.meta.url);
|
|
6499
|
+
const pkgDir = resolvePath(here, '..', '..');
|
|
6220
6500
|
const bundled = joinPath(pkgDir, 'skill');
|
|
6501
|
+
// Running from SOURCE (`pnpm abx`, tsx on packages/cli/src/**) means we're in the repo working
|
|
6502
|
+
// tree, where the canonical skill is the source of truth and `<pkg>/skill` is gitignored PREPACK
|
|
6503
|
+
// OUTPUT that may be arbitrarily old. Preferring the bundle there is actively destructive: a
|
|
6504
|
+
// leftover `skill/` from a months-old `npm pack` silently overwrote the canonical skill with a
|
|
6505
|
+
// copy 8 versions behind (and the drift check then reported the damage as if the user had caused
|
|
6506
|
+
// it). In the published layout there is no repo and no canonical copy, so the bundle is correct.
|
|
6507
|
+
const fromSource = /[/\\]src[/\\][^/\\]+$/.test(here);
|
|
6508
|
+
const root = findRepoRoot();
|
|
6509
|
+
const canonical = root ? joinPath(root, '.claude', 'skills', 'abx-self-host') : null;
|
|
6510
|
+
if (fromSource && canonical && existsSync(joinPath(canonical, 'SKILL.md')))
|
|
6511
|
+
return canonical;
|
|
6512
|
+
// Published layout: dist/main.js → <pkg>/skill (see scripts/bundle-skill.mjs).
|
|
6221
6513
|
if (existsSync(joinPath(bundled, 'SKILL.md')))
|
|
6222
6514
|
return bundled;
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
if (root) {
|
|
6226
|
-
const canonical = joinPath(root, '.claude', 'skills', 'abx-self-host');
|
|
6227
|
-
if (existsSync(joinPath(canonical, 'SKILL.md')))
|
|
6228
|
-
return canonical;
|
|
6229
|
-
}
|
|
6515
|
+
if (canonical && existsSync(joinPath(canonical, 'SKILL.md')))
|
|
6516
|
+
return canonical;
|
|
6230
6517
|
return null;
|
|
6231
6518
|
}
|
|
6232
6519
|
/** Locate the in-chain renderer Foundry scaffold: bundled beside the CLI (published), else the
|
|
@@ -6298,11 +6585,20 @@ function resolveInstallParents(agentFlag) {
|
|
|
6298
6585
|
return [...parents];
|
|
6299
6586
|
}
|
|
6300
6587
|
/** Copy the bundled skill folder to `dest`, replacing any prior copy so a re-install after an
|
|
6301
|
-
* upgrade never leaves stale reference files behind.
|
|
6588
|
+
* upgrade never leaves stale reference files behind.
|
|
6589
|
+
*
|
|
6590
|
+
* Returns `'already-canonical'` when destination IS the source, having done nothing. That case is
|
|
6591
|
+
* not hypothetical: in a dev checkout of the abx repo `resolveBundledSkill()` resolves to the
|
|
6592
|
+
* canonical `.claude/skills/abx-self-host`, which is exactly where a cwd-relative install writes —
|
|
6593
|
+
* so the `rmSync` below would delete the canonical skill, and the copy would then have no source.
|
|
6594
|
+
* A self-destructing install is a bad way to learn that. */
|
|
6302
6595
|
function installSkillTo(src, dest) {
|
|
6596
|
+
if (resolvePath(src) === resolvePath(dest))
|
|
6597
|
+
return 'already-canonical';
|
|
6303
6598
|
mkdirSync(joinPath(dest, '..'), { recursive: true });
|
|
6304
6599
|
rmSync(dest, { recursive: true, force: true });
|
|
6305
6600
|
cpSync(src, dest, { recursive: true });
|
|
6601
|
+
return 'installed';
|
|
6306
6602
|
}
|
|
6307
6603
|
/**
|
|
6308
6604
|
* Install the bundled skill into the default per-agent parents (or `~` with `global`), reporting
|
|
@@ -6316,9 +6612,10 @@ function installSkillToDefaults(src, opts = {}) {
|
|
|
6316
6612
|
ok(`installed the abx skill v${version}${opts.global ? ' (global, ~)' : ''}:`);
|
|
6317
6613
|
for (const parent of parents) {
|
|
6318
6614
|
const dest = joinPath(base, parent, SKILL_DIR_NAME);
|
|
6319
|
-
installSkillTo(src, dest);
|
|
6615
|
+
const outcome = installSkillTo(src, dest);
|
|
6320
6616
|
const label = SKILL_PARENT_LABELS[parent];
|
|
6321
|
-
|
|
6617
|
+
const note = outcome === 'already-canonical' ? dim(' (already the canonical copy — left as is)') : label ? dim(' → ' + label) : '';
|
|
6618
|
+
console.log(` ${g(joinPath(parent, SKILL_DIR_NAME))}${note}`);
|
|
6322
6619
|
}
|
|
6323
6620
|
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
6324
6621
|
info(`the skill is version-locked to this CLI (v${version}); re-run ${g('abx skill install')} after upgrading so the two stay in sync.`);
|
|
@@ -6346,7 +6643,10 @@ async function cmdSkill(rest, flags) {
|
|
|
6346
6643
|
// whose skills dir we don't special-case, or a bespoke location).
|
|
6347
6644
|
if (typeof flags.target === 'string' && flags.target !== 'true') {
|
|
6348
6645
|
const dest = joinPath(resolvePath(flags.target), SKILL_DIR_NAME);
|
|
6349
|
-
installSkillTo(src, dest)
|
|
6646
|
+
if (installSkillTo(src, dest) === 'already-canonical') {
|
|
6647
|
+
ok(`${dest} is already the canonical skill v${version} — nothing to install.`);
|
|
6648
|
+
return;
|
|
6649
|
+
}
|
|
6350
6650
|
ok(`installed the abx skill v${version} → ${dest}`);
|
|
6351
6651
|
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
6352
6652
|
return;
|