@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/ownerops.js
CHANGED
|
@@ -6,21 +6,22 @@
|
|
|
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, prepareTransfer, prepareTransferOwnership, 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';
|
|
24
|
+
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
24
25
|
const CHAIN = process.env.ABX_CHAIN ?? DEFAULT_CHAIN_KEY; // base-sepolia default — MUST match main.ts/config.ts (a stale 'sepolia' here silently ran every owner-op on the wrong chain)
|
|
25
26
|
// ── ANSI (local) ─────────────────────────────────────────────────────────────
|
|
26
27
|
const C = { reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[38;5;115m', yellow: '\x1b[38;5;221m' };
|
|
@@ -33,17 +34,28 @@ const bold = (s) => `${C.bold}${s}${C.reset}`;
|
|
|
33
34
|
// COMPLETE set — but it's COMPUTED by the resolver/renderer from the token's fields + effect
|
|
34
35
|
// outputs; it is never a field you set. These keys are that computed output, so setting them by
|
|
35
36
|
// hand would only pollute the manifest with a bogus entry (a real round-1 agent trap). Refuse them
|
|
36
|
-
// and point at the real verb.
|
|
37
|
-
|
|
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']);
|
|
38
41
|
export function assertSettableField(field) {
|
|
39
42
|
if (COMPUTED_FIELD_KEYS.has(field)) {
|
|
40
43
|
const what = field === 'artifacts'
|
|
41
44
|
? "the COMPUTED manifest (the complete list of this token's files), assembled by the resolver/renderer from your fields"
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
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://…>`));
|
|
45
52
|
}
|
|
46
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
|
+
}
|
|
47
59
|
/** Flags `abx attach` recognizes — anything else warns (non-fatal), so a silent no-op flag surfaces. */
|
|
48
60
|
const ATTACH_FLAGS = ['file', 'compress', 'collection', 'token', 'send', 'sign', 'unsigned', 'yes', 'dry-run', 'port', 'sign-url-file', 'remote'];
|
|
49
61
|
/** Auto-detect the on-chain representation for an off-chain locator by its URI scheme. Returns null
|
|
@@ -239,8 +251,9 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
239
251
|
}));
|
|
240
252
|
const [exists, paramTypeIdx, , , , , , selectOptions] = schema;
|
|
241
253
|
// Contract scope (`-`): the raw owner setters (`setContractParam[Data]`) — the write path of
|
|
242
|
-
// well-known contract params like `
|
|
243
|
-
//
|
|
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.
|
|
244
257
|
if (tokenIdRaw === '-') {
|
|
245
258
|
if (exists) {
|
|
246
259
|
throw new Error(`"${key}" is schema-governed — it's a per-token PostParam (abx configure-param ${contract} <tokenId> ${key} …). ` +
|
|
@@ -253,9 +266,9 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
253
266
|
await runWrite(contract, prepareSetContractParamData({ contract, key, data, chainId: cid }), flags, owner);
|
|
254
267
|
return;
|
|
255
268
|
}
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
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)) {
|
|
259
272
|
console.log(` ${key} (contract scope) ← "${valueInput}" ${dim('(literal bytes32; owner-only raw setter)')}`);
|
|
260
273
|
await runWrite(contract, prepareSetContractParam({ contract, key, value: encodeTagSdk(valueInput), display: valueInput, chainId: cid }), flags, owner);
|
|
261
274
|
}
|
|
@@ -267,9 +280,9 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
267
280
|
}
|
|
268
281
|
const tokenId = BigInt(tokenIdRaw);
|
|
269
282
|
if (!exists) {
|
|
270
|
-
throw new Error(`no PostParam schema for "${key}" on ${contract}.
|
|
271
|
-
`abx deploy-code … --schema ${key}:<Type>:<Auth
|
|
272
|
-
`
|
|
283
|
+
throw new Error(`no PostParam schema for "${key}" on ${contract}. Declare one — at deploy with ` +
|
|
284
|
+
`abx deploy-code … --schema ${key}:<Type>:<Auth>, or right now on the live contract with ` +
|
|
285
|
+
`abx set-schema ${contract} --schema ${key}:<Type>:<Auth> (e.g. ${key}:HexColor:TokenOwner).`);
|
|
273
286
|
}
|
|
274
287
|
const typeName = ['Bool', 'Select', 'Uint256Range', 'Int256Range', 'DecimalRange', 'HexColor', 'Timestamp', 'String', 'Bytes'][paramTypeIdx];
|
|
275
288
|
let sent;
|
|
@@ -284,22 +297,7 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
284
297
|
console.log(` ${key} (${typeName}) ← ${display} ${dim('(canonical encode; the chain enforces schema + auth)')}`);
|
|
285
298
|
sent = await runWrite(contract, prepareConfigureTokenParam({ contract, tokenId, key, value, display, chainId: chainId() }), flags);
|
|
286
299
|
}
|
|
287
|
-
// params.keys drift nudge (best-effort, never fails the command): a project on the on-chain
|
|
288
|
-
// URI lane enumerates its param surface in the `params.keys` contract param — the canonical
|
|
289
|
-
// generator reads it to build on-chain tokenData. A configured key that isn't listed is
|
|
290
|
-
// silently OMITTED there (byte-parity with the resolver breaks), so say it, with the fix.
|
|
291
300
|
if (sent) {
|
|
292
|
-
try {
|
|
293
|
-
const csv = await readParamsKeys(publicClient, contract);
|
|
294
|
-
const fixed = paramsKeysNudge(csv, key);
|
|
295
|
-
if (fixed !== null) {
|
|
296
|
-
console.log(` ${yellow('⚠')} params.keys doesn't list "${key}" — the on-chain generator's tokenData will omit it. Fix:\n` +
|
|
297
|
-
` ${bold(`abx configure-param ${contract} - params.keys ${fixed}`)}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
/* nudge is advisory — an RPC hiccup or a non-Params contract must never fail the write */
|
|
302
|
-
}
|
|
303
301
|
// What happens to the IMAGE depends on how it's produced. If the `image` field is computed
|
|
304
302
|
// on-chain (a `renderer` representation — the in-chain SVG lane), it re-tints AUTOMATICALLY: the
|
|
305
303
|
// renderer reads the param live, so tokenURI's image is already updated, nothing to re-render.
|
|
@@ -550,15 +548,29 @@ export async function cmdSetPrimaryPayee(address, flags) {
|
|
|
550
548
|
// marketplaces fetch the on-chain tokenURI; if the resolver wasn't warm they cache
|
|
551
549
|
// a miss until refreshed. We emit ERC-4906 on URI changes (4906-aware marketplaces
|
|
552
550
|
// auto-refresh) — this is the manual fallback for the rest, and after the genesis mint.
|
|
553
|
-
|
|
554
|
-
|
|
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' };
|
|
555
563
|
export async function cmdRefresh(address, flags) {
|
|
556
564
|
const contract = requireAddress(address, 'abx refresh <address> [--token 0]');
|
|
557
565
|
const tokenId = flags.token ?? '0';
|
|
558
|
-
const
|
|
559
|
-
const
|
|
566
|
+
const chain = resolveChain(CHAIN);
|
|
567
|
+
const osChain = OPENSEA_CHAIN[CHAIN];
|
|
568
|
+
const explorer = chain.blockExplorers?.default?.url ?? '';
|
|
560
569
|
const apiKey = process.env.OPENSEA_API_KEY;
|
|
561
|
-
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) {
|
|
562
574
|
const url = `https://api.opensea.io/api/v2/chain/${osChain}/contract/${contract}/nfts/${tokenId}/refresh`;
|
|
563
575
|
try {
|
|
564
576
|
const res = await fetch(url, { method: 'POST', headers: { 'x-api-key': apiKey, accept: 'application/json' } });
|
|
@@ -574,8 +586,10 @@ export async function cmdRefresh(address, flags) {
|
|
|
574
586
|
else {
|
|
575
587
|
console.log(dim(' no OPENSEA_API_KEY set — open these and click “Refresh metadata”:'));
|
|
576
588
|
}
|
|
577
|
-
|
|
578
|
-
|
|
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}`);
|
|
579
593
|
if (explorer)
|
|
580
594
|
console.log(` ${dim('Etherscan')} ${explorer}/token/${contract}?a=${tokenId}`);
|
|
581
595
|
console.log(dim(' (ERC-4906 already pings 4906-aware marketplaces on URI changes; this covers the genesis mint + the rest.)'));
|
|
@@ -659,37 +673,27 @@ export function parseCompress(v) {
|
|
|
659
673
|
* store is ownerless, so any funded signer can stand it up.
|
|
660
674
|
*/
|
|
661
675
|
async function ensureChunkStore(send, override) {
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
|
|
676
|
+
// The resolution logic lives in the SDK (`ensureChunkStore`) so an SDK integrator bootstraps
|
|
677
|
+
// identically instead of hand-rolling the "is this store capable?" guard — forget it and an
|
|
678
|
+
// incapable store fails deep inside a mint, after transactions have landed. The CLI keeps only the
|
|
679
|
+
// narration: the SDK reports progress through `onEvent` rather than printing.
|
|
665
680
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const pcode = await publicClient.getCode({ address: predicted });
|
|
683
|
-
if (pcode && pcode !== '0x' && (await storeSupportsWriteContent(publicClient, predicted))) {
|
|
684
|
-
console.log(dim(` using the canonical chunk store at its deterministic address ${predicted}`));
|
|
685
|
-
return predicted;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
console.log(dim(' deploying the canonical multi-chunk content store (AbxChunkStore) — CREATE2…'));
|
|
689
|
-
const { chunkStore } = await deployChunkStore(send);
|
|
690
|
-
console.log(` ${green('✓')} chunk store ${chunkStore}`);
|
|
691
|
-
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)`));
|
|
692
|
-
return chunkStore;
|
|
681
|
+
return sdkEnsureChunkStore(publicClient, send, {
|
|
682
|
+
chainId: chainId(),
|
|
683
|
+
override,
|
|
684
|
+
onEvent: (e) => {
|
|
685
|
+
if (e.kind === 'stale')
|
|
686
|
+
console.log(yellow(` configured chunk store ${e.address} is a stale deployment (no writeContent) — deploying a current one`));
|
|
687
|
+
else if (e.kind === 'canonical')
|
|
688
|
+
console.log(dim(` using the canonical chunk store at its deterministic address ${e.address}`));
|
|
689
|
+
else if (e.kind === 'deploying')
|
|
690
|
+
console.log(dim(' deploying the canonical multi-chunk content store (AbxChunkStore) — CREATE2…'));
|
|
691
|
+
else {
|
|
692
|
+
console.log(` ${green('✓')} chunk store ${e.address}`);
|
|
693
|
+
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)`));
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
});
|
|
693
697
|
}
|
|
694
698
|
/**
|
|
695
699
|
* The hot-lane staging signer: the env key signs + broadcasts each chunk-store write. Built
|
|
@@ -844,6 +848,55 @@ export async function stageImageFieldsBatch(imagePaths, compress, send, storeOve
|
|
|
844
848
|
}
|
|
845
849
|
return { fields, store };
|
|
846
850
|
}
|
|
851
|
+
// ── the generator repoint guard ───────────────────────────────────────────────
|
|
852
|
+
// The canonical AbxGenerator reads a token's param surface FROM CHAIN (`tokenParamKeys` /
|
|
853
|
+
// `contractParamKeys`). A LEGACY implementation — deployed before enumeration shipped — has neither
|
|
854
|
+
// getter, so the generator finds nothing: every configured param vanishes from tokenData and from
|
|
855
|
+
// the live view, silently, behind a tokenURI that still looks perfectly healthy. That is the exact
|
|
856
|
+
// failure class this toolkit refuses rather than warns about, so pointing a legacy token at the
|
|
857
|
+
// current generator is REFUSED. (Repointing the metadata RENDERER is safe and unguarded: a v4
|
|
858
|
+
// renderer on a legacy token just emits no params block.)
|
|
859
|
+
/** The field-renderer address a `--value` names: the canonical `abi.encode(address)` (32 bytes),
|
|
860
|
+
* or a bare 20-byte address. Null when it is neither. */
|
|
861
|
+
function fieldRendererTarget(value) {
|
|
862
|
+
const hex = value.trim();
|
|
863
|
+
if (!/^0x[0-9a-fA-F]*$/.test(hex))
|
|
864
|
+
return null;
|
|
865
|
+
if (hex.length === 42)
|
|
866
|
+
return getAddress(hex);
|
|
867
|
+
if (hex.length !== 66)
|
|
868
|
+
return null;
|
|
869
|
+
try {
|
|
870
|
+
return decodeFieldRenderer(hex);
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
export async function assertGeneratorRepointable(contract, field, flags,
|
|
877
|
+
/** Injected for tests; the real probe is one eth_call. */
|
|
878
|
+
probe = (c) => hasParamEnumeration(makePublicClient({ chainKey: CHAIN }), c)) {
|
|
879
|
+
if (flags.representation !== R.renderer || !flags.value || flags.value === 'true')
|
|
880
|
+
return;
|
|
881
|
+
const target = fieldRendererTarget(flags.value);
|
|
882
|
+
const generator = resolveGenerator(chainId());
|
|
883
|
+
// Only the CANONICAL generator is guarded — any other field renderer is the creator's own contract
|
|
884
|
+
// and none of our business.
|
|
885
|
+
if (!target || !generator || target.toLowerCase() !== generator.toLowerCase())
|
|
886
|
+
return;
|
|
887
|
+
// The `owner` read has already succeeded by the time this runs, so the RPC is proven reachable:
|
|
888
|
+
// a failing probe here means the getter is absent, not that the node is down.
|
|
889
|
+
if (await probe(contract))
|
|
890
|
+
return;
|
|
891
|
+
throw new Error(`refusing to point ${field} at the canonical generator ${generator} — ${contract} does not expose the param\n` +
|
|
892
|
+
` enumeration surface (tokenParamKeys/contractParamKeys), so it is a LEGACY implementation. The current\n` +
|
|
893
|
+
` generator reads params FROM CHAIN, so on this token it would read NOTHING: every configured param would\n` +
|
|
894
|
+
` silently disappear from tokenData, the live view, and every render — with a tokenURI that still looks fine.\n\n` +
|
|
895
|
+
` Two honest options:\n` +
|
|
896
|
+
` • stay on the generator this project already uses (pass that address as --value; it reads the project's\n` +
|
|
897
|
+
` params.keys list, which is how it has always worked here), or\n` +
|
|
898
|
+
` • redeploy the project with the current \`abx deploy-code\` — new projects enumerate on-chain and need no list.`);
|
|
899
|
+
}
|
|
847
900
|
// ── set-field ──────────────────────────────────────────────────────────────--
|
|
848
901
|
// Set an on-chain metadata field. `--field` is what (e.g. image, description),
|
|
849
902
|
// `--representation` is how it's carried (default `inline` for --text, `keccak256`
|
|
@@ -858,6 +911,7 @@ export async function cmdSetField(address, flags) {
|
|
|
858
911
|
const collection = !!flags.collection;
|
|
859
912
|
const lane = laneFromFlags(flags);
|
|
860
913
|
const owner = await read(contract, 'owner');
|
|
914
|
+
await assertGeneratorRepointable(contract, field, flags); // legacy impl + the current generator = params silently invisible
|
|
861
915
|
const staging = !!(flags.file && flags.file !== 'true'); // large content ON-CHAIN via chunk store/reader
|
|
862
916
|
// --file + --dry-run: preview the on-chain staging plan and store/send NOTHING. (The locator/text
|
|
863
917
|
// path flows through runWrite, which previews there; staging must short-circuit before any upload.)
|
|
@@ -987,9 +1041,25 @@ export async function cmdAttach(rest, flags) {
|
|
|
987
1041
|
dim('Point the URI at the file itself (…/master.tiff, …/coa.pdf) so collectors get the right type.'));
|
|
988
1042
|
}
|
|
989
1043
|
console.log(` attaching ${bold(key)} ${dim(`(${mimeType}, ${representation})`)} to ${scope}: ${dim(uri)}`);
|
|
990
|
-
// Where it surfaces
|
|
991
|
-
//
|
|
992
|
-
|
|
1044
|
+
// Where it surfaces. This used to be one dim line, and it read as a footnote rather than as a
|
|
1045
|
+
// dependency: an integrator attached five audio stems to a fully-on-chain token, paid to store
|
|
1046
|
+
// them, and found `tokenURI` listed none of them — "paid for, stored on-chain, and invisible".
|
|
1047
|
+
// The on-chain renderer deliberately omits locator-represented artifacts (they duplicate no
|
|
1048
|
+
// on-chain type information — see specs/protocol/data-plane.md), so the artifacts manifest comes
|
|
1049
|
+
// from a RESOLVER. When the project has no resolver baked in, that is not a footnote, it is the
|
|
1050
|
+
// difference between a feature working and not existing, so say it as a warning.
|
|
1051
|
+
const uriBase = await read(contract, 'tokenURIBase').catch(() => '');
|
|
1052
|
+
const artifactsPath = `/t/${chainId()}/${contract}/${flags.token ?? '0'}`;
|
|
1053
|
+
if (uriBase && uriBase.trim() !== '') {
|
|
1054
|
+
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.`));
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
console.log(yellow(' ⚠ ') +
|
|
1058
|
+
`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 — ` +
|
|
1059
|
+
`so ${bold(key)} will NOT appear in ${bold('tokenURI')}. The bytes are stored and provable, but nothing surfaces them to a marketplace or wallet. ` +
|
|
1060
|
+
dim('(Configured params DO appear on-chain — attachments are the surface that needs a resolver.)'));
|
|
1061
|
+
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}.`));
|
|
1062
|
+
}
|
|
993
1063
|
const buildTx = () => collection
|
|
994
1064
|
? prepareSetContractField({ contract, field: key, representation, value: toHex(uri), chainId: chainId() })
|
|
995
1065
|
: prepareSetTokenField({ contract, tokenId: BigInt(flags.token ?? '0'), field: key, representation, value: toHex(uri), chainId: chainId() });
|
|
@@ -1000,7 +1070,7 @@ export async function cmdAttach(rest, flags) {
|
|
|
1000
1070
|
dim(` verify (a resolver serves the complete listing): `) +
|
|
1001
1071
|
`curl <your-resolver>/t/${chainId()}/${contract}/${id}` +
|
|
1002
1072
|
dim(` ${collection ? '' : `→ artifacts[].key "${key}"; /data/${key} fetches it`}\n`) +
|
|
1003
|
-
dim(` (The complete file listing is a resolver surface — the bare on-chain tokenURI enumerates reserved fields
|
|
1073
|
+
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`));
|
|
1004
1074
|
}
|
|
1005
1075
|
}
|
|
1006
1076
|
// ── lock-field ───────────────────────────────────────────────────────────────
|
|
@@ -1266,4 +1336,129 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1266
1336
|
await runMinterWrite(preparePurchase({ minter, token, to, value, chainId: chainId() }), flags);
|
|
1267
1337
|
console.log(dim(` next: \`abx refresh ${token}\` so marketplaces pick up the new token.`));
|
|
1268
1338
|
}
|
|
1339
|
+
// ── PostParam schemas, after deploy ──────────────────────────────────────────
|
|
1340
|
+
// `setParamSchema` is owner-gated with no deploy-time restriction, so a project's param surface was
|
|
1341
|
+
// never actually frozen at deploy — the toolkit just had no way to reach it, which read to creators
|
|
1342
|
+
// as a protocol limitation ("we must guess every param up front, or redeploy and lose the address").
|
|
1343
|
+
// Two commands close that: `set-schema` (attach or replace one key) and `retire-param` (stop all
|
|
1344
|
+
// further writes, the closest thing to a delete the protocol has).
|
|
1345
|
+
/** Values already stored under a key can be stranded by a schema change — the contract does NOT
|
|
1346
|
+
* re-validate them. Compare old vs new and name what would break, so the guard can refuse. */
|
|
1347
|
+
export function strandingRisks(before, after) {
|
|
1348
|
+
const risks = [];
|
|
1349
|
+
const typeName = (i) => PARAM_TYPES[i] ?? String(i);
|
|
1350
|
+
if (before.paramType !== after.paramType) {
|
|
1351
|
+
risks.push(`type ${typeName(before.paramType)} → ${typeName(after.paramType)} (a stored value keeps its old encoding)`);
|
|
1352
|
+
}
|
|
1353
|
+
const dropped = before.selectOptions.filter((o) => !after.selectOptions.includes(o));
|
|
1354
|
+
if (before.selectOptions.length && dropped.length) {
|
|
1355
|
+
risks.push(`Select option(s) removed: ${dropped.join(', ')} (a token already set to one keeps it)`);
|
|
1356
|
+
}
|
|
1357
|
+
// Narrowing either bound can strand a value that sat inside the old range.
|
|
1358
|
+
const asInt = (h) => (PARAM_TYPES[after.paramType] === 'Int256Range' ? BigInt.asIntN(256, BigInt(h)) : BigInt(h));
|
|
1359
|
+
if (before.min !== after.min && asInt(after.min) > asInt(before.min))
|
|
1360
|
+
risks.push(`min raised (${asInt(before.min)} → ${asInt(after.min)})`);
|
|
1361
|
+
if (before.max !== after.max && asInt(before.max) !== 0n && asInt(after.max) < asInt(before.max)) {
|
|
1362
|
+
risks.push(`max lowered (${asInt(before.max)} → ${asInt(after.max)})`);
|
|
1363
|
+
}
|
|
1364
|
+
return risks;
|
|
1365
|
+
}
|
|
1366
|
+
/** `abx set-schema <address> --schema key:Type:Auth[:lock=<when>]` — attach or replace ONE key's
|
|
1367
|
+
* on-chain schema, any time in a project's life. Owner-only. */
|
|
1368
|
+
export async function cmdSetSchema(address, flags) {
|
|
1369
|
+
const usage = 'abx set-schema <address> --schema key:Type:Auth[:lock=<when>] [--force] [--dry-run] [--sign|--unsigned]';
|
|
1370
|
+
const contract = requireAddress(address, usage);
|
|
1371
|
+
const specs = parseSchemaSpecs(flags.schema);
|
|
1372
|
+
if (specs.length !== 1) {
|
|
1373
|
+
console.error(`usage: ${usage}\n\n One key per call — a schema write is a full-row upsert, so batching them hides which one changed.\n`);
|
|
1374
|
+
process.exit(1);
|
|
1375
|
+
}
|
|
1376
|
+
const next = specs[0];
|
|
1377
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1378
|
+
const owner = await read(contract, 'owner');
|
|
1379
|
+
const before = await readParamSchema(publicClient, contract, next.key);
|
|
1380
|
+
if (!before.exists) {
|
|
1381
|
+
console.log(` ${next.key} ${dim('— new governed key')} ${describeSchema(next)}`);
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
// The upsert hazard: this is a FULL row write, so anything not restated is overwritten. Show
|
|
1385
|
+
// both sides, and refuse a change that could strand values unless the caller insists.
|
|
1386
|
+
console.log(` ${next.key} ${dim('— replacing an existing schema')}`);
|
|
1387
|
+
console.log(` ${dim('before')} ${describeSchema(onChainToParsed(next.key, before))}`);
|
|
1388
|
+
console.log(` ${dim('after ')} ${describeSchema(next)}`);
|
|
1389
|
+
const risks = strandingRisks(before, next);
|
|
1390
|
+
if (risks.length && flags.force === undefined) {
|
|
1391
|
+
throw new Error(`refusing to replace "${next.key}" — this change can strand values already stored under it:\n` +
|
|
1392
|
+
risks.map((r) => ` • ${r}`).join('\n') +
|
|
1393
|
+
`\n\n The contract does NOT re-validate stored values against a new schema, so affected tokens would keep\n` +
|
|
1394
|
+
` values their own schema no longer allows. Re-run with --force if that is what you intend.`);
|
|
1395
|
+
}
|
|
1396
|
+
// With --force, say what is being overridden. Silently applying a value-stranding change is the
|
|
1397
|
+
// one outcome worse than refusing it: the operator gets no record of which tokens they may have
|
|
1398
|
+
// just invalidated, and neither does anyone reading the terminal afterwards.
|
|
1399
|
+
if (risks.length) {
|
|
1400
|
+
console.log(` ${C.yellow}⚠${C.reset} ${bold('--force')} — applying a change that can strand stored values:`);
|
|
1401
|
+
for (const r of risks)
|
|
1402
|
+
console.log(` • ${r}`);
|
|
1403
|
+
console.log(dim(` any token already holding a value for "${next.key}" keeps it, now outside what its schema allows.`));
|
|
1404
|
+
}
|
|
1405
|
+
if (before.lockAfter && !next.lockAfter) {
|
|
1406
|
+
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.`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
// ONE op. A schema write used to need a `params.keys` companion write in the same tx to keep the
|
|
1410
|
+
// on-chain generator's key list in step; the generator now enumerates params from the token
|
|
1411
|
+
// itself, so the schema write is the whole change.
|
|
1412
|
+
await runWrite(contract, prepareSetParamSchema({
|
|
1413
|
+
contract,
|
|
1414
|
+
key: next.key,
|
|
1415
|
+
paramType: next.paramType,
|
|
1416
|
+
auth: next.auth,
|
|
1417
|
+
authAddress: next.authAddress,
|
|
1418
|
+
lockAfter: next.lockAfter,
|
|
1419
|
+
min: next.min,
|
|
1420
|
+
max: next.max,
|
|
1421
|
+
selectOptions: next.selectOptions,
|
|
1422
|
+
chainId: chainId(),
|
|
1423
|
+
display: describeSchema(next),
|
|
1424
|
+
}), flags, owner);
|
|
1425
|
+
}
|
|
1426
|
+
/** Render an on-chain schema through the same formatter the CLI uses for a parsed one. */
|
|
1427
|
+
function onChainToParsed(key, s) {
|
|
1428
|
+
return {
|
|
1429
|
+
key,
|
|
1430
|
+
paramType: s.paramType,
|
|
1431
|
+
auth: s.auth,
|
|
1432
|
+
authAddress: s.authAddress,
|
|
1433
|
+
lockAfter: s.lockAfter,
|
|
1434
|
+
min: s.min,
|
|
1435
|
+
max: s.max,
|
|
1436
|
+
selectOptions: s.selectOptions,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
/** `abx retire-param <address> <key>` — permanently stop further writes to a PostParam. */
|
|
1440
|
+
export async function cmdRetireParam(address, rest, flags) {
|
|
1441
|
+
const usage = 'abx retire-param <address> <key> [--dry-run] [--sign|--unsigned]';
|
|
1442
|
+
const contract = requireAddress(address, usage);
|
|
1443
|
+
const [key] = positionalArgs(rest);
|
|
1444
|
+
if (!key) {
|
|
1445
|
+
console.error(`usage: ${usage}\n`);
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1449
|
+
const owner = await read(contract, 'owner');
|
|
1450
|
+
const current = await readParamSchema(publicClient, contract, key);
|
|
1451
|
+
if (!current.exists)
|
|
1452
|
+
throw new Error(`no PostParam schema for "${key}" on ${contract} — nothing to retire.`);
|
|
1453
|
+
if (current.lockAfter && current.lockAfter < Math.floor(Date.now() / 1000)) {
|
|
1454
|
+
console.log(` ${C.green}✓${C.reset} "${key}" is already retired (locked ${new Date(current.lockAfter * 1000).toISOString().slice(0, 19)}Z). Nothing to do.`);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
console.log(` ${key} ${dim(describeSchema(onChainToParsed(key, current)))}`);
|
|
1458
|
+
console.log(` ${dim('after this: every write reverts ParamLockExpired — permanently, with no way back.')}`);
|
|
1459
|
+
console.log(` ${dim('the schema stays (a key can never be un-governed), and any value ALREADY stored stays and keeps serving.')}`);
|
|
1460
|
+
// Read-modify-write: carry every field forward and change only the lock. Composing a fresh schema
|
|
1461
|
+
// here would silently reset type/auth/bounds/options — the upsert clobber this command exists to avoid.
|
|
1462
|
+
await runWrite(contract, prepareRetireParam({ contract, key, current, chainId: chainId() }), flags, owner);
|
|
1463
|
+
}
|
|
1269
1464
|
//# sourceMappingURL=ownerops.js.map
|