@artblocks/abx-cli 0.1.0-alpha.12 → 0.1.0-alpha.14
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/config.d.ts.map +1 -1
- package/dist/config.js +8 -8
- package/dist/config.js.map +1 -1
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +1 -1
- package/dist/flags.js.map +1 -1
- 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 +418 -75
- package/dist/main.js.map +1 -1
- package/dist/onchain-uri.d.ts +23 -39
- package/dist/onchain-uri.d.ts.map +1 -1
- package/dist/onchain-uri.js +94 -86
- package/dist/onchain-uri.js.map +1 -1
- package/dist/ownerops.d.ts +13 -1
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +268 -73
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +14 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +86 -25
- 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 +29 -39
- package/skill/reference/code-projects.md +42 -5
- package/skill/reference/decisions.md +61 -0
- package/skill/reference/operating.md +1 -1
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 {
|
|
60
|
+
import { expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport, readLegacyParamsKeys, readParamSchemaKeys, readSetParamKeys, } 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
|
-
import { previewConfigFromFlags, previewDepTags, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
64
|
+
import { previewConfigFromFlags, previewDepTags, parsePreviewParams, 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
|
|
@@ -150,6 +174,22 @@ async function main() {
|
|
|
150
174
|
loadDotEnv();
|
|
151
175
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
152
176
|
const flags = parseFlags(rest);
|
|
177
|
+
// `--chain` is the flag people reach for, and it does not exist: chain selection is the ABX_CHAIN
|
|
178
|
+
// env var. It used to be swallowed by the generic stray-flag WARNING and the command then acted on
|
|
179
|
+
// the DEFAULT chain — `abx deploy-code … --chain sepolia` deployed to base-sepolia. On a dry run
|
|
180
|
+
// that costs a confused minute; on a funded send it is a wrong-chain deploy with real artifacts at
|
|
181
|
+
// the wrong address. Prose that was ignored once will be ignored again, so this refuses instead,
|
|
182
|
+
// on EVERY command (a read-only command that quietly ignored it would still teach the wrong model).
|
|
183
|
+
if (flags.chain !== undefined) {
|
|
184
|
+
const asked = typeof flags.chain === 'string' && flags.chain !== 'true' ? flags.chain : '';
|
|
185
|
+
process.stderr.write(`\n${c.red}✗${c.reset} there is no ${bold('--chain')} flag — the chain is chosen by the ${bold('ABX_CHAIN')} environment variable.\n` +
|
|
186
|
+
` active now: ${bold(CHAIN)}${asked && asked !== CHAIN ? ` ${dim(`(you asked for '${asked}')`)}` : ''}\n` +
|
|
187
|
+
` known chains: ${KNOWN_CHAIN_KEYS.join(', ')}\n\n` +
|
|
188
|
+
` run it on ${asked && KNOWN_CHAIN_KEYS.includes(asked) ? asked : '<chain>'} like this:\n` +
|
|
189
|
+
` ${bold(`ABX_CHAIN=${asked && KNOWN_CHAIN_KEYS.includes(asked) ? asked : '<chain>'} abx ${cmd ?? '<command>'} …`)}\n` +
|
|
190
|
+
` ${dim('(the CLI is deliberately stateless — every run resolves its chain from the environment, so nothing a previous command set can leak into this one.)')}\n\n`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
153
193
|
// `version` / --version / -v is a crisp query — print and exit, no update nag around it.
|
|
154
194
|
if (cmd === 'version' || cmd === '--version' || cmd === '-v') {
|
|
155
195
|
console.log(readCliVersion());
|
|
@@ -186,6 +226,8 @@ async function main() {
|
|
|
186
226
|
case 'set-minter': return cmdSetMinter(rest[0], flags);
|
|
187
227
|
case 'set-max-invocations': return cmdSetMaxInvocations(rest[0], flags);
|
|
188
228
|
case 'configure-param': return cmdConfigureParam(rest[0], rest.slice(1), flags);
|
|
229
|
+
case 'set-schema': return cmdSetSchema(rest[0], flags);
|
|
230
|
+
case 'retire-param': return cmdRetireParam(rest[0], rest.slice(1), flags);
|
|
189
231
|
case 'set-param-hooks': return cmdSetParamHooks(rest[0], flags);
|
|
190
232
|
case 'set-dependency': return cmdSetDependency(rest[0], rest.slice(1), flags);
|
|
191
233
|
case 'remove-last-dependency': return cmdRemoveLastDependency(rest[0], flags);
|
|
@@ -662,10 +704,12 @@ async function cmdSetRendererCli(address, flags) {
|
|
|
662
704
|
flags.renderer = await ensureRenderer();
|
|
663
705
|
return cmdSetRenderer(address, flags);
|
|
664
706
|
}
|
|
665
|
-
/** Is the configured renderer the spec version this build expects (
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
707
|
+
/** Is the configured renderer the spec version this build expects (v4 — the computed `abx_params`
|
|
708
|
+
* object, enumerated from chain, plus `animation_url` wrapped as `data:text/html;base64` for the
|
|
709
|
+
* inline/reader representations)? A behind-spec renderer is treated as stale so {ensureRenderer}
|
|
710
|
+
* deploys a fresh one (greenfield redeploy-freely: this number is what tells the CLI a deployed
|
|
711
|
+
* renderer predates a projection the current build emits). A v4 renderer is safe on ANY token —
|
|
712
|
+
* pointed at a legacy implementation it simply emits no params block. Bump in lockstep with
|
|
669
713
|
* `AbxMetadataRenderer.SPEC_VERSION`. */
|
|
670
714
|
async function isCurrentRenderer(publicClient, renderer) {
|
|
671
715
|
try {
|
|
@@ -674,7 +718,7 @@ async function isCurrentRenderer(publicClient, renderer) {
|
|
|
674
718
|
abi: abxMetadataRendererAbi,
|
|
675
719
|
functionName: 'specVersion',
|
|
676
720
|
}));
|
|
677
|
-
return v ===
|
|
721
|
+
return v === 4n;
|
|
678
722
|
}
|
|
679
723
|
catch {
|
|
680
724
|
return false;
|
|
@@ -708,7 +752,51 @@ const imageUrlTemplateField = (template) => ({
|
|
|
708
752
|
representation: encodeTag(R.urlTemplate),
|
|
709
753
|
value: toHex(template),
|
|
710
754
|
});
|
|
755
|
+
/** Backends that can hand out a publicly-readable URL for uploaded bytes — the ones that make
|
|
756
|
+
* "image off-chain, JSON on-chain, no server" possible. `fs` serves only from this machine. */
|
|
757
|
+
const DIRECT_URL_BACKENDS = new Set(['ipfs', 'arweave', 'cloud']);
|
|
711
758
|
const looksLikeSvg = (s) => /^\s*<(\?xml|svg)/i.test(s);
|
|
759
|
+
/**
|
|
760
|
+
* Does the IMAGE itself end up on-chain for this deploy? Two ways it can:
|
|
761
|
+
* • `--onchain-image` — the bytes are staged in the chunk store and read back via a `reader` field.
|
|
762
|
+
* • `--onchain-uri` with an SVG — v1 inlines SVG directly as a data: URI (raster cannot be).
|
|
763
|
+
* Anything else falls through to keccak256 custody, and the on-chain renderer serves its PLACEHOLDER
|
|
764
|
+
* image. This distinction is the difference between a token that is durable and one that only looks
|
|
765
|
+
* durable, so it must be computed from the actual file — never inferred from the flag alone (the
|
|
766
|
+
* success banner used to claim "fully on-chain, no hosting needed" for a keccak-anchored raster).
|
|
767
|
+
*/
|
|
768
|
+
/**
|
|
769
|
+
* Will `tokenURI` resolve to the creator's REAL image, with nothing to keep running?
|
|
770
|
+
*
|
|
771
|
+
* Three ways yes, and they are not the same promise: the bytes are on-chain
|
|
772
|
+
* ({@link imageEndsUpOnChain}), or they sit at a durable public URL that the on-chain JSON points at
|
|
773
|
+
* (a direct-URL backend — the "no server" pattern). Only `fs` custody under `--onchain-uri` fails,
|
|
774
|
+
* because the renderer then holds a hash and nothing can serve it. Keep this separate from
|
|
775
|
+
* "fully on-chain": conflating them is what let a keccak-anchored raster ship under a permanence
|
|
776
|
+
* banner it did not earn.
|
|
777
|
+
*/
|
|
778
|
+
function imageResolvesWithoutServer(flags, onchainImage) {
|
|
779
|
+
if (imageEndsUpOnChain(flags, onchainImage))
|
|
780
|
+
return true;
|
|
781
|
+
return !!flags.image && DIRECT_URL_BACKENDS.has(backendResolution(storageOverrides(flags)).backend);
|
|
782
|
+
}
|
|
783
|
+
function imageEndsUpOnChain(flags, onchainImage) {
|
|
784
|
+
if (onchainImage)
|
|
785
|
+
return true;
|
|
786
|
+
const image = typeof flags.image === 'string' ? flags.image : undefined;
|
|
787
|
+
if (!image)
|
|
788
|
+
return false;
|
|
789
|
+
try {
|
|
790
|
+
const p = resolvePath(image);
|
|
791
|
+
if (!statSync(p).isFile())
|
|
792
|
+
return false;
|
|
793
|
+
// Only the head matters — looksLikeSvg tests the leading tag.
|
|
794
|
+
return looksLikeSvg(readFileSync(p).subarray(0, 256).toString('utf8'));
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
712
800
|
async function prepareContent(imagePath, clone, overrides, store = true, // false for --dry-run: compute the hash but don't custody bytes
|
|
713
801
|
onChain = false, // true (--onchain-uri): put the image ON-CHAIN (inline SVG) so it self-resolves
|
|
714
802
|
remoteEth, // wallet-lane Turbo uploads: sign+pay with the connected browser wallet
|
|
@@ -730,9 +818,8 @@ walletAddr) {
|
|
|
730
818
|
const path = resolvePath(imagePath);
|
|
731
819
|
const bytes = new Uint8Array(readFileSync(path));
|
|
732
820
|
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.
|
|
821
|
+
// On-chain mode can inline SVG directly (the renderer emits it as a data: URI).
|
|
822
|
+
let onchainUrlLane = false;
|
|
736
823
|
if (onChain) {
|
|
737
824
|
const text = Buffer.from(bytes).toString('utf8');
|
|
738
825
|
if (looksLikeSvg(text)) {
|
|
@@ -741,7 +828,18 @@ walletAddr) {
|
|
|
741
828
|
contentNote: `content: ${basename(path)} (${bytes.length} bytes SVG) INLINE on-chain`,
|
|
742
829
|
};
|
|
743
830
|
}
|
|
744
|
-
|
|
831
|
+
// A raster can't be inlined in v1. That used to mean "fall through to keccak custody and let the
|
|
832
|
+
// renderer show a placeholder" — which quietly made the documented pattern-2 recipe (image
|
|
833
|
+
// off-chain, JSON on-chain, no server) impossible for a 1/1 even though `deploy-series` did it
|
|
834
|
+
// fine. Now the 1/1 takes the same route the Series takes: upload to a durable, publicly-readable
|
|
835
|
+
// backend and bake the resulting URL on-chain as the image field, so `tokenURI` resolves from the
|
|
836
|
+
// chain to a real image with nothing to keep running. Only a backend that can hand out a public
|
|
837
|
+
// read URL qualifies; `fs` (this machine only) still falls through to custody.
|
|
838
|
+
onchainUrlLane = DIRECT_URL_BACKENDS.has(backendResolution(overrides).backend);
|
|
839
|
+
if (!onchainUrlLane) {
|
|
840
|
+
warn(`--onchain-uri can't inline ${contentType} on-chain (v1 inlines SVG only), and '${backendResolution(overrides).backend}' can't serve a public URL — ` +
|
|
841
|
+
`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.`);
|
|
842
|
+
}
|
|
745
843
|
}
|
|
746
844
|
const hash = hashContent(bytes);
|
|
747
845
|
const opts = storageOptions(overrides); // flags override config/env
|
|
@@ -750,6 +848,13 @@ walletAddr) {
|
|
|
750
848
|
opts.arweave = { ...opts.arweave, remoteEth, jwk: undefined, ethSignerKey: undefined };
|
|
751
849
|
noteArweavePlan(opts, bytes.length); // free-vs-credit readout (Turbo) — shown for dry-run too
|
|
752
850
|
if (!store) {
|
|
851
|
+
const backendId = backendResolution(overrides).backend;
|
|
852
|
+
if (onchainUrlLane) {
|
|
853
|
+
return {
|
|
854
|
+
tokenFields: [imageUrlField(`<${backendId}-url>`)],
|
|
855
|
+
contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) → would upload to ${backendId}; its public URL becomes the ON-CHAIN image field (no server)`,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
753
858
|
return {
|
|
754
859
|
tokenFields: [imageKeccakField(hash)],
|
|
755
860
|
contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) · image keccak256 ${hash} (would be stored on deploy)`,
|
|
@@ -761,6 +866,26 @@ walletAddr) {
|
|
|
761
866
|
if (!opts.arweave?.remoteEth)
|
|
762
867
|
await assertTurboFundsForUpload(opts, [bytes.length], walletAddr);
|
|
763
868
|
const backend = resolveBackend(opts);
|
|
869
|
+
if (onchainUrlLane) {
|
|
870
|
+
// One file, uploaded as a one-entry directory — the same call `deploy-series` uses for its
|
|
871
|
+
// uniform-extension folder, so both commands produce the same URL shape from the same code path.
|
|
872
|
+
// Named `0.<ext>` because a 1/1 is token 0. Falls back to custody if the backend turns out not to
|
|
873
|
+
// support directories, rather than silently producing a token whose image never resolves.
|
|
874
|
+
const ext = extname(path).toLowerCase() || '.bin';
|
|
875
|
+
if (backend.putDirectory) {
|
|
876
|
+
const { base } = await backend.putDirectory([{ name: `0${ext}`, bytes, contentType }]);
|
|
877
|
+
const url = `${base}/0${ext}`;
|
|
878
|
+
// Keep the bytes under their hash too: `abx verify` can then still prove the served image
|
|
879
|
+
// matches what was uploaded, even though the on-chain field addresses it by URL.
|
|
880
|
+
await backend.put(hash, { bytes, contentType });
|
|
881
|
+
ok(`image → ${url} ${dim(`(${backend.id}; baked on-chain, no server)`)}`);
|
|
882
|
+
return {
|
|
883
|
+
tokenFields: [imageUrlField(url)],
|
|
884
|
+
contentNote: `content: ${basename(path)} (${bytes.length} bytes, ${contentType}) on ${backend.id} · ON-CHAIN image url ${url}`,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
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).`);
|
|
888
|
+
}
|
|
764
889
|
await backend.put(hash, { bytes, contentType });
|
|
765
890
|
return {
|
|
766
891
|
tokenFields: [imageKeccakField(hash)],
|
|
@@ -1023,7 +1148,7 @@ async function remoteLocators(address, stored, flags) {
|
|
|
1023
1148
|
}
|
|
1024
1149
|
// ── deploy (+ optionally serve) ──────────────────────────────────────────────
|
|
1025
1150
|
async function cmdDeploy(flags, serveAfter) {
|
|
1026
|
-
|
|
1151
|
+
refuseStrayFlags(flags, DEPLOY_FLAGS, 'deploy');
|
|
1027
1152
|
warnSignWithoutFor(flags);
|
|
1028
1153
|
const dimension = resolveDimension(flags.type ?? '1of1');
|
|
1029
1154
|
// A multi-token collection is its OWN command; `abx deploy` is the single-token (1/1) path.
|
|
@@ -1222,6 +1347,23 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1222
1347
|
if (!hasPublicUrl)
|
|
1223
1348
|
info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
|
|
1224
1349
|
}
|
|
1350
|
+
// `--onchain-uri` puts the tokenURI JSON on-chain; it does NOT put the IMAGE on-chain. Without
|
|
1351
|
+
// `--onchain-image` the image is committed as a keccak256 anchor, which the on-chain renderer
|
|
1352
|
+
// cannot serve bytes for — so `tokenURI` returns a PLACEHOLDER image. Say that here, before the
|
|
1353
|
+
// spend (this runs in `--dry-run` too), because the success banner used to claim "fully on-chain,
|
|
1354
|
+
// no hosting needed" for exactly this config and a creator would believe they had permanence they
|
|
1355
|
+
// don't. The code lane's dry run already warns about this case; the 1/1 lane shipped it silently.
|
|
1356
|
+
if (onChainUri && !imageResolvesWithoutServer(flags, onchainImage)) {
|
|
1357
|
+
warn(`${bold('--onchain-uri puts the metadata JSON on-chain, not the image.')} With '${backendResolution(storageOverrides(flags)).backend}' custody the image is only ` +
|
|
1358
|
+
`a keccak256 anchor, so ${bold('tokenURI')} will serve a PLACEHOLDER — nothing can fetch the bytes.`);
|
|
1359
|
+
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).`);
|
|
1360
|
+
}
|
|
1361
|
+
else if (onChainUri && !imageEndsUpOnChain(flags, onchainImage)) {
|
|
1362
|
+
// Resolves, durably, with no server — but the IMAGE is not on-chain, and that distinction is
|
|
1363
|
+
// exactly what a creator asking for permanence is buying. Say which one they are getting.
|
|
1364
|
+
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. ` +
|
|
1365
|
+
`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')}.`);
|
|
1366
|
+
}
|
|
1225
1367
|
// IPFS/Arweave custody serves the image from the gateway URL. A LOCAL gateway produces an image
|
|
1226
1368
|
// URL only this machine can load — the image equivalent of the localhost-tokenURI footgun above.
|
|
1227
1369
|
if (!onChainUri && !onchainImage) {
|
|
@@ -1524,7 +1666,23 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1524
1666
|
if (!serveAfter) {
|
|
1525
1667
|
if (onChainUri) {
|
|
1526
1668
|
// Fully on-chain → it self-resolves. Prove it from the chain, no server/localhost.
|
|
1527
|
-
|
|
1669
|
+
// BUT only claim "fully on-chain" when the IMAGE is on-chain too. With `--onchain-uri` alone
|
|
1670
|
+
// the image is a keccak256 anchor the renderer can't serve, so tokenURI yields a placeholder —
|
|
1671
|
+
// and this banner previously said "no server or hosting needed" over exactly that, which is a
|
|
1672
|
+
// promise of permanence the token does not have. Truthful, per-config wording instead.
|
|
1673
|
+
if (imageEndsUpOnChain(flags, onchainImage)) {
|
|
1674
|
+
console.log(`\n${g('Done — fully on-chain.')} ${dim('The token self-resolves; no server or hosting needed.')}`);
|
|
1675
|
+
}
|
|
1676
|
+
else if (imageResolvesWithoutServer(flags, onchainImage)) {
|
|
1677
|
+
const bk = backendResolution(storageOverrides(flags)).backend;
|
|
1678
|
+
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.')}`);
|
|
1679
|
+
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).`)}`);
|
|
1680
|
+
}
|
|
1681
|
+
else {
|
|
1682
|
+
console.log(`\n${g('Done — metadata on-chain.')} ${dim('tokenURI/contractURI resolve from the chain with no server.')}`);
|
|
1683
|
+
console.log(` ${c.orange}The IMAGE is not on-chain${c.reset} ${dim('— it is a keccak256 anchor, so tokenURI serves a placeholder image.')}`);
|
|
1684
|
+
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).`)}`);
|
|
1685
|
+
}
|
|
1528
1686
|
console.log(` ${bold(`abx tokenuri ${clone}`)} ${dim('# read tokenURI(0) straight from the contract + decode the JSON')}`);
|
|
1529
1687
|
if (noMint)
|
|
1530
1688
|
console.log(` ${bold(`abx mint ${clone}`)} ${dim('# issue token #0')}`);
|
|
@@ -1598,7 +1756,7 @@ const tokenFieldOf = (tokenId, f) => ({
|
|
|
1598
1756
|
value: f.value,
|
|
1599
1757
|
});
|
|
1600
1758
|
async function cmdDeploySeries(flags) {
|
|
1601
|
-
|
|
1759
|
+
refuseStrayFlags(flags, DEPLOY_SERIES_FLAGS, 'deploy-series');
|
|
1602
1760
|
warnSignWithoutFor(flags);
|
|
1603
1761
|
const name = flags.name ?? 'ABX Series';
|
|
1604
1762
|
const symbol = flags.symbol ?? 'ABXS';
|
|
@@ -2791,22 +2949,38 @@ async function cmdPreview(flags) {
|
|
|
2791
2949
|
if (shootDir) {
|
|
2792
2950
|
step(`Render ${count} seeds headlessly`);
|
|
2793
2951
|
try {
|
|
2952
|
+
const shotParams = parsePreviewParams(flags.param);
|
|
2953
|
+
if (Object.keys(shotParams).length) {
|
|
2954
|
+
info(`param overrides applied to every frame: ${Object.entries(shotParams).map(([k, v]) => `${k}=${v || dim('(unset)')}`).join(' · ')}`);
|
|
2955
|
+
}
|
|
2794
2956
|
const shots = await shootPreview(server.url, shootDir, count, {
|
|
2795
2957
|
width: Number(flags.width ?? 1000) || 1000,
|
|
2796
2958
|
timeoutMs: Number(flags['timeout-ms'] ?? 10_000) || 10_000,
|
|
2959
|
+
params: shotParams,
|
|
2797
2960
|
});
|
|
2798
2961
|
ok(`${shots.length} frames → ${shootDir}/ ${dim('(traits in traits.json)')}`);
|
|
2799
2962
|
for (const s of shots) {
|
|
2800
2963
|
const t = s.traits && Object.keys(s.traits).length
|
|
2801
2964
|
? Object.entries(s.traits).map(([k, v]) => `${k} ${String(v)}`).join(' · ')
|
|
2802
|
-
:
|
|
2803
|
-
|
|
2965
|
+
: s.timedOut
|
|
2966
|
+
? `${c.orange}timed out — traits unknown${c.reset}`
|
|
2967
|
+
: `${c.orange}no traits reported${c.reset}`;
|
|
2968
|
+
console.log(` ${dim(s.seed.slice(0, 10) + '…')} ${t}${s.done || s.timedOut ? '' : dim(' (no abx.done())')}`);
|
|
2804
2969
|
}
|
|
2970
|
+
const timedOut = shots.filter((s) => s.timedOut).length;
|
|
2805
2971
|
const silent = shots.filter((s) => !s.traits || !Object.keys(s.traits).length).length;
|
|
2806
|
-
|
|
2972
|
+
// A timeout is a measurement failure, not a finding about the program — so it must never be
|
|
2973
|
+
// reported as one. The old code printed the "NO frame reported traits" alarm whenever traits
|
|
2974
|
+
// came back empty for any reason, which condemned a CORRECT program on a loaded machine and
|
|
2975
|
+
// inverted the one line agents are told to trust.
|
|
2976
|
+
if (timedOut) {
|
|
2977
|
+
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 — ` +
|
|
2978
|
+
`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.`);
|
|
2979
|
+
}
|
|
2980
|
+
if (silent === shots.length && !timedOut) {
|
|
2807
2981
|
warn('NO frame reported traits — `abx.traits({…})` is the only thing that becomes marketplace `attributes`. Verify with `abx inspect`.');
|
|
2808
2982
|
}
|
|
2809
|
-
else if (shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
|
|
2983
|
+
else if (silent < shots.length && shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
|
|
2810
2984
|
// Only meaningful when traits DID come back: identical values across seeds is the signature
|
|
2811
2985
|
// of a sketch that never reads `abx.tokenData.seed` (prototyped on Math.random()), which
|
|
2812
2986
|
// deploys as N visually identical tokens. Skipped when nothing reported at all — the
|
|
@@ -2962,6 +3136,33 @@ const FLAG_HINTS = {
|
|
|
2962
3136
|
supply: () => 'did you mean --max <N> (the supply cap)?',
|
|
2963
3137
|
amount: () => 'a sale price/amount is set post-deploy via `abx minter configure`.',
|
|
2964
3138
|
};
|
|
3139
|
+
/**
|
|
3140
|
+
* Unknown flags on a command that can SEND: refuse, don't warn.
|
|
3141
|
+
*
|
|
3142
|
+
* This used to warn and carry on, and `--chain sepolia` proved why that isn't enough — the warning was
|
|
3143
|
+
* accurate, it scrolled past, and the deploy ran on the DEFAULT chain. On a dry run that costs a
|
|
3144
|
+
* confused minute; on a funded send it is a wrong-chain deploy. "Prose that was ignored once will be
|
|
3145
|
+
* ignored again" is the standing rule here, so the tx-sending commands stop instead.
|
|
3146
|
+
*
|
|
3147
|
+
* Safe to refuse rather than warn because a flag absent from the allowlist is, by construction, one
|
|
3148
|
+
* the command never reads — verified per command by comparing every `flags.x` read in its body against
|
|
3149
|
+
* its allowlist. So refusal cannot break a working flag; it only makes an already-ignored one loud.
|
|
3150
|
+
*
|
|
3151
|
+
* `--dry-run` refuses identically. A preview that accepts what the real send rejects is its own trap:
|
|
3152
|
+
* you'd validate a command in preview and have it fail at the moment it matters.
|
|
3153
|
+
*/
|
|
3154
|
+
function refuseStrayFlags(flags, allowed, cmd) {
|
|
3155
|
+
const stray = unknownFlags(flags, allowed);
|
|
3156
|
+
if (!stray.length)
|
|
3157
|
+
return;
|
|
3158
|
+
const hints = stray.map((f) => FLAG_HINTS[f]?.(cmd)).filter(Boolean);
|
|
3159
|
+
throw new Error(`unrecognized flag(s): ${stray.map((f) => '--' + f).join(', ')} — this command would have ignored them silently, so it is refusing instead.\n` +
|
|
3160
|
+
hints.map((h, i) => ` ↳ --${stray[i]}: ${h}`).join('\n') +
|
|
3161
|
+
(hints.length ? '\n' : '') +
|
|
3162
|
+
` see \`abx help ${cmd}\` for the flags it accepts.`);
|
|
3163
|
+
}
|
|
3164
|
+
/** Unknown flags on a READ-ONLY command: warn, don't refuse. Nothing can be mis-sent, and a stray
|
|
3165
|
+
* flag on `preview`/`inspect` shouldn't stop a creator mid-iteration. */
|
|
2965
3166
|
function warnStrayFlags(flags, allowed, cmd) {
|
|
2966
3167
|
const stray = unknownFlags(flags, allowed);
|
|
2967
3168
|
if (stray.length) {
|
|
@@ -3003,7 +3204,7 @@ async function cmdDeployCode(flags) {
|
|
|
3003
3204
|
// Surface unsupported/typo'd flags BEFORE any work — a silent no-op on a write-adjacent value
|
|
3004
3205
|
// (e.g. --description landing nowhere) is the worst failure mode. Non-fatal (see unknownFlags).
|
|
3005
3206
|
// Shared path so the "did you mean --max / price is post-deploy" hints fire here too.
|
|
3006
|
-
|
|
3207
|
+
refuseStrayFlags(flags, DEPLOY_CODE_FLAGS, 'deploy-code');
|
|
3007
3208
|
warnSignWithoutFor(flags);
|
|
3008
3209
|
const dryRun = !!flags['dry-run'];
|
|
3009
3210
|
const name = flags.name ?? 'ABX Code';
|
|
@@ -3438,27 +3639,21 @@ async function cmdDeployCode(flags) {
|
|
|
3438
3639
|
// gas cap and appear to REVERT. Expected, not a bug — a real session mis-diagnosed it as indexing lag.
|
|
3439
3640
|
const tokenUriGasRisk = onChainUri && !codeDir && (scriptAnalysis?.doc.estBytes ?? 0) > 30_000;
|
|
3440
3641
|
// --onchain-uri legs (ride the setup multicall, before the mints): the animation_url field
|
|
3441
|
-
// pointing at the generator, the
|
|
3442
|
-
//
|
|
3443
|
-
//
|
|
3444
|
-
//
|
|
3445
|
-
// the generator's on-chain tokenData stays byte-aligned with the resolver's (which carries
|
|
3446
|
-
// every evented param — inputsHash parity is load-bearing).
|
|
3642
|
+
// pointing at the generator, and the two URI renderers. The param surface needs NO leg — the
|
|
3643
|
+
// generator enumerates it from the token on-chain (`contractParamKeys`/`tokenParamKeys`, kept in
|
|
3644
|
+
// step by the write paths themselves), so there is nothing for a deploy to declare and nothing to
|
|
3645
|
+
// drift. (The retired `params.keys` CSV convention is gone; a legacy project keeps its old generator.)
|
|
3447
3646
|
// THE FOLD: on the renderer-only in-chain lane (no program), the on-chain-URI wiring (tokenURI/
|
|
3448
3647
|
// contractURI renderers) + the reserve mint move INTO the deploy tx's init params — the token is
|
|
3449
3648
|
// fully configured at deploy (the renderers already exist on-chain), so the setup multicall is
|
|
3450
3649
|
// needed ONLY for a PostParam schema (and is skipped entirely when there's none → a 1-tx drop).
|
|
3451
3650
|
// The script/dir lane is UNCHANGED: its chunks always need the multicall, and its mint must ride
|
|
3452
3651
|
// AFTER the chunks (a mint-before-chunks token would be transiently unresolvable), so its
|
|
3453
|
-
// URI-renderers + animation +
|
|
3652
|
+
// URI-renderers + animation + mint stay in the multicall exactly as before.
|
|
3454
3653
|
const foldIntoInit = onChainUri && !hasProgram;
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
// generator consumes params.keys; renderer-only has no generator and folds the renderers to init).
|
|
3459
|
-
const onchainUriLegs = onChainUri && hasProgram
|
|
3460
|
-
? onchainUriSetupCalls({ generator, metadataRenderer, paramsKeys: paramsKeysCsv })
|
|
3461
|
-
: null;
|
|
3654
|
+
// The URI-renderer/animation legs only ride the multicall for a PROGRAM (renderer-only has no
|
|
3655
|
+
// generator and folds the renderers into init).
|
|
3656
|
+
const onchainUriLegs = onChainUri && hasProgram ? onchainUriSetupCalls({ generator, metadataRenderer }) : null;
|
|
3462
3657
|
const setupMintCount = foldIntoInit ? 0 : mintCount; // folded mints ride init, not the multicall
|
|
3463
3658
|
// opt-in --confirm: one y/N before ANY upload or send (no-op without --confirm; never blocks scripts).
|
|
3464
3659
|
if (!dryRun) {
|
|
@@ -3466,7 +3661,7 @@ async function cmdDeployCode(flags) {
|
|
|
3466
3661
|
await confirmSend(`About to deploy code project "${name}" (${symbol}) — ${contentSummary}; ` +
|
|
3467
3662
|
(identityFields.length ? `${identityFields.join(' + ')} on-chain; ` : '') +
|
|
3468
3663
|
(onChainUri
|
|
3469
|
-
? `tokenURI ON-CHAIN via renderer ${metadataRenderer}${hasProgram ? ` (generator ${generator}
|
|
3664
|
+
? `tokenURI ON-CHAIN via renderer ${metadataRenderer}${hasProgram ? ` (generator ${generator})` : ' (renderer-only — image + traits from your Solidity renderers, no animation)'}; `
|
|
3470
3665
|
: `resolver base ${baseUrl}; `) +
|
|
3471
3666
|
`dependencies: ${deps.length ? deps.map((d) => d.display).join(', ') + (depRegistry ? ` (registry ${depRegistry})` : '') : 'none'}; ` +
|
|
3472
3667
|
`mint: ${mintCount > 0 ? `${mintCount} at deploy${foldIntoInit ? ' (in the deploy tx)' : ''}` : 'deferred'}; ` +
|
|
@@ -3501,7 +3696,7 @@ async function cmdDeployCode(flags) {
|
|
|
3501
3696
|
? (await publicClient.readContract({ address: factory, abi: seriesCodeFactoryAbi, functionName: 'predictDeterministicAddress', args: [salt] }))
|
|
3502
3697
|
: null;
|
|
3503
3698
|
const depLegs = deps.length + (depRegistry ? 1 : 0);
|
|
3504
|
-
const setupLen = scriptChunks.length + schemas.length + depLegs + (onchainUriLegs?.
|
|
3699
|
+
const setupLen = scriptChunks.length + schemas.length + depLegs + (onchainUriLegs?.length ?? 0) + setupMintCount;
|
|
3505
3700
|
step(`Deploy plan — a ${max}-token code project to ${CHAIN}`);
|
|
3506
3701
|
if (deployer)
|
|
3507
3702
|
info(`deployer ${deployer}`);
|
|
@@ -3526,7 +3721,7 @@ async function cmdDeployCode(flags) {
|
|
|
3526
3721
|
: `; no animation_url (renderer-only — image + attributes computed on-chain by your Solidity renderer(s))`));
|
|
3527
3722
|
if (hasProgram) {
|
|
3528
3723
|
info(` on-chain URI legs (ride the setup multicall): collection animation_url field (renderer rep → ${generator}) · ` +
|
|
3529
|
-
`
|
|
3724
|
+
`setTokenURIRenderer · setContractURIRenderer ${dim('(the param surface enumerates on-chain — no leg, nothing to maintain)')}`);
|
|
3530
3725
|
}
|
|
3531
3726
|
else {
|
|
3532
3727
|
info(` on-chain URI wiring rides the ${bold('deploy tx')} itself (tokenURI/contractURI renderers${mintCount > 0 ? ' + the reserve mint' : ''} in init) — no separate setup tx for it.`);
|
|
@@ -3578,7 +3773,7 @@ async function cmdDeployCode(flags) {
|
|
|
3578
3773
|
BigInt(storedBytes) * 216n + // ~200 gas/byte code deposit + ~16 gas/byte calldata
|
|
3579
3774
|
BigInt(schemas.length) * 45000n +
|
|
3580
3775
|
BigInt(depLegs) * 55000n +
|
|
3581
|
-
BigInt(onchainUriLegs?.
|
|
3776
|
+
BigInt(onchainUriLegs?.length ?? 0) * 60000n +
|
|
3582
3777
|
BigInt(mintCount) * 65000n;
|
|
3583
3778
|
const gasPrice = await publicClient.getGasPrice();
|
|
3584
3779
|
const eth = Number(roughGas * gasPrice) / 1e18;
|
|
@@ -3682,9 +3877,9 @@ async function cmdDeployCode(flags) {
|
|
|
3682
3877
|
});
|
|
3683
3878
|
const setupCalls = (owner) => [
|
|
3684
3879
|
...scriptChunks.map((chunk, i) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setScriptChunk', args: [BigInt(i), chunk] })),
|
|
3685
|
-
...schemas.map(({ key, paramType, auth, min, max, selectOptions }) => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setParamSchema', args: [encodeTag(key), paramType, auth,
|
|
3880
|
+
...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] })),
|
|
3686
3881
|
...dependencySetupCalls(deps, depRegistry), // setDependency legs in index order (+ the soft registry pointer)
|
|
3687
|
-
...(onchainUriLegs
|
|
3882
|
+
...(onchainUriLegs ?? []), // program lane only: animation_url field · the URI renderers (before the mints)
|
|
3688
3883
|
].concat(Array.from({ length: setupMintCount }, () => encodeFunctionData({ abi: seriesCodeAbi, functionName: 'mint', args: [owner] }))); // folded mints ride init, not here
|
|
3689
3884
|
const cid = resolveChain(CHAIN).id;
|
|
3690
3885
|
const preparedFor = async (owner) => {
|
|
@@ -3693,7 +3888,7 @@ async function cmdDeployCode(flags) {
|
|
|
3693
3888
|
const calls = setupCalls(owner);
|
|
3694
3889
|
const txs = [prepareDeploySeriesCode({ factory, params: initParamsFor(owner), salt, chainId: cid, clone })];
|
|
3695
3890
|
if (calls.length)
|
|
3696
|
-
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 }));
|
|
3891
|
+
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 }));
|
|
3697
3892
|
return { clone, txs };
|
|
3698
3893
|
};
|
|
3699
3894
|
let clone;
|
|
@@ -3745,8 +3940,19 @@ async function cmdDeployCode(flags) {
|
|
|
3745
3940
|
// the mempool rejects the duplicate as "replacement transaction underpriced" and the setup leg is
|
|
3746
3941
|
// lost (leaving a deployed-but-unconfigured clone). Fetch once, then increment per send.
|
|
3747
3942
|
let nonce = await publicClient.getTransactionCount({ address: account.address, blockTag: 'pending' });
|
|
3748
|
-
for (const tx of txs) {
|
|
3749
|
-
|
|
3943
|
+
for (const [i, tx] of txs.entries()) {
|
|
3944
|
+
// The SAME read-after-write lag the nonce is pinned for, one field over. Every tx after the
|
|
3945
|
+
// first targets the clone the FIRST tx created; if the node serving `eth_estimateGas` hasn't
|
|
3946
|
+
// seen that block yet, the target looks codeless and the estimate comes back as the CALLDATA
|
|
3947
|
+
// cost alone (~200k). That cannot fund the CREATE inside `SSTORE2.write` (~200 gas per stored
|
|
3948
|
+
// byte), so the CREATE returns 0 and the tx reverts `DeploymentFailed()` — which is what a
|
|
3949
|
+
// reporter hit on Base Sepolia on every `deploy-code` attempt, in both lanes: 201,616 sent
|
|
3950
|
+
// where 941,331 was needed. Wait for the code to be visible, then PIN the gas ourselves
|
|
3951
|
+
// (never re-read at send time), flooring it at what the payload provably costs.
|
|
3952
|
+
if (i > 0 && tx.to)
|
|
3953
|
+
await waitForCodeAt(publicClient, tx.to);
|
|
3954
|
+
const gas = await pinGas(publicClient, { from: account.address, to: tx.to, data: tx.data, gasFloor: tx.gasFloor });
|
|
3955
|
+
const hash = await wallet.sendTransaction({ to: tx.to, data: tx.data, account, chain: wallet.chain, nonce, gas });
|
|
3750
3956
|
nonce += 1;
|
|
3751
3957
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
3752
3958
|
if (receipt.status !== 'success')
|
|
@@ -3773,7 +3979,7 @@ async function cmdDeployCode(flags) {
|
|
|
3773
3979
|
step('On-chain URI');
|
|
3774
3980
|
ok(`tokenURI/contractURI resolve ON-CHAIN via the renderer ${metadataRenderer} — any RPC returns the metadata, no server in the graph`);
|
|
3775
3981
|
if (hasProgram)
|
|
3776
|
-
info(`animation_url computes on-chain via the generator ${generator}
|
|
3982
|
+
info(`animation_url computes on-chain via the generator ${generator} — it enumerates this token's params from chain, so there is no key list to maintain`);
|
|
3777
3983
|
else
|
|
3778
3984
|
info(`renderer-only — image + attributes computed on-chain by your Solidity renderer(s); no animation_url · ${g('fully on-chain')}, zero dependency outside the EVM`);
|
|
3779
3985
|
if (hasProgram && !codeDir) {
|
|
@@ -4203,6 +4409,18 @@ async function cmdVerify(address, flags) {
|
|
|
4203
4409
|
console.log(` ${c.orange}⚠${c.reset} no animation_url in the on-chain JSON — the generator field may be missing or unrenderable`);
|
|
4204
4410
|
}
|
|
4205
4411
|
}
|
|
4412
|
+
// The READ-side envelope. Params enumerate on-chain, so the write side is unbounded — but
|
|
4413
|
+
// tokenURI and tokenData assemble EVERY enumerated param per call, and that is what grows.
|
|
4414
|
+
// ~64 keys is the documented design envelope; hard failure only nears at several hundred.
|
|
4415
|
+
// Advisory (never a ✗) — a big surface is a legitimate choice, it just isn't free.
|
|
4416
|
+
const set = await readSetParamKeys(client, state.address, probe ? BigInt(probe.tokenId) : undefined);
|
|
4417
|
+
if (set) {
|
|
4418
|
+
const total = new Set([...set.contract, ...set.token]).size;
|
|
4419
|
+
if (total > 64) {
|
|
4420
|
+
console.log(` ${c.orange}⚠${c.reset} ${total} params enumerate on-chain${probe ? ` for token #${probe.tokenId}` : ''} — tokenURI and tokenData assemble every one of them per call, so both grow with this count. ` +
|
|
4421
|
+
dim('The documented design envelope is ~64; past a few hundred a public eth_call can hit its gas cap.'));
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4206
4424
|
}
|
|
4207
4425
|
catch (e) {
|
|
4208
4426
|
console.log(` ${c.orange}⚠${c.reset} on-chain URI check unavailable: ${e.message}`);
|
|
@@ -4381,29 +4599,35 @@ async function reportRemoteByteIntegrity(address, remote, base) {
|
|
|
4381
4599
|
// Read tokenURI(id) STRAIGHT FROM THE CONTRACT on-chain (no server, no node) and decode
|
|
4382
4600
|
// the data: URI. For a fully on-chain token (`--onchain-uri`), this is the proof it
|
|
4383
4601
|
// self-resolves: the renderer assembles the JSON on-chain; any RPC returns it.
|
|
4384
|
-
function decodeOnChainJson(uri) {
|
|
4602
|
+
function decodeOnChainJson(uri, verbatim = false) {
|
|
4385
4603
|
const m = /^data:application\/json(;[^,]*)?,([\s\S]*)$/.exec(uri);
|
|
4386
4604
|
if (!m)
|
|
4387
4605
|
return null;
|
|
4388
4606
|
const raw = (m[1] ?? '').includes('base64') ? Buffer.from(m[2], 'base64').toString('utf8') : decodeURIComponent(m[2]);
|
|
4389
4607
|
try {
|
|
4390
4608
|
const obj = JSON.parse(raw);
|
|
4609
|
+
// `verbatim` (--json) returns the document EXACTLY as the chain assembled it. Everything below is
|
|
4610
|
+
// a courtesy for human eyes, and abbreviating for eyes is fine — abbreviating for a PROGRAM is
|
|
4611
|
+
// not. An integrator scraped this output, got a `data:` URI cut to 96 chars that still looked
|
|
4612
|
+
// valid, and stored it; the only way to read their own token was to reimplement `eth_call`.
|
|
4613
|
+
if (verbatim)
|
|
4614
|
+
return JSON.stringify(obj, null, 2);
|
|
4391
4615
|
for (const k of Object.keys(obj)) {
|
|
4392
4616
|
const v = obj[k];
|
|
4393
4617
|
// Truncate only huge inline `data:` blobs (base64 SVGs/images); show plain locator URLs
|
|
4394
4618
|
// (http/ipfs/ar) IN FULL so the creator can verify where the image actually resolves.
|
|
4395
4619
|
if (typeof v === 'string' && v.startsWith('data:') && v.length > 96)
|
|
4396
|
-
obj[k] = `${v.slice(0, 96)}… (${v.length} chars)`;
|
|
4620
|
+
obj[k] = `${v.slice(0, 96)}… (${v.length} chars) [--json for the full value]`;
|
|
4397
4621
|
}
|
|
4398
4622
|
return JSON.stringify(obj, null, 2);
|
|
4399
4623
|
}
|
|
4400
4624
|
catch {
|
|
4401
|
-
return raw.slice(0, 600);
|
|
4625
|
+
return verbatim ? raw : raw.slice(0, 600);
|
|
4402
4626
|
}
|
|
4403
4627
|
}
|
|
4404
4628
|
async function cmdTokenUri(address, flags) {
|
|
4405
4629
|
if (!address || address.startsWith('--')) {
|
|
4406
|
-
console.error('usage: abx tokenuri <address> [--token <id>]\n');
|
|
4630
|
+
console.error('usage: abx tokenuri <address> [--token <id>] [--json]\n');
|
|
4407
4631
|
process.exit(1);
|
|
4408
4632
|
}
|
|
4409
4633
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
@@ -4438,6 +4662,12 @@ async function cmdTokenUri(address, flags) {
|
|
|
4438
4662
|
}
|
|
4439
4663
|
process.exit(1);
|
|
4440
4664
|
}
|
|
4665
|
+
// --json: the verbatim document, nothing else on stdout — no banner, no ANSI, no truncation, so
|
|
4666
|
+
// `abx tokenuri <addr> --json | jq` is a supported read path rather than something to regex.
|
|
4667
|
+
if (flags.json !== undefined) {
|
|
4668
|
+
process.stdout.write((decodeOnChainJson(uri, true) ?? uri) + '\n');
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4441
4671
|
console.log(`\n ${bold(`tokenURI(${tokenId})`)} ${dim(`— read directly from ${address} on ${CHAIN}, no server`)}`);
|
|
4442
4672
|
const json = decodeOnChainJson(uri);
|
|
4443
4673
|
if (json) {
|
|
@@ -4682,7 +4912,7 @@ async function cmdStorage(rest) {
|
|
|
4682
4912
|
*/
|
|
4683
4913
|
async function cmdStorageUpload(path, flags) {
|
|
4684
4914
|
if (!path || path.startsWith('--')) {
|
|
4685
|
-
console.error('usage: abx storage upload <path> [--backend ipfs|arweave|cloud] [--key <name>]\n uploads one file and prints its locator (the URI `abx attach` wants).\n');
|
|
4915
|
+
console.error('usage: abx storage upload <path> [--backend ipfs|arweave|cloud] [--key <name>] [--json]\n uploads one file and prints its locator (the URI `abx attach` wants); --json emits it as data.\n');
|
|
4686
4916
|
process.exit(1);
|
|
4687
4917
|
}
|
|
4688
4918
|
const abs = resolvePath(path);
|
|
@@ -4694,7 +4924,9 @@ async function cmdStorageUpload(path, flags) {
|
|
|
4694
4924
|
throw new Error("the 'fs' backend has no public URL — an attached file must be reachable off this machine. " +
|
|
4695
4925
|
'Upload with `--backend arweave` (pay-once permanent) or `--backend ipfs` (keep it pinned), or `--backend cloud` (your S3/R2).');
|
|
4696
4926
|
}
|
|
4697
|
-
|
|
4927
|
+
const jsonMode = flags.json !== undefined;
|
|
4928
|
+
if (!jsonMode)
|
|
4929
|
+
noteArweavePlan(opts, bytes.length); // free-vs-credit readout for the arweave lane
|
|
4698
4930
|
const name = (flags.key && flags.key !== 'true' ? flags.key : basename(abs)).replace(/^\/+/, '');
|
|
4699
4931
|
// --dry-run: an upload is IRREVERSIBLE and can happen even keyless (Arweave's free tier under
|
|
4700
4932
|
// 100 KB auto-creates an identity), so a "preview" that actually uploaded would surprise. Show
|
|
@@ -4704,17 +4936,37 @@ async function cmdStorageUpload(path, flags) {
|
|
|
4704
4936
|
console.log(`\n ${bold('◆ storage upload')} ${dim('(dry run — nothing uploaded)')}`);
|
|
4705
4937
|
console.log(` ${dim('file'.padEnd(10))} ${basename(abs)} ${dim(`(${bytes.length} bytes, ${contentType})`)}`);
|
|
4706
4938
|
console.log(` ${dim('backend'.padEnd(10))} ${backend.id}`);
|
|
4939
|
+
// Name the IPFS mode, and say what an absent credential actually means. There is no silent
|
|
4940
|
+
// fallback to local `fs` (an earlier skill line wrongly claimed one) — without PINATA_JWT the
|
|
4941
|
+
// backend resolves to kubo against a LOCAL node, so a dry run that looked fine would fail at
|
|
4942
|
+
// upload for anyone not running one. A preview should surface that, not defer it to the failure.
|
|
4943
|
+
if (backend.id === 'ipfs' && !process.env.PINATA_JWT && !process.env.ABX_IPFS_MODE) {
|
|
4944
|
+
warn(`no ${bold('PINATA_JWT')} set → this resolves to a LOCAL IPFS node (kubo at ${process.env.ABX_IPFS_API_URL ?? 'http://127.0.0.1:5001'}). The real upload fails unless that node is running — set PINATA_JWT for the managed lane, or ${bold('--backend arweave')} for no-setup permanent storage.`);
|
|
4945
|
+
}
|
|
4707
4946
|
console.log(` ${dim('locator'.padEnd(10))} ${shape} ${dim('(filename preserved → declared type survives)')}`);
|
|
4708
4947
|
console.log(dim(`\n Re-run without --dry-run to upload, then: `) + `${g(`abx attach <address> <key> ${shape}`)}\n`);
|
|
4709
4948
|
return;
|
|
4710
4949
|
}
|
|
4711
|
-
|
|
4950
|
+
// In --json mode nothing but the JSON may touch stdout; progress still goes out, on stderr.
|
|
4951
|
+
const progress = (line) => (jsonMode ? process.stderr.write(line + '\n') : console.log(line));
|
|
4952
|
+
progress(` uploading ${bold(basename(abs))} ${dim(`(${bytes.length} bytes, ${contentType})`)} to '${backend.id}' …`);
|
|
4712
4953
|
// The returned locator MUST carry the filename so the declared mimeType survives when attached
|
|
4713
4954
|
// (the on-chain field has no MIME slot — the URL extension IS the declaration). uploadAndLocate
|
|
4714
4955
|
// encapsulates the capability-based branch (cloud key / dir-wrap / bare fallback).
|
|
4715
4956
|
const { locator, filenamePreserved, fallbackReason } = await uploadAndLocate(backend, name, { bytes, contentType });
|
|
4716
4957
|
if (!filenamePreserved && fallbackReason) {
|
|
4717
|
-
|
|
4958
|
+
const msg = `couldn't wrap the file with its name (${fallbackReason}) — the locator has no extension, so its declared type will be application/octet-stream when attached.`;
|
|
4959
|
+
if (jsonMode)
|
|
4960
|
+
process.stderr.write(` ⚠ ${msg}\n`);
|
|
4961
|
+
else
|
|
4962
|
+
warn(msg);
|
|
4963
|
+
}
|
|
4964
|
+
// --json: the locator as data, not as prose. An integrator scraped this line, captured the ANSI
|
|
4965
|
+
// colour codes along with the URL, wrote the result into a STORED player URL, and only found out
|
|
4966
|
+
// when it 404'd in production. A value a program needs must be obtainable without parsing output.
|
|
4967
|
+
if (jsonMode) {
|
|
4968
|
+
process.stdout.write(JSON.stringify({ locator, backend: backend.id, name, bytes: bytes.length, contentType, filenamePreserved }) + '\n');
|
|
4969
|
+
return;
|
|
4718
4970
|
}
|
|
4719
4971
|
console.log(` ${g('✓')} uploaded → ${bold(locator)}`);
|
|
4720
4972
|
if (contentType === 'application/octet-stream') {
|
|
@@ -4843,6 +5095,51 @@ async function cmdState(address, flags) {
|
|
|
4843
5095
|
if (royalty)
|
|
4844
5096
|
info(`royalty ${Number(royalty[1]) / 100}% → ${royalty[0]}`);
|
|
4845
5097
|
info(`renderer ${renderer && renderer !== zeroAddress ? `on-chain (${renderer})` : dim('off-chain (stored URI base / override)')}`);
|
|
5098
|
+
// The PostParam surface, read straight from chain: `paramSchemaKeys()` is the DECLARED (governed)
|
|
5099
|
+
// set — append-only, and it includes keys declared but never yet written, which nothing off-chain
|
|
5100
|
+
// could otherwise discover — and `contractParamKeys()` is the collection-scope set values. Both
|
|
5101
|
+
// are maintained by the contract's own write paths, so there is no list to drift. Read-only,
|
|
5102
|
+
// best-effort, and skipped entirely for a project that has none (a 1/1, or a code project with
|
|
5103
|
+
// no params). LEGACY projects (deployed before enumeration) expose neither getter: they fall back
|
|
5104
|
+
// to the retired `params.keys` CSV so `abx state` still describes them.
|
|
5105
|
+
try {
|
|
5106
|
+
let keys = await readParamSchemaKeys(publicClient, address);
|
|
5107
|
+
let legacy = false;
|
|
5108
|
+
if (keys === null) {
|
|
5109
|
+
// The pre-enumeration lane, READ-ONLY. Nothing writes this CSV any more.
|
|
5110
|
+
keys = (await readLegacyParamsKeys(publicClient, address).catch(() => null)) ?? [];
|
|
5111
|
+
legacy = keys.length > 0;
|
|
5112
|
+
}
|
|
5113
|
+
const rows = keys.length
|
|
5114
|
+
? await Promise.all(keys.map(async (key) => ({ key, s: await readParamSchema(publicClient, address, key) })))
|
|
5115
|
+
: [];
|
|
5116
|
+
const governed = rows.filter((r) => r.s.exists);
|
|
5117
|
+
const now = Math.floor(Date.now() / 1000);
|
|
5118
|
+
if (governed.length) {
|
|
5119
|
+
console.log(`\n ${bold('PostParams')} ${dim(`${governed.length} governed · collectors/artist set these; the generator injects them`)}` +
|
|
5120
|
+
(legacy ? dim(' (legacy project — read from its params.keys list)') : ''));
|
|
5121
|
+
for (const { key, s } of governed) {
|
|
5122
|
+
const retired = s.lockAfter !== 0 && s.lockAfter < now;
|
|
5123
|
+
const locks = s.lockAfter !== 0 && !retired ? dim(` locks ${new Date(s.lockAfter * 1000).toISOString().slice(0, 10)}`) : '';
|
|
5124
|
+
const tag = retired ? ` ${c.orange}retired${c.reset}${dim(' — writes revert')}` : locks;
|
|
5125
|
+
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}`);
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
// Legacy only: a key the old CSV lists with no schema behind it. On an enumerating project the
|
|
5129
|
+
// class doesn't exist — schema keys come from the schema list, set values from the value lists.
|
|
5130
|
+
const ungoverned = legacy ? rows.filter((r) => !r.s.exists).map((r) => r.key) : [];
|
|
5131
|
+
if (ungoverned.length)
|
|
5132
|
+
info(`${dim('also in params.keys, no schema:')} ${ungoverned.join(', ')}`);
|
|
5133
|
+
// Collection-scope values (every token sees these). `seed` is per-token and never enumerated —
|
|
5134
|
+
// it is a tokenData coordinate, read directly.
|
|
5135
|
+
const set = legacy ? null : await readSetParamKeys(publicClient, address);
|
|
5136
|
+
if (set?.contract.length) {
|
|
5137
|
+
info(`${dim('contract-scope params set:')} ${[...set.contract].sort().join(', ')} ${dim('(apply to every token)')}`);
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
catch {
|
|
5141
|
+
/* best-effort — a non-code project has no params surface to read */
|
|
5142
|
+
}
|
|
4846
5143
|
console.log('');
|
|
4847
5144
|
}
|
|
4848
5145
|
/**
|
|
@@ -5826,10 +6123,13 @@ const COMMAND_HELP = {
|
|
|
5826
6123
|
${g('--onchain-uri')} the CHAIN-COMPLETE lane: tokenURI/contractURI resolve ON-CHAIN via the canonical metadata renderer, and
|
|
5827
6124
|
animation_url is COMPUTED on-chain by the canonical ${bold('AbxGenerator')} (template: the full HTML document;
|
|
5828
6125
|
directory: a parameterized gateway URL — 8KB URL budget, gateway liveness applies). No resolver base is baked
|
|
5829
|
-
(no --public-base-url needed); the
|
|
5830
|
-
|
|
6126
|
+
(no --public-base-url needed); the param surface is enumerated ON-CHAIN, so there is nothing to declare or
|
|
6127
|
+
keep in sync. 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>')}.
|
|
5831
6128
|
(--generator 0x.. / --renderer 0x.. override the canonical singletons; ABX_GENERATOR / ABX_RENDERER via env.)
|
|
5832
|
-
--schema key:Type:Auth,… governed PostParams (e.g. ${g('palette:HexColor:TokenOwner')}) —
|
|
6129
|
+
--schema key:Type:Auth,… governed PostParams (e.g. ${g('palette:HexColor:TokenOwner')}) — set values later with ${g('abx configure-param')},
|
|
6130
|
+
change the SCHEMA later with ${g('abx set-schema')}. An Address leg names its writer inline —
|
|
6131
|
+
${g('key:Type:Address(0x…)')} (a CONTRACT may hold it: the controller pattern for open participation).
|
|
6132
|
+
Append ${g(':lock=<when>')} (ISO date, unix seconds, or ${g('now')}) to freeze the value after that time.
|
|
5833
6133
|
${dim('repeatable OR comma-separable (like --dep): ')}${g('--schema a --schema b')}${dim(' ≡ ')}${g('--schema a,b')}
|
|
5834
6134
|
Type ∈ Bool·Select·Uint256Range·Int256Range·DecimalRange·HexColor·Timestamp·String·Bytes; Auth ∈ Artist·TokenOwner·Address (+ Or-combos).
|
|
5835
6135
|
${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)
|
|
@@ -5854,6 +6154,7 @@ const COMMAND_HELP = {
|
|
|
5854
6154
|
--backend ipfs|arweave directory-mode custody for the build upload ${g('--unpaused')} · --minter 0x.. · --primary-payee 0x..
|
|
5855
6155
|
${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.')}
|
|
5856
6156
|
signing: ${g('--send')} (default) · ${g('--sign')} · ${g('--unsigned')} ${dim('(needs --for 0x..)')} ${g('--bootstrap-factory')} ${dim('(private chains/sandboxes only)')}
|
|
6157
|
+
${g('--yes')} accept inferred --name/--symbol defaults (the real send otherwise REFUSES to bake placeholders)
|
|
5857
6158
|
${g('--dry-run')} preview (address · resolver base · schema · mint plan · tx count) — nothing sent, no bytes pinned
|
|
5858
6159
|
${g('--confirm')} interactive y/N gate before the real send
|
|
5859
6160
|
${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)')}:
|
|
@@ -5891,14 +6192,33 @@ const COMMAND_HELP = {
|
|
|
5891
6192
|
${dim('ABX_RESOLVER_ADMIN_TOKEN — and a backend that can name a public URL (cloud/ipfs/arweave), since the resolver takes the')}
|
|
5892
6193
|
${dim('URL and refuses the bytes. PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
|
|
5893
6194
|
${dim('To HOST the runner (fly/docker), use `abx deploy-effects`.')}`,
|
|
6195
|
+
'set-schema': `
|
|
6196
|
+
${bold('abx set-schema')} <address> ${g('--schema key:Type:Auth[:lock=<when>]')} ${dim('— attach or replace ONE PostParam schema. Owner-only. Sends a tx.')}
|
|
6197
|
+
A project's param surface is NOT frozen at deploy: ${g('setParamSchema')} is owner-gated with no deploy-time restriction,
|
|
6198
|
+
so you can add a key a piece turned out to need without redeploying (and losing the address, mints, and collectors).
|
|
6199
|
+
${bold('This is a full-row upsert, not a patch.')} Replacing an existing schema rewrites every field — restate anything you
|
|
6200
|
+
want to keep, including an existing ${g('lock=')}. The contract does ${bold('not')} re-validate values already stored under the key,
|
|
6201
|
+
so narrowing a bound, dropping a Select option, or changing the type strands them; that is refused unless you pass
|
|
6202
|
+
${g('--force')}. A key can never be un-governed — to decommission one use ${g('abx retire-param')}.
|
|
6203
|
+
${g('--force')} apply a change that could strand already-stored values
|
|
6204
|
+
${g('--dry-run')} print the exact tx and send nothing`,
|
|
6205
|
+
'retire-param': `
|
|
6206
|
+
${bold('abx retire-param')} <address> <key> ${dim('— permanently stop further writes to a PostParam. Owner-only. Sends a tx.')}
|
|
6207
|
+
Sets the schema's ${g('lockAfter')} into the past, so every later ${g('configure-param')} reverts ${g('ParamLockExpired')}. This is the
|
|
6208
|
+
closest the protocol comes to deleting a parameter, and it is ${bold('irreversible')}.
|
|
6209
|
+
Reads the current schema and changes ONLY the lock, so type/auth/bounds/options are carried forward untouched.
|
|
6210
|
+
What it does NOT do: remove the key (a governed key stays governed forever) or erase a value already stored — that
|
|
6211
|
+
value keeps serving in token data. A value written under a TokenOwner/Address leg came from a collector, and the
|
|
6212
|
+
artist deliberately cannot delete it.
|
|
6213
|
+
${g('--dry-run')} print the exact tx and send nothing`,
|
|
5894
6214
|
'configure-param': `
|
|
5895
6215
|
${bold('abx configure-param')} <address> <tokenId|-> <key> <value> ${dim('— set a PostParam (typed, canonical encode). Sends a tx.')}
|
|
5896
6216
|
Reads the on-chain schema for <key> and canonically encodes <value> (${g('#rrggbb')} for HexColor, fixed-decimal ranges,
|
|
5897
6217
|
Select by label, address, bool). Auth is per the schema leg (Artist=owner · TokenOwner — delegate.xyz honored · Address).
|
|
5898
6218
|
A change ${bold('re-addresses renders')} → the still becomes a placeholder until you re-render (${g('abx render <addr> <id>')} or the runner).
|
|
5899
6219
|
${g('<tokenId> "-"')} CONTRACT scope (owner-only raw setter; schema-less keys only) — the write path of well-known params
|
|
5900
|
-
like ${g('
|
|
5901
|
-
|
|
6220
|
+
like ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
|
|
6221
|
+
A contract-scope param applies to every token, and enumerates on-chain like any other.
|
|
5902
6222
|
--file <path> read the value from a file (String / Bytes payloads)
|
|
5903
6223
|
${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
|
|
5904
6224
|
the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
|
|
@@ -6000,7 +6320,8 @@ const COMMAND_HELP = {
|
|
|
6000
6320
|
${g('--dry-run')} preview the tx, send nothing · signing: ${g('--send')} (default, needs a hot key) · ${g('--sign')} (browser wallet, no key) · ${g('--unsigned')}
|
|
6001
6321
|
${dim('Need the URI first? Upload the file with')} ${g('abx storage upload <path> --backend arweave|ipfs')} ${dim('— it prints the locator to pass here.')}
|
|
6002
6322
|
${dim('mimeType is declared from the URL EXTENSION (…/master.tiff → image/tiff) — give the file a real extension.')}
|
|
6003
|
-
${dim('Surfaces in a resolver\'s artifacts listing (/data/<key> fetches it); a bare on-chain tokenURI shows reserved fields only
|
|
6323
|
+
${dim('Surfaces in a resolver\'s artifacts listing (/data/<key> fetches it); a bare on-chain tokenURI shows reserved fields + abx_params only —')}
|
|
6324
|
+
${dim('configured params DO appear on-chain (as')} ${bold('abx_params')}${dim('); ATTACHMENTS are the surface that needs a resolver.')}
|
|
6004
6325
|
${dim('The image/animation are just reserved members of the same manifest. `artifacts` itself is COMPUTED — not settable.')}`,
|
|
6005
6326
|
'set-field': `
|
|
6006
6327
|
${bold('abx set-field')} <address> --field <name> ${dim('— set an on-chain metadata field (the low-level primitive). Sends a tx.')}
|
|
@@ -6008,7 +6329,7 @@ const COMMAND_HELP = {
|
|
|
6008
6329
|
--field <name> a reserved display field (image · description · animation_url · external_url · attributes ·
|
|
6009
6330
|
background_color · …), a collection authorship/rights field with ${g('--collection')}
|
|
6010
6331
|
(artist · display_notes · artist_links · license), OR any custom key (which becomes an
|
|
6011
|
-
${bold('artifacts')} entry). NOT ${g('artifacts')}/${g('abx_provenance')} (computed).
|
|
6332
|
+
${bold('artifacts')} entry). NOT ${g('artifacts')}/${g('abx_params')}/${g('abx_provenance')} (computed).
|
|
6012
6333
|
--text "<s>" store literal UTF-8 ON-CHAIN (representation defaults ${g('inline')}) — NOT for an off-chain URL (use ${bold('attach')})
|
|
6013
6334
|
--file <path> store a FILE on-chain in SSTORE2 chunks behind the shared reader (any size; multi-chunk)
|
|
6014
6335
|
${g('--compress')} none|fastlz|gzip for --file (default none): ${g('fastlz')} = on-chain decode, stays renderable, cheaper storage;
|
|
@@ -6070,7 +6391,7 @@ const COMMAND_HELP = {
|
|
|
6070
6391
|
storage: `
|
|
6071
6392
|
${bold('abx storage')} <show|upload|balance|topup|backup-key> ${dim('— inspect / operate byte custody. Mostly read-only.')}
|
|
6072
6393
|
${g('show')} the resolved backend (fs | cloud | ipfs | arweave) + where each value came from
|
|
6073
|
-
${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run]
|
|
6394
|
+
${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run] [${g('--json')}]
|
|
6074
6395
|
${g('balance')} · ${g('topup')} --usd <n> Turbo (arweave) upload credits · ${g('backup-key')} --out <path> copy the managed key`,
|
|
6075
6396
|
demo: `
|
|
6076
6397
|
${bold('abx demo')} <${dim('no args')}> ${dim('— deploy a throwaway 1/1 to the testnet, index it, and serve it — a guided first run. Sends a tx.')}
|
|
@@ -6205,6 +6526,8 @@ function help() {
|
|
|
6205
6526
|
--dep-registry 0x.. · --description "<s>" · --external-url <url> · ${g('--image-base <url>')} (off-chain thumbnails at a deterministic /{id} URL) ·
|
|
6206
6527
|
${g('--attributes-renderer 0x..')} (traits on-chain) · --max N · ${g('--mint-all')}|${g('--mint-count')} N · --backend ipfs|arweave · ${g('--dry-run')}
|
|
6207
6528
|
${g('abx configure-param')} <addr> <id> <key> <value> set a governed PostParam (typed encode; a change re-addresses the render)
|
|
6529
|
+
${g('abx set-schema')} <addr> --schema key:Type:Auth[:lock=<when>] attach or replace ONE key's schema, any time after deploy
|
|
6530
|
+
${g('abx retire-param')} <addr> <key> stop all further writes to a param, permanently (the closest thing to removing one)
|
|
6208
6531
|
${g('abx set-dependency')} <addr> <index> <ref> declare/replace a code dependency (name@version via the registry pointer, or 0x.. on-chain)
|
|
6209
6532
|
${g('abx remove-last-dependency')} <addr> · ${g('abx set-dependency-registry')} <addr> <0x..|none> · ${g('abx lock-dependencies')} <addr> operate/freeze the set
|
|
6210
6533
|
${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]
|
|
@@ -6278,18 +6601,25 @@ function parseSaltFlag(raw) {
|
|
|
6278
6601
|
/** Locate the skill folder: the bundled copy beside the compiled CLI (published), else the
|
|
6279
6602
|
* canonical repo copy (dev). Returns null if neither is present. */
|
|
6280
6603
|
function resolveBundledSkill() {
|
|
6281
|
-
|
|
6282
|
-
const pkgDir = resolvePath(
|
|
6604
|
+
const here = fileURLToPath(import.meta.url);
|
|
6605
|
+
const pkgDir = resolvePath(here, '..', '..');
|
|
6283
6606
|
const bundled = joinPath(pkgDir, 'skill');
|
|
6607
|
+
// Running from SOURCE (`pnpm abx`, tsx on packages/cli/src/**) means we're in the repo working
|
|
6608
|
+
// tree, where the canonical skill is the source of truth and `<pkg>/skill` is gitignored PREPACK
|
|
6609
|
+
// OUTPUT that may be arbitrarily old. Preferring the bundle there is actively destructive: a
|
|
6610
|
+
// leftover `skill/` from a months-old `npm pack` silently overwrote the canonical skill with a
|
|
6611
|
+
// copy 8 versions behind (and the drift check then reported the damage as if the user had caused
|
|
6612
|
+
// it). In the published layout there is no repo and no canonical copy, so the bundle is correct.
|
|
6613
|
+
const fromSource = /[/\\]src[/\\][^/\\]+$/.test(here);
|
|
6614
|
+
const root = findRepoRoot();
|
|
6615
|
+
const canonical = root ? joinPath(root, '.claude', 'skills', 'abx-self-host') : null;
|
|
6616
|
+
if (fromSource && canonical && existsSync(joinPath(canonical, 'SKILL.md')))
|
|
6617
|
+
return canonical;
|
|
6618
|
+
// Published layout: dist/main.js → <pkg>/skill (see scripts/bundle-skill.mjs).
|
|
6284
6619
|
if (existsSync(joinPath(bundled, 'SKILL.md')))
|
|
6285
6620
|
return bundled;
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
if (root) {
|
|
6289
|
-
const canonical = joinPath(root, '.claude', 'skills', 'abx-self-host');
|
|
6290
|
-
if (existsSync(joinPath(canonical, 'SKILL.md')))
|
|
6291
|
-
return canonical;
|
|
6292
|
-
}
|
|
6621
|
+
if (canonical && existsSync(joinPath(canonical, 'SKILL.md')))
|
|
6622
|
+
return canonical;
|
|
6293
6623
|
return null;
|
|
6294
6624
|
}
|
|
6295
6625
|
/** Locate the in-chain renderer Foundry scaffold: bundled beside the CLI (published), else the
|
|
@@ -6361,11 +6691,20 @@ function resolveInstallParents(agentFlag) {
|
|
|
6361
6691
|
return [...parents];
|
|
6362
6692
|
}
|
|
6363
6693
|
/** Copy the bundled skill folder to `dest`, replacing any prior copy so a re-install after an
|
|
6364
|
-
* upgrade never leaves stale reference files behind.
|
|
6694
|
+
* upgrade never leaves stale reference files behind.
|
|
6695
|
+
*
|
|
6696
|
+
* Returns `'already-canonical'` when destination IS the source, having done nothing. That case is
|
|
6697
|
+
* not hypothetical: in a dev checkout of the abx repo `resolveBundledSkill()` resolves to the
|
|
6698
|
+
* canonical `.claude/skills/abx-self-host`, which is exactly where a cwd-relative install writes —
|
|
6699
|
+
* so the `rmSync` below would delete the canonical skill, and the copy would then have no source.
|
|
6700
|
+
* A self-destructing install is a bad way to learn that. */
|
|
6365
6701
|
function installSkillTo(src, dest) {
|
|
6702
|
+
if (resolvePath(src) === resolvePath(dest))
|
|
6703
|
+
return 'already-canonical';
|
|
6366
6704
|
mkdirSync(joinPath(dest, '..'), { recursive: true });
|
|
6367
6705
|
rmSync(dest, { recursive: true, force: true });
|
|
6368
6706
|
cpSync(src, dest, { recursive: true });
|
|
6707
|
+
return 'installed';
|
|
6369
6708
|
}
|
|
6370
6709
|
/**
|
|
6371
6710
|
* Install the bundled skill into the default per-agent parents (or `~` with `global`), reporting
|
|
@@ -6379,9 +6718,10 @@ function installSkillToDefaults(src, opts = {}) {
|
|
|
6379
6718
|
ok(`installed the abx skill v${version}${opts.global ? ' (global, ~)' : ''}:`);
|
|
6380
6719
|
for (const parent of parents) {
|
|
6381
6720
|
const dest = joinPath(base, parent, SKILL_DIR_NAME);
|
|
6382
|
-
installSkillTo(src, dest);
|
|
6721
|
+
const outcome = installSkillTo(src, dest);
|
|
6383
6722
|
const label = SKILL_PARENT_LABELS[parent];
|
|
6384
|
-
|
|
6723
|
+
const note = outcome === 'already-canonical' ? dim(' (already the canonical copy — left as is)') : label ? dim(' → ' + label) : '';
|
|
6724
|
+
console.log(` ${g(joinPath(parent, SKILL_DIR_NAME))}${note}`);
|
|
6385
6725
|
}
|
|
6386
6726
|
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
6387
6727
|
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.`);
|
|
@@ -6409,7 +6749,10 @@ async function cmdSkill(rest, flags) {
|
|
|
6409
6749
|
// whose skills dir we don't special-case, or a bespoke location).
|
|
6410
6750
|
if (typeof flags.target === 'string' && flags.target !== 'true') {
|
|
6411
6751
|
const dest = joinPath(resolvePath(flags.target), SKILL_DIR_NAME);
|
|
6412
|
-
installSkillTo(src, dest)
|
|
6752
|
+
if (installSkillTo(src, dest) === 'already-canonical') {
|
|
6753
|
+
ok(`${dest} is already the canonical skill v${version} — nothing to install.`);
|
|
6754
|
+
return;
|
|
6755
|
+
}
|
|
6413
6756
|
ok(`installed the abx skill v${version} → ${dest}`);
|
|
6414
6757
|
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
6415
6758
|
return;
|