@artblocks/abx-cli 0.1.0-alpha.13 → 0.1.0-alpha.15
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/main.js +271 -78
- 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 +29 -13
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +270 -127
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +8 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +27 -2
- package/dist/preview.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +4 -2
- package/skill/reference/code-projects.md +5 -3
- package/skill/reference/creator-token.md +71 -0
- package/skill/reference/decisions.md +2 -2
- package/skill/reference/operating.md +1 -1
package/dist/ownerops.js
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
* served state reflects the change. The agent picks the lane; the human only
|
|
7
7
|
* approves (wallet lane) or it's the env key (hot lane).
|
|
8
8
|
*/
|
|
9
|
-
import { assertChainId,
|
|
9
|
+
import { assertChainId, ensureChunkStore as sdkEnsureChunkStore, predictFixedPriceMinter, encodeReader, makePublicClient, makeWalletClient, oneOfOneImageAbi, seriesImageAbi, abxFixedPriceMinterAbi, deployFixedPriceMinter, prepareConfigureSale, preparePurchase, planChunks, planContentTxs, prepareLockContractField, prepareLockContractURI, prepareLockTokenField, prepareLockTokenURI, prepareMint, prepareSeriesMintMany, prepareSetMinter, prepareSetMaxInvocations, prepareSetPrimaryPayee, prepareSetPaused, prepareSetContractField, prepareSetContractURIBase, prepareSetContractURIOverride, prepareSetContractURIRenderer, prepareSetParamHooks, prepareSetParamSchema, prepareRetireParam, readParamSchema, PARAM_TYPES, prepareSetRoyalty, prepareSetTokenField, prepareSetTokenURIBase, prepareSetTokenURIOverride, prepareSetTokenURIRenderer, prepareSetTransferValidator, prepareTransfer, prepareTransferOwnership, readCreatorTokenStatus, resolveRecommendedTransferValidator, RECOMMENDED_TRANSFER_VALIDATOR, KNOWN_CHAIN_KEYS, resolveChain, resolveRpcUrl, redactRpcUrl, DEFAULT_CHAIN_KEY, stageContent, encodeTag, METADATA_FIELD as F, METADATA_REPRESENTATION as R, } from '@artblocks/abx-sdk';
|
|
10
10
|
import { SelfHostIndexer } from '@artblocks/abx-indexer';
|
|
11
11
|
import { encodeScalarParam, encodeTag as encodeTagSdk, isAccepted, prepareConfigureTokenParam, prepareConfigureTokenParamData, prepareSetContractParam, prepareSetContractParamData, seriesCodeAbi, } from '@artblocks/abx-sdk';
|
|
12
|
-
import {
|
|
13
|
-
import { DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
12
|
+
import { hasParamEnumeration } from './onchain-uri.js';
|
|
13
|
+
import { decodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
14
14
|
import { contentTypeFromPath } from '@artblocks/abx-storage';
|
|
15
15
|
import { toHex as toHexSdk } from 'viem';
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
17
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
18
18
|
import { gzipSync } from 'node:zlib';
|
|
19
19
|
import { formatEther, getAddress, isAddress, parseEther, toHex, zeroAddress } from 'viem';
|
|
20
|
-
import {
|
|
20
|
+
import { fixedPriceMinterAddress } from './config.js';
|
|
21
21
|
import { openWalletSession, signTx } from './signer.js';
|
|
22
22
|
import { resolveRemote, serviceClient } from './remote.js';
|
|
23
23
|
import { unknownFlags } from './flags.js';
|
|
@@ -34,17 +34,28 @@ const bold = (s) => `${C.bold}${s}${C.reset}`;
|
|
|
34
34
|
// COMPLETE set — but it's COMPUTED by the resolver/renderer from the token's fields + effect
|
|
35
35
|
// outputs; it is never a field you set. These keys are that computed output, so setting them by
|
|
36
36
|
// hand would only pollute the manifest with a bogus entry (a real round-1 agent trap). Refuse them
|
|
37
|
-
// and point at the real verb.
|
|
38
|
-
|
|
37
|
+
// and point at the real verb. `abx_params` joins them for the same reason: it is COMPUTED from the
|
|
38
|
+
// token's on-chain param enumeration, so a hand-set field of that name would be a decoy sitting
|
|
39
|
+
// beside the real block. See specs/protocol/data-plane.md + specs/protocol/onchain-metadata.md.
|
|
40
|
+
const COMPUTED_FIELD_KEYS = new Set(['artifacts', 'abx_params', 'abx_provenance']);
|
|
39
41
|
export function assertSettableField(field) {
|
|
40
42
|
if (COMPUTED_FIELD_KEYS.has(field)) {
|
|
41
43
|
const what = field === 'artifacts'
|
|
42
44
|
? "the COMPUTED manifest (the complete list of this token's files), assembled by the resolver/renderer from your fields"
|
|
43
|
-
:
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
: field === 'abx_params'
|
|
46
|
+
? "the COMPUTED parameter block, enumerated from the token's on-chain params (set one with `abx configure-param`, declare one with `abx set-schema`)"
|
|
47
|
+
: 'the COMPUTED provenance list, assembled by the resolver/renderer';
|
|
48
|
+
throw new Error(`"${field}" is not a field you set — it's ${what}.` +
|
|
49
|
+
(field === 'abx_params'
|
|
50
|
+
? `\n Set a param instead:\n abx configure-param <address> <tokenId|-> <key> <value>\n (or attach a FILE under your own key: abx attach <address> <yourkey> <ipfs://… | ar://… | https://…>)`
|
|
51
|
+
: ` To attach a file so it appears in the manifest, pick your OWN key:\n abx attach <address> <yourkey> <ipfs://… | ar://… | https://…>`));
|
|
46
52
|
}
|
|
47
53
|
}
|
|
54
|
+
/** Whether a value fits the literal `bytes32` lane of a raw contract-param write (printable ASCII,
|
|
55
|
+
* ≤ 31 bytes). Anything longer takes the data path — one blob, its hash evented. */
|
|
56
|
+
function fitsLiteralBytes32(value) {
|
|
57
|
+
return value.length <= 31 && /^[\x20-\x7e]+$/.test(value);
|
|
58
|
+
}
|
|
48
59
|
/** Flags `abx attach` recognizes — anything else warns (non-fatal), so a silent no-op flag surfaces. */
|
|
49
60
|
const ATTACH_FLAGS = ['file', 'compress', 'collection', 'token', 'send', 'sign', 'unsigned', 'yes', 'dry-run', 'port', 'sign-url-file', 'remote'];
|
|
50
61
|
/** Auto-detect the on-chain representation for an off-chain locator by its URI scheme. Returns null
|
|
@@ -240,8 +251,9 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
240
251
|
}));
|
|
241
252
|
const [exists, paramTypeIdx, , , , , , selectOptions] = schema;
|
|
242
253
|
// Contract scope (`-`): the raw owner setters (`setContractParam[Data]`) — the write path of
|
|
243
|
-
// well-known contract params like `
|
|
244
|
-
//
|
|
254
|
+
// well-known contract params like `display.gateway`. Schema-less keys only: a schema'd key closes
|
|
255
|
+
// the raw path on-chain (`SchemaGoverned`) and is per-token by design. No key is special here: a
|
|
256
|
+
// contract param is enumerated on-chain like any other and lands in tokenData for every token.
|
|
245
257
|
if (tokenIdRaw === '-') {
|
|
246
258
|
if (exists) {
|
|
247
259
|
throw new Error(`"${key}" is schema-governed — it's a per-token PostParam (abx configure-param ${contract} <tokenId> ${key} …). ` +
|
|
@@ -254,9 +266,9 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
254
266
|
await runWrite(contract, prepareSetContractParamData({ contract, key, data, chainId: cid }), flags, owner);
|
|
255
267
|
return;
|
|
256
268
|
}
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
if (
|
|
269
|
+
// Printable ASCII ≤ 31 chars rides as a literal readable bytes32; anything longer takes the
|
|
270
|
+
// data path (one blob, hash evented).
|
|
271
|
+
if (fitsLiteralBytes32(valueInput)) {
|
|
260
272
|
console.log(` ${key} (contract scope) ← "${valueInput}" ${dim('(literal bytes32; owner-only raw setter)')}`);
|
|
261
273
|
await runWrite(contract, prepareSetContractParam({ contract, key, value: encodeTagSdk(valueInput), display: valueInput, chainId: cid }), flags, owner);
|
|
262
274
|
}
|
|
@@ -285,22 +297,7 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
285
297
|
console.log(` ${key} (${typeName}) ← ${display} ${dim('(canonical encode; the chain enforces schema + auth)')}`);
|
|
286
298
|
sent = await runWrite(contract, prepareConfigureTokenParam({ contract, tokenId, key, value, display, chainId: chainId() }), flags);
|
|
287
299
|
}
|
|
288
|
-
// params.keys drift nudge (best-effort, never fails the command): a project on the on-chain
|
|
289
|
-
// URI lane enumerates its param surface in the `params.keys` contract param — the canonical
|
|
290
|
-
// generator reads it to build on-chain tokenData. A configured key that isn't listed is
|
|
291
|
-
// silently OMITTED there (byte-parity with the resolver breaks), so say it, with the fix.
|
|
292
300
|
if (sent) {
|
|
293
|
-
try {
|
|
294
|
-
const csv = await readParamsKeys(publicClient, contract);
|
|
295
|
-
const fixed = paramsKeysNudge(csv, key);
|
|
296
|
-
if (fixed !== null) {
|
|
297
|
-
console.log(` ${yellow('⚠')} params.keys doesn't list "${key}" — the on-chain generator's tokenData will omit it. Fix:\n` +
|
|
298
|
-
` ${bold(`abx configure-param ${contract} - params.keys ${fixed}`)}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
/* nudge is advisory — an RPC hiccup or a non-Params contract must never fail the write */
|
|
303
|
-
}
|
|
304
301
|
// What happens to the IMAGE depends on how it's produced. If the `image` field is computed
|
|
305
302
|
// on-chain (a `renderer` representation — the in-chain SVG lane), it re-tints AUTOMATICALLY: the
|
|
306
303
|
// renderer reads the param live, so tokenURI's image is already updated, nothing to re-render.
|
|
@@ -551,15 +548,29 @@ export async function cmdSetPrimaryPayee(address, flags) {
|
|
|
551
548
|
// marketplaces fetch the on-chain tokenURI; if the resolver wasn't warm they cache
|
|
552
549
|
// a miss until refreshed. We emit ERC-4906 on URI changes (4906-aware marketplaces
|
|
553
550
|
// auto-refresh) — this is the manual fallback for the rest, and after the genesis mint.
|
|
554
|
-
|
|
555
|
-
|
|
551
|
+
/**
|
|
552
|
+
* OpenSea's own chain slugs, keyed by ours. They are NOT our keys (`base-sepolia` is `base_sepolia`
|
|
553
|
+
* there) and not derivable from viem, so the map is unavoidable — but a chain missing from it must
|
|
554
|
+
* produce NO link rather than a wrong one, which is what `osChain ?? CHAIN` used to do.
|
|
555
|
+
*
|
|
556
|
+
* It previously listed only `sepolia` and `mainnet`, so the CLI's own DEFAULT chain (`base-sepolia`)
|
|
557
|
+
* fell through to the raw key: the refresh POST went to `/chain/base-sepolia/…` (a slug OpenSea does
|
|
558
|
+
* not know) and the printed link pointed at **mainnet** `opensea.io` for a testnet token. Same shape
|
|
559
|
+
* as the hardcoded explorer table that once sent every Base Sepolia link to Etherscan — hence
|
|
560
|
+
* `testnet` now comes from the chain registry instead of a second hand-maintained set.
|
|
561
|
+
*/
|
|
562
|
+
const OPENSEA_CHAIN = { 'base-sepolia': 'base_sepolia', sepolia: 'sepolia' };
|
|
556
563
|
export async function cmdRefresh(address, flags) {
|
|
557
564
|
const contract = requireAddress(address, 'abx refresh <address> [--token 0]');
|
|
558
565
|
const tokenId = flags.token ?? '0';
|
|
559
|
-
const
|
|
560
|
-
const
|
|
566
|
+
const chain = resolveChain(CHAIN);
|
|
567
|
+
const osChain = OPENSEA_CHAIN[CHAIN];
|
|
568
|
+
const explorer = chain.blockExplorers?.default?.url ?? '';
|
|
561
569
|
const apiKey = process.env.OPENSEA_API_KEY;
|
|
562
|
-
if (
|
|
570
|
+
if (!osChain) {
|
|
571
|
+
console.log(dim(` no OpenSea slug known for '${CHAIN}' — skipping the OpenSea refresh (a guessed slug 404s, and a guessed link would point at the wrong network).`));
|
|
572
|
+
}
|
|
573
|
+
else if (apiKey) {
|
|
563
574
|
const url = `https://api.opensea.io/api/v2/chain/${osChain}/contract/${contract}/nfts/${tokenId}/refresh`;
|
|
564
575
|
try {
|
|
565
576
|
const res = await fetch(url, { method: 'POST', headers: { 'x-api-key': apiKey, accept: 'application/json' } });
|
|
@@ -575,8 +586,10 @@ export async function cmdRefresh(address, flags) {
|
|
|
575
586
|
else {
|
|
576
587
|
console.log(dim(' no OPENSEA_API_KEY set — open these and click “Refresh metadata”:'));
|
|
577
588
|
}
|
|
578
|
-
|
|
579
|
-
|
|
589
|
+
// `testnet` from the chain registry (viem), never a local set — that is the drift this bug was.
|
|
590
|
+
const osBase = chain.testnet ? 'https://testnets.opensea.io' : 'https://opensea.io';
|
|
591
|
+
if (osChain)
|
|
592
|
+
console.log(` ${dim('OpenSea ')}${osBase}/assets/${osChain}/${contract}/${tokenId}`);
|
|
580
593
|
if (explorer)
|
|
581
594
|
console.log(` ${dim('Etherscan')} ${explorer}/token/${contract}?a=${tokenId}`);
|
|
582
595
|
console.log(dim(' (ERC-4906 already pings 4906-aware marketplaces on URI changes; this covers the genesis mint + the rest.)'));
|
|
@@ -648,6 +661,112 @@ export async function cmdSetRoyalty(address, flags) {
|
|
|
648
661
|
}
|
|
649
662
|
await runWrite(contract, prepareSetRoyalty({ contract, receiver, bps, chainId: chainId() }), flags, owner);
|
|
650
663
|
}
|
|
664
|
+
// ── ERC-721C (creator token) — the transfer-validator surface ─────────────────
|
|
665
|
+
// Enrollment is a DEPLOY-TIME decision (`--721c` on the deploy commands) and permanent in both
|
|
666
|
+
// directions: an unenrolled token can never gain a validator, an enrolled one never sheds the
|
|
667
|
+
// standard. Within an enrolled token the owner re-points or suspends (zero) the validator freely.
|
|
668
|
+
/** The chain keys the manifest recommends a transfer validator for — for refusal messages. */
|
|
669
|
+
function chainsWithRecommendedValidator() {
|
|
670
|
+
const ids = new Set(Object.keys(RECOMMENDED_TRANSFER_VALIDATOR).map(Number));
|
|
671
|
+
return KNOWN_CHAIN_KEYS.filter((k) => ids.has(resolveChain(k).id)).join(', ');
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Parse a transfer-validator choice — the shared grammar of the deploy flag (`--721c`) and the
|
|
675
|
+
* owner op (`abx set-transfer-validator`):
|
|
676
|
+
* - `recommended` (or a bare `--721c`) → the per-chain recommended validator, refusing on a
|
|
677
|
+
* chain the manifest has no entry for (naming the chains that do — never guess one);
|
|
678
|
+
* - a `0x…` address → checksum-validated (EIP-55) and returned as its canonical form;
|
|
679
|
+
* - `none`/zero → `allowNone` decides: the owner op suspends with it; the DEPLOY flag refuses
|
|
680
|
+
* it (a zero validator at deploy never enrolls — that is already the default, so someone
|
|
681
|
+
* passing it either wants plain ERC-721 (drop the flag) or mistakenly believes "enrolled but
|
|
682
|
+
* suspended" is a deploy-time state — it isn't).
|
|
683
|
+
* Pure (no RPC) — callers do the has-code precheck themselves. Exported for the regression test.
|
|
684
|
+
*/
|
|
685
|
+
export function parseTransferValidatorValue(raw, opts) {
|
|
686
|
+
const s = raw.trim().toLowerCase();
|
|
687
|
+
if (s === 'true' || s === '' || s === 'recommended') {
|
|
688
|
+
const rec = resolveRecommendedTransferValidator(opts.chainId);
|
|
689
|
+
if (!rec) {
|
|
690
|
+
throw new Error(`no recommended transfer validator is known for '${opts.chainLabel}' (chainId ${opts.chainId}) — ` +
|
|
691
|
+
`chains with one: ${chainsWithRecommendedValidator() || '(none shipped)'}. ` +
|
|
692
|
+
`Pass an explicit validator address instead (it must be a deployed contract on this chain).`);
|
|
693
|
+
}
|
|
694
|
+
return rec;
|
|
695
|
+
}
|
|
696
|
+
if (s === 'none' || s === 'zero' || s === '0' || s === '0x0' || s === zeroAddress) {
|
|
697
|
+
if (opts.allowNone)
|
|
698
|
+
return zeroAddress;
|
|
699
|
+
throw new Error(`a zero transfer validator never enrolls — plain ERC-721 is already the default, so drop --721c. ` +
|
|
700
|
+
`("enrolled but suspended" is not a deploy-time state: enroll with a real validator, then suspend ` +
|
|
701
|
+
`with \`abx set-transfer-validator <address> none\`.)`);
|
|
702
|
+
}
|
|
703
|
+
const trimmed = raw.trim();
|
|
704
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
|
|
705
|
+
throw new Error(`transfer validator must be 'recommended'${opts.allowNone ? ", 'none'," : ''} or a 0x address (0x + 40 hex); got '${raw}'`);
|
|
706
|
+
}
|
|
707
|
+
// `isAddress` strict-validates the EIP-55 checksum of a mixed-case address (all-lowercase carries
|
|
708
|
+
// no checksum and passes); `getAddress` alone only NORMALIZES — it would silently accept a
|
|
709
|
+
// mis-cased paste, which is exactly the transposition this check exists to catch.
|
|
710
|
+
if (!isAddress(trimmed, { strict: true })) {
|
|
711
|
+
throw new Error(`transfer validator address failed its EIP-55 checksum: '${raw}' — paste it exactly (or all-lowercase).`);
|
|
712
|
+
}
|
|
713
|
+
return getAddress(trimmed);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* `abx set-transfer-validator <address> <0x…|none|recommended>` — re-point an ENROLLED (ERC-721C)
|
|
717
|
+
* collection's transfer validator, or suspend enforcement with `none` (address(0); the token
|
|
718
|
+
* STAYS enrolled). Refuses up front, before any signing: a plain ERC-721 (enrollment is
|
|
719
|
+
* deploy-time-only — the contract would revert `NotCreatorToken()`), and a codeless validator
|
|
720
|
+
* (would revert `InvalidTransferValidator()`). Owner-only, any lane, guards `--dry-run`.
|
|
721
|
+
*/
|
|
722
|
+
export async function cmdSetTransferValidator(address, rest, flags) {
|
|
723
|
+
const usage = 'abx set-transfer-validator <address> <0x…|none|recommended> [--sign|--unsigned] [--dry-run]';
|
|
724
|
+
const contract = requireAddress(address, usage);
|
|
725
|
+
const [raw] = positionalArgs(rest);
|
|
726
|
+
if (!raw) {
|
|
727
|
+
console.error(`usage: ${usage}\n`);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
const cid = chainId();
|
|
731
|
+
const validator = parseTransferValidatorValue(raw, { chainId: cid, chainLabel: CHAIN, allowNone: true });
|
|
732
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
733
|
+
// Enrollment guard FIRST — a read, not a send. An unenrolled token would revert
|
|
734
|
+
// `NotCreatorToken()`; refuse with the real story instead of letting the chain say it in hex.
|
|
735
|
+
// (readCreatorTokenStatus is defensive, so check the contract exists first — a typo'd address
|
|
736
|
+
// must not read as "plain ERC-721".)
|
|
737
|
+
await assertContractExists(contract);
|
|
738
|
+
const status = await readCreatorTokenStatus(publicClient, contract);
|
|
739
|
+
if (!status.enrolled) {
|
|
740
|
+
throw new Error(`${contract} is a plain ERC-721 — 721C is a deploy-time decision, and this collection didn't enroll. ` +
|
|
741
|
+
`Enrollment can never be added to a live collection; if enforcement is required, redeploy with ` +
|
|
742
|
+
`--721c recommended (or --721c 0x…) on the deploy command.`);
|
|
743
|
+
}
|
|
744
|
+
// Codeless-validator guard: the contract refuses a non-zero validator with no code
|
|
745
|
+
// (`InvalidTransferValidator()`) — surface it before gas is spent.
|
|
746
|
+
if (validator !== zeroAddress) {
|
|
747
|
+
let code;
|
|
748
|
+
try {
|
|
749
|
+
code = await publicClient.getCode({ address: validator });
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
throw new Error(`couldn't verify the validator has code at ${validator} (${err.message}) — refusing to re-point blind; retry when the RPC answers.`);
|
|
753
|
+
}
|
|
754
|
+
if (!code || code === '0x') {
|
|
755
|
+
const rec = resolveRecommendedTransferValidator(cid);
|
|
756
|
+
throw new Error(`no contract code at ${validator} on ${CHAIN} — the token would revert InvalidTransferValidator(). ` +
|
|
757
|
+
`A transfer validator must be a DEPLOYED contract on this chain` +
|
|
758
|
+
(rec ? ` (the recommended one: \`abx set-transfer-validator ${contract} recommended\` → ${rec})` : '') + `.`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (eqAddr(status.validator, validator)) {
|
|
762
|
+
console.log(dim(` no change — the validator is already ${validator === zeroAddress ? 'suspended (0x0)' : validator}. Nothing sent.`));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const label = (a) => (eqAddr(a, zeroAddress) ? 'suspended (0x0)' : a);
|
|
766
|
+
console.log(dim(` transfer validator: ${label(status.validator)} → ${label(validator)}${validator === zeroAddress ? ' — transfers go unvalidated until one is set again (the token stays enrolled)' : ''}`));
|
|
767
|
+
const owner = await read(contract, 'owner');
|
|
768
|
+
await runWrite(contract, prepareSetTransferValidator({ contract, validator, chainId: cid }), flags, owner);
|
|
769
|
+
}
|
|
651
770
|
export function parseCompress(v) {
|
|
652
771
|
const c = (v ?? 'none').toLowerCase();
|
|
653
772
|
if (c === 'none' || c === 'fastlz' || c === 'gzip')
|
|
@@ -660,37 +779,27 @@ export function parseCompress(v) {
|
|
|
660
779
|
* store is ownerless, so any funded signer can stand it up.
|
|
661
780
|
*/
|
|
662
781
|
async function ensureChunkStore(send, override) {
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
|
|
782
|
+
// The resolution logic lives in the SDK (`ensureChunkStore`) so an SDK integrator bootstraps
|
|
783
|
+
// identically instead of hand-rolling the "is this store capable?" guard — forget it and an
|
|
784
|
+
// incapable store fails deep inside a mint, after transactions have landed. The CLI keeps only the
|
|
785
|
+
// narration: the SDK reports progress through `onEvent` rather than printing.
|
|
666
786
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const pcode = await publicClient.getCode({ address: predicted });
|
|
684
|
-
if (pcode && pcode !== '0x' && (await storeSupportsWriteContent(publicClient, predicted))) {
|
|
685
|
-
console.log(dim(` using the canonical chunk store at its deterministic address ${predicted}`));
|
|
686
|
-
return predicted;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
console.log(dim(' deploying the canonical multi-chunk content store (AbxChunkStore) — CREATE2…'));
|
|
690
|
-
const { chunkStore } = await deployChunkStore(send);
|
|
691
|
-
console.log(` ${green('✓')} chunk store ${chunkStore}`);
|
|
692
|
-
console.log(dim(` not in the shipped manifest for ${CHAIN} — to reuse it set ABX_CHUNK_STORE=${chunkStore} (or add it to packages/sdk/src/deployments.ts)`));
|
|
693
|
-
return chunkStore;
|
|
787
|
+
return sdkEnsureChunkStore(publicClient, send, {
|
|
788
|
+
chainId: chainId(),
|
|
789
|
+
override,
|
|
790
|
+
onEvent: (e) => {
|
|
791
|
+
if (e.kind === 'stale')
|
|
792
|
+
console.log(yellow(` configured chunk store ${e.address} is a stale deployment (no writeContent) — deploying a current one`));
|
|
793
|
+
else if (e.kind === 'canonical')
|
|
794
|
+
console.log(dim(` using the canonical chunk store at its deterministic address ${e.address}`));
|
|
795
|
+
else if (e.kind === 'deploying')
|
|
796
|
+
console.log(dim(' deploying the canonical multi-chunk content store (AbxChunkStore) — CREATE2…'));
|
|
797
|
+
else {
|
|
798
|
+
console.log(` ${green('✓')} chunk store ${e.address}`);
|
|
799
|
+
console.log(dim(` not in the shipped manifest for ${CHAIN} — to reuse it set ABX_CHUNK_STORE=${e.address} (or add it to packages/sdk/src/deployments.ts)`));
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
});
|
|
694
803
|
}
|
|
695
804
|
/**
|
|
696
805
|
* The hot-lane staging signer: the env key signs + broadcasts each chunk-store write. Built
|
|
@@ -845,6 +954,55 @@ export async function stageImageFieldsBatch(imagePaths, compress, send, storeOve
|
|
|
845
954
|
}
|
|
846
955
|
return { fields, store };
|
|
847
956
|
}
|
|
957
|
+
// ── the generator repoint guard ───────────────────────────────────────────────
|
|
958
|
+
// The canonical AbxGenerator reads a token's param surface FROM CHAIN (`tokenParamKeys` /
|
|
959
|
+
// `contractParamKeys`). A LEGACY implementation — deployed before enumeration shipped — has neither
|
|
960
|
+
// getter, so the generator finds nothing: every configured param vanishes from tokenData and from
|
|
961
|
+
// the live view, silently, behind a tokenURI that still looks perfectly healthy. That is the exact
|
|
962
|
+
// failure class this toolkit refuses rather than warns about, so pointing a legacy token at the
|
|
963
|
+
// current generator is REFUSED. (Repointing the metadata RENDERER is safe and unguarded: a v4
|
|
964
|
+
// renderer on a legacy token just emits no params block.)
|
|
965
|
+
/** The field-renderer address a `--value` names: the canonical `abi.encode(address)` (32 bytes),
|
|
966
|
+
* or a bare 20-byte address. Null when it is neither. */
|
|
967
|
+
function fieldRendererTarget(value) {
|
|
968
|
+
const hex = value.trim();
|
|
969
|
+
if (!/^0x[0-9a-fA-F]*$/.test(hex))
|
|
970
|
+
return null;
|
|
971
|
+
if (hex.length === 42)
|
|
972
|
+
return getAddress(hex);
|
|
973
|
+
if (hex.length !== 66)
|
|
974
|
+
return null;
|
|
975
|
+
try {
|
|
976
|
+
return decodeFieldRenderer(hex);
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
export async function assertGeneratorRepointable(contract, field, flags,
|
|
983
|
+
/** Injected for tests; the real probe is one eth_call. */
|
|
984
|
+
probe = (c) => hasParamEnumeration(makePublicClient({ chainKey: CHAIN }), c)) {
|
|
985
|
+
if (flags.representation !== R.renderer || !flags.value || flags.value === 'true')
|
|
986
|
+
return;
|
|
987
|
+
const target = fieldRendererTarget(flags.value);
|
|
988
|
+
const generator = resolveGenerator(chainId());
|
|
989
|
+
// Only the CANONICAL generator is guarded — any other field renderer is the creator's own contract
|
|
990
|
+
// and none of our business.
|
|
991
|
+
if (!target || !generator || target.toLowerCase() !== generator.toLowerCase())
|
|
992
|
+
return;
|
|
993
|
+
// The `owner` read has already succeeded by the time this runs, so the RPC is proven reachable:
|
|
994
|
+
// a failing probe here means the getter is absent, not that the node is down.
|
|
995
|
+
if (await probe(contract))
|
|
996
|
+
return;
|
|
997
|
+
throw new Error(`refusing to point ${field} at the canonical generator ${generator} — ${contract} does not expose the param\n` +
|
|
998
|
+
` enumeration surface (tokenParamKeys/contractParamKeys), so it is a LEGACY implementation. The current\n` +
|
|
999
|
+
` generator reads params FROM CHAIN, so on this token it would read NOTHING: every configured param would\n` +
|
|
1000
|
+
` silently disappear from tokenData, the live view, and every render — with a tokenURI that still looks fine.\n\n` +
|
|
1001
|
+
` Two honest options:\n` +
|
|
1002
|
+
` • stay on the generator this project already uses (pass that address as --value; it reads the project's\n` +
|
|
1003
|
+
` params.keys list, which is how it has always worked here), or\n` +
|
|
1004
|
+
` • redeploy the project with the current \`abx deploy-code\` — new projects enumerate on-chain and need no list.`);
|
|
1005
|
+
}
|
|
848
1006
|
// ── set-field ──────────────────────────────────────────────────────────────--
|
|
849
1007
|
// Set an on-chain metadata field. `--field` is what (e.g. image, description),
|
|
850
1008
|
// `--representation` is how it's carried (default `inline` for --text, `keccak256`
|
|
@@ -859,6 +1017,7 @@ export async function cmdSetField(address, flags) {
|
|
|
859
1017
|
const collection = !!flags.collection;
|
|
860
1018
|
const lane = laneFromFlags(flags);
|
|
861
1019
|
const owner = await read(contract, 'owner');
|
|
1020
|
+
await assertGeneratorRepointable(contract, field, flags); // legacy impl + the current generator = params silently invisible
|
|
862
1021
|
const staging = !!(flags.file && flags.file !== 'true'); // large content ON-CHAIN via chunk store/reader
|
|
863
1022
|
// --file + --dry-run: preview the on-chain staging plan and store/send NOTHING. (The locator/text
|
|
864
1023
|
// path flows through runWrite, which previews there; staging must short-circuit before any upload.)
|
|
@@ -988,9 +1147,25 @@ export async function cmdAttach(rest, flags) {
|
|
|
988
1147
|
dim('Point the URI at the file itself (…/master.tiff, …/coa.pdf) so collectors get the right type.'));
|
|
989
1148
|
}
|
|
990
1149
|
console.log(` attaching ${bold(key)} ${dim(`(${mimeType}, ${representation})`)} to ${scope}: ${dim(uri)}`);
|
|
991
|
-
// Where it surfaces
|
|
992
|
-
//
|
|
993
|
-
|
|
1150
|
+
// Where it surfaces. This used to be one dim line, and it read as a footnote rather than as a
|
|
1151
|
+
// dependency: an integrator attached five audio stems to a fully-on-chain token, paid to store
|
|
1152
|
+
// them, and found `tokenURI` listed none of them — "paid for, stored on-chain, and invisible".
|
|
1153
|
+
// The on-chain renderer deliberately omits locator-represented artifacts (they duplicate no
|
|
1154
|
+
// on-chain type information — see specs/protocol/data-plane.md), so the artifacts manifest comes
|
|
1155
|
+
// from a RESOLVER. When the project has no resolver baked in, that is not a footnote, it is the
|
|
1156
|
+
// difference between a feature working and not existing, so say it as a warning.
|
|
1157
|
+
const uriBase = await read(contract, 'tokenURIBase').catch(() => '');
|
|
1158
|
+
const artifactsPath = `/t/${chainId()}/${contract}/${flags.token ?? '0'}`;
|
|
1159
|
+
if (uriBase && uriBase.trim() !== '') {
|
|
1160
|
+
console.log(dim(` → listed in this project's resolver artifacts (${artifactsPath} and /data/${key}); a bare on-chain tokenURI carries reserved fields plus abx_params only.`));
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
console.log(yellow(' ⚠ ') +
|
|
1164
|
+
`this project resolves ON-CHAIN (no resolver base baked in), and the on-chain document carries reserved fields plus the computed ${bold('abx_params')} block only — ` +
|
|
1165
|
+
`so ${bold(key)} will NOT appear in ${bold('tokenURI')}. The bytes are stored and provable, but nothing surfaces them to a marketplace or wallet. ` +
|
|
1166
|
+
dim('(Configured params DO appear on-chain — attachments are the surface that needs a resolver.)'));
|
|
1167
|
+
console.log(dim(` to make attached artifacts visible, point the project at a resolver (${bold('abx deploy-resolver')}, or a managed one via ${bold('abx add <addr> --remote <name>')}) — it serves the listing at ${artifactsPath}.`));
|
|
1168
|
+
}
|
|
994
1169
|
const buildTx = () => collection
|
|
995
1170
|
? prepareSetContractField({ contract, field: key, representation, value: toHex(uri), chainId: chainId() })
|
|
996
1171
|
: prepareSetTokenField({ contract, tokenId: BigInt(flags.token ?? '0'), field: key, representation, value: toHex(uri), chainId: chainId() });
|
|
@@ -1001,7 +1176,7 @@ export async function cmdAttach(rest, flags) {
|
|
|
1001
1176
|
dim(` verify (a resolver serves the complete listing): `) +
|
|
1002
1177
|
`curl <your-resolver>/t/${chainId()}/${contract}/${id}` +
|
|
1003
1178
|
dim(` ${collection ? '' : `→ artifacts[].key "${key}"; /data/${key} fetches it`}\n`) +
|
|
1004
|
-
dim(` (The complete file listing is a resolver surface — the bare on-chain tokenURI enumerates reserved fields
|
|
1179
|
+
dim(` (The complete file listing is a resolver surface — the bare on-chain tokenURI enumerates reserved fields plus abx_params. Params never need a resolver; attachments do.)\n`));
|
|
1005
1180
|
}
|
|
1006
1181
|
}
|
|
1007
1182
|
// ── lock-field ───────────────────────────────────────────────────────────────
|
|
@@ -1273,25 +1448,6 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1273
1448
|
// as a protocol limitation ("we must guess every param up front, or redeploy and lose the address").
|
|
1274
1449
|
// Two commands close that: `set-schema` (attach or replace one key) and `retire-param` (stop all
|
|
1275
1450
|
// further writes, the closest thing to a delete the protocol has).
|
|
1276
|
-
/**
|
|
1277
|
-
* The `params.keys` companion write for a newly governed key, or null when none is needed.
|
|
1278
|
-
*
|
|
1279
|
-
* Pure so it can be tested without a chain: the decision (does this project even use the on-chain
|
|
1280
|
-
* generator? is the key already listed? does the new CSV still fit a literal bytes32?) is the part
|
|
1281
|
-
* that goes wrong, not the RPC call. `csv === null` means the project isn't on the on-chain URI lane
|
|
1282
|
-
* — a resolver reads schemas directly and needs no key list.
|
|
1283
|
-
*/
|
|
1284
|
-
export function paramsKeysSyncOp(csv, key, contract, cid) {
|
|
1285
|
-
const fixed = paramsKeysNudge(csv, key);
|
|
1286
|
-
if (fixed === null)
|
|
1287
|
-
return null;
|
|
1288
|
-
// Past 31 chars the CSV can no longer ride as a literal bytes32 and takes the data path — the same
|
|
1289
|
-
// rule `configure-param` and `deploy-code` apply, so all three agree on how a long list is stored.
|
|
1290
|
-
const op = paramsKeysIsLiteral(fixed)
|
|
1291
|
-
? prepareSetContractParam({ contract, key: PARAMS_KEYS, value: encodeTagSdk(fixed), display: fixed, chainId: cid })
|
|
1292
|
-
: prepareSetContractParamData({ contract, key: PARAMS_KEYS, data: toHexSdk(new TextEncoder().encode(fixed)), chainId: cid });
|
|
1293
|
-
return { op, listed: fixed };
|
|
1294
|
-
}
|
|
1295
1451
|
/** Values already stored under a key can be stranded by a schema change — the contract does NOT
|
|
1296
1452
|
* re-validate them. Compare old vs new and name what would break, so the guard can refuse. */
|
|
1297
1453
|
export function strandingRisks(before, after) {
|
|
@@ -1343,48 +1499,35 @@ export async function cmdSetSchema(address, flags) {
|
|
|
1343
1499
|
`\n\n The contract does NOT re-validate stored values against a new schema, so affected tokens would keep\n` +
|
|
1344
1500
|
` values their own schema no longer allows. Re-run with --force if that is what you intend.`);
|
|
1345
1501
|
}
|
|
1502
|
+
// With --force, say what is being overridden. Silently applying a value-stranding change is the
|
|
1503
|
+
// one outcome worse than refusing it: the operator gets no record of which tokens they may have
|
|
1504
|
+
// just invalidated, and neither does anyone reading the terminal afterwards.
|
|
1505
|
+
if (risks.length) {
|
|
1506
|
+
console.log(` ${C.yellow}⚠${C.reset} ${bold('--force')} — applying a change that can strand stored values:`);
|
|
1507
|
+
for (const r of risks)
|
|
1508
|
+
console.log(` • ${r}`);
|
|
1509
|
+
console.log(dim(` any token already holding a value for "${next.key}" keeps it, now outside what its schema allows.`));
|
|
1510
|
+
}
|
|
1346
1511
|
if (before.lockAfter && !next.lockAfter) {
|
|
1347
1512
|
console.log(` ${C.yellow}⚠${C.reset} the existing lock (${new Date(before.lockAfter * 1000).toISOString().slice(0, 19)}Z) is being REMOVED — restate it with :lock= to keep it.`);
|
|
1348
1513
|
}
|
|
1349
1514
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
// Keep `params.keys` in step, ATOMICALLY. On the on-chain URI lane the canonical generator builds
|
|
1367
|
-
// tokenData from that CSV, so a key governed but not listed is silently omitted from every render —
|
|
1368
|
-
// the schema exists, the collector can set it, and the art never sees it. `deploy-code` composes
|
|
1369
|
-
// the list from `--schema` for exactly this reason; a schema added later has to do the same or
|
|
1370
|
-
// `set-schema` is a second-class path that quietly half-works. `readParamsKeys` returns null when
|
|
1371
|
-
// the project isn't on that lane (a resolver reads schemas directly), so this adds nothing there.
|
|
1372
|
-
try {
|
|
1373
|
-
const csv = await readParamsKeys(publicClient, contract);
|
|
1374
|
-
const sync = paramsKeysSyncOp(csv, next.key, contract, cid);
|
|
1375
|
-
if (sync) {
|
|
1376
|
-
console.log(` ${dim('params.keys')} ${dim('←')} ${sync.listed} ${dim('(so the on-chain generator injects it; same tx)')}`);
|
|
1377
|
-
ops.push(sync.op);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
catch {
|
|
1381
|
-
// Best-effort, never fails the command — but say so, because silence here looks like "no update
|
|
1382
|
-
// was needed" when it actually means "we could not check".
|
|
1383
|
-
console.log(` ${C.yellow}⚠${C.reset} couldn't read params.keys — if this project is on the on-chain URI lane, verify it lists "${next.key}".`);
|
|
1384
|
-
}
|
|
1385
|
-
await runWrite(contract, ops.length > 1
|
|
1386
|
-
? prepareMulticall({ ops, summary: `Set the schema for "${next.key}" and list it in params.keys` })
|
|
1387
|
-
: ops[0], flags, owner);
|
|
1515
|
+
// ONE op. A schema write used to need a `params.keys` companion write in the same tx to keep the
|
|
1516
|
+
// on-chain generator's key list in step; the generator now enumerates params from the token
|
|
1517
|
+
// itself, so the schema write is the whole change.
|
|
1518
|
+
await runWrite(contract, prepareSetParamSchema({
|
|
1519
|
+
contract,
|
|
1520
|
+
key: next.key,
|
|
1521
|
+
paramType: next.paramType,
|
|
1522
|
+
auth: next.auth,
|
|
1523
|
+
authAddress: next.authAddress,
|
|
1524
|
+
lockAfter: next.lockAfter,
|
|
1525
|
+
min: next.min,
|
|
1526
|
+
max: next.max,
|
|
1527
|
+
selectOptions: next.selectOptions,
|
|
1528
|
+
chainId: chainId(),
|
|
1529
|
+
display: describeSchema(next),
|
|
1530
|
+
}), flags, owner);
|
|
1388
1531
|
}
|
|
1389
1532
|
/** Render an on-chain schema through the same formatter the CLI uses for a parsed one. */
|
|
1390
1533
|
function onChainToParsed(key, s) {
|