@artblocks/abx-cli 0.1.0-alpha.20 → 0.1.0-alpha.22
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/CHANGELOG.md +1081 -0
- package/assets/renderer-scaffold/README.md +2 -2
- package/assets/renderer-scaffold/src/MyRenderer.sol +2 -2
- package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +1 -1
- package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +2 -2
- package/assets/renderer-scaffold/test/MyRenderer.t.sol +1 -1
- package/dist/commands/deploy.d.ts +30 -17
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +523 -117
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/project.d.ts +16 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +171 -15
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +35 -5
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts +3 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +81 -29
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/service.js +1 -1
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +11 -3
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/submit-app.d.ts +58 -0
- package/dist/commands/submit-app.d.ts.map +1 -0
- package/dist/commands/submit-app.js +512 -0
- package/dist/commands/submit-app.js.map +1 -0
- package/dist/config.d.ts +1 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/flag-allowlists.d.ts +53 -0
- package/dist/flag-allowlists.d.ts.map +1 -0
- package/dist/flag-allowlists.js +152 -0
- package/dist/flag-allowlists.js.map +1 -0
- package/dist/flags.d.ts +6 -2
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +23 -0
- package/dist/flags.js.map +1 -1
- package/dist/kind.d.ts +8 -3
- package/dist/kind.d.ts.map +1 -1
- package/dist/kind.js +25 -0
- package/dist/kind.js.map +1 -1
- package/dist/main.js +166 -53
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +29 -4
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +32 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +74 -9
- package/dist/output.js.map +1 -1
- package/dist/ownerops.d.ts +101 -13
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +421 -71
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +8 -2
- package/dist/remote.js.map +1 -1
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +37 -2
- package/dist/schema.js.map +1 -1
- package/dist/served.js +1 -1
- package/dist/update-check.d.ts +25 -0
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +49 -9
- package/dist/update-check.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +64 -32
- package/skill/reference/code-projects.md +98 -22
- package/skill/reference/creator-token.md +16 -3
- package/skill/reference/decisions.md +83 -11
- package/skill/reference/hosting.md +1 -1
- package/skill/reference/operating.md +35 -12
- package/skill/reference/setup.md +1 -1
- package/skill/reference/troubleshooting.md +16 -6
package/dist/ownerops.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
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, ensureChunkStore as sdkEnsureChunkStore, predictFixedPriceMinter, predictFixedPriceMinter1155, makeHotSender, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneEditionAbi, seriesImageAbi, deployFixedPriceMinter, deployFixedPriceMinter1155, prepareConfigureSale, prepareConfigureSale1155, preparePurchase, preparePurchase1155, readSaleConfig, readSaleConfig1155, prepareEditionMint, prepareEditionTransfer, prepareSetMaxSupply, preparePingURI, prepareLockContractField, prepareLockContractURI, prepareLockTokenField, prepareLockTokenURI, prepareMint, mintedTokenIds, prepareSeriesMintMany, prepareSetMinter, prepareSetMaxInvocations, prepareSetPrimaryPayee, prepareSetPaused, prepareSetContractField, prepareSetContractURIBase, prepareSetContractURIOverride, prepareSetContractURIRenderer, prepareSetParamHooks, prepareSetParamSchema, prepareRetireParam, readParamSchema, PARAM_TYPES, prepareSetRoyalty, prepareSetTokenField, batchOps, prepareSetTokenURIBase, prepareSetTokenURIOverride, prepareSetTokenURIRenderer, prepareSetTransferValidator, prepareTransfer, prepareTransferOwnership, readCreatorTokenStatus, resolveRecommendedTransferValidator, RECOMMENDED_TRANSFER_VALIDATOR, KNOWN_CHAIN_KEYS, resolveChain, resolveRpcUrl, redactRpcUrl, encodeTag, METADATA_FIELD as F, METADATA_REPRESENTATION as R, stageFieldContent, planStagedContent, exceedsOnchainSoftLimit, ONCHAIN_IMAGE_SOFT_LIMIT, ONCHAIN_PROJECT_SOFT_LIMIT, } from '@artblocks/abx-sdk';
|
|
9
|
+
import { assertChainId, ensureChunkStore as sdkEnsureChunkStore, predictFixedPriceMinter, predictFixedPriceMinter1155, makeHotSender, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneEditionAbi, seriesImageAbi, deployFixedPriceMinter, deployFixedPriceMinter1155, prepareConfigureSale, prepareConfigureSale1155, preparePurchase, preparePurchase1155, readSaleConfig, readSaleConfig1155, prepareEditionMint, prepareEditionTransfer, prepareSetMaxSupply, preparePingURI, prepareLockContractField, prepareLockContractURI, prepareLockTokenField, prepareLockTokenURI, prepareMint, mintedTokenIds, prepareSeriesMintMany, prepareSetMinter, prepareSetMaxInvocations, prepareSetPrimaryPayee, prepareSetPaused, prepareSetContractField, prepareSetContractURIBase, prepareSetContractURIOverride, prepareSetContractURIRenderer, prepareSetParamHooks, prepareSetParamSchema, prepareRetireParam, readParamSchema, PARAM_TYPES, prepareSetRoyalty, prepareSetTokenField, batchOps, prepareSetTokenURIBase, prepareSetTokenURIOverride, prepareSetTokenURIRenderer, prepareSetTransferValidator, prepareSetSeedSource, probeSeedSource, readSeedSource, predictSeedSource, getDeployment, prepareTransfer, prepareTransferOwnership, probeTransferValidator, readCreatorTokenStatus, resolveRecommendedTransferValidator, RECOMMENDED_TRANSFER_VALIDATOR, KNOWN_CHAIN_KEYS, resolveChain, resolveRpcUrl, redactRpcUrl, encodeTag, METADATA_FIELD as F, METADATA_REPRESENTATION as R, stageFieldContent, planStagedContent, exceedsOnchainSoftLimit, classifyOnchainReadSize, tokenUriGasEstimate, ONCHAIN_IMAGE_SOFT_LIMIT, ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, ONCHAIN_READ_REFUSE_BYTES, } from '@artblocks/abx-sdk';
|
|
10
10
|
import { encodeScalarParam, encodeTag as encodeTagSdk, isAccepted, prepareConfigureTokenParam, prepareConfigureTokenParamData, prepareSetContractParam, prepareSetContractParamData, seriesCodeAbi, tryReadContract, } from '@artblocks/abx-sdk';
|
|
11
11
|
import { hasParamEnumeration } from '@artblocks/abx-sdk';
|
|
12
|
-
import { decodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
12
|
+
import { decodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareLockParamHooks, prepareLockScript, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
13
13
|
import { contentTypeFromPath } from '@artblocks/abx-storage';
|
|
14
14
|
import { toHex as toHexSdk } from 'viem';
|
|
15
15
|
import { readFileSync } from 'node:fs';
|
|
@@ -23,12 +23,12 @@ import { openWalletSession } from './signer.js';
|
|
|
23
23
|
import { gatedSend, laneFromFlags } from './riskgate.js';
|
|
24
24
|
import { withJson } from './jsonout.js';
|
|
25
25
|
import { resolveRemote, serviceClient } from './remote.js';
|
|
26
|
-
import { isDryRun, positionalArgs, unknownFlags } from './flags.js';
|
|
26
|
+
import { allowUnreadableOnchain, isDryRun, positionalArgs, unknownFlags, warnStrayFlags } from './flags.js';
|
|
27
27
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
28
28
|
// Re-exported for main.ts (the on-chain-vs-off-chain cost model + the compression-mode type now
|
|
29
29
|
// live in the SDK's staging.ts, layered on planChunks/planContentTxs — see the comment at their
|
|
30
30
|
// definition below).
|
|
31
|
-
export { ONCHAIN_PROJECT_SOFT_LIMIT };
|
|
31
|
+
export { ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, ONCHAIN_READ_REFUSE_BYTES, tokenUriGasEstimate };
|
|
32
32
|
// ── ANSI (local) ─────────────────────────────────────────────────────────────
|
|
33
33
|
const C = { reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[38;5;115m', yellow: '\x1b[38;5;221m' };
|
|
34
34
|
const dim = (s) => `${C.dim}${s}${C.reset}`;
|
|
@@ -40,16 +40,17 @@ const bold = (s) => `${C.bold}${s}${C.reset}`;
|
|
|
40
40
|
// COMPLETE set — but it's COMPUTED by the resolver/renderer from the token's fields + effect
|
|
41
41
|
// outputs; it is never a field you set. These keys are that computed output, so setting them by
|
|
42
42
|
// hand would only pollute the manifest with a bogus entry (a real round-1 agent trap). Refuse them
|
|
43
|
-
// and point at the real verb. `abx_params`
|
|
44
|
-
//
|
|
45
|
-
//
|
|
43
|
+
// and point at the real verb. `abx_params` stays in this set even though spec v8 removed the
|
|
44
|
+
// projection from `tokenURI`: the name is now free, which makes it MORE attractive as a decoy field
|
|
45
|
+
// and no more meaningful than before. Params are chain state; a field of that name would be a
|
|
46
|
+
// hand-set impostor of a read that lives on the contract. See specs/protocol/data-plane.md + specs/protocol/onchain-metadata.md.
|
|
46
47
|
const COMPUTED_FIELD_KEYS = new Set(['artifacts', 'abx_params', 'abx_provenance']);
|
|
47
48
|
export function assertSettableField(field) {
|
|
48
49
|
if (COMPUTED_FIELD_KEYS.has(field)) {
|
|
49
50
|
const what = field === 'artifacts'
|
|
50
51
|
? "the COMPUTED manifest (the complete list of this token's files), assembled by the resolver/renderer from your fields"
|
|
51
52
|
: field === 'abx_params'
|
|
52
|
-
? "
|
|
53
|
+
? "a reserved name, not a field — params are chain state read straight off the contract (set one with `abx configure-param`, declare one with `abx set-schema`)"
|
|
53
54
|
: 'the COMPUTED provenance list, assembled by the resolver/renderer';
|
|
54
55
|
throw new Error(`"${field}" is not a field you set — it's ${what}.` +
|
|
55
56
|
(field === 'abx_params'
|
|
@@ -64,6 +65,28 @@ function fitsLiteralBytes32(value) {
|
|
|
64
65
|
}
|
|
65
66
|
/** Flags `abx attach` recognizes — anything else warns (non-fatal), so a silent no-op flag surfaces. */
|
|
66
67
|
const ATTACH_FLAGS = ['file', 'compress', 'collection', 'token', 'send', 'sign', 'unsigned', 'yes', 'dry-run', 'confirm', 'port', 'sign-url-file', 'remote'];
|
|
68
|
+
/**
|
|
69
|
+
* The flags EVERY owner write accepts, regardless of command — the signing lane (`laneFromFlags`),
|
|
70
|
+
* the opt-in confirm gate, the wallet-lane plumbing, the post-write reindex nudge, and `--json`.
|
|
71
|
+
* Factored out so a per-command allowlist below only has to name that command's OWN flags.
|
|
72
|
+
*/
|
|
73
|
+
const SHARED_WRITE_FLAGS = ['send', 'sign', 'unsigned', 'dry-run', 'yes', 'confirm', 'port', 'sign-url-file', 'remote', 'remote-token', 'json'];
|
|
74
|
+
/**
|
|
75
|
+
* Per-command allowlists for the owner ops where a silently-ignored flag changes MONEY or SUPPLY.
|
|
76
|
+
*
|
|
77
|
+
* These commands took no stray-flag notice at all, and the ERC-1155 lane made that expensive: an
|
|
78
|
+
* edition's semantics live in its OPTIONAL flags, which default rather than fail. A typo'd
|
|
79
|
+
* `--amount 50` minted 1 copy; a typo'd `--quantity 5` bought 1 and paid 1×; and nothing said a word.
|
|
80
|
+
* (`deploy` refuses strays outright, but its allowlists are exhaustively maintained. Here we WARN —
|
|
81
|
+
* per `unknownFlags`' own contract and the owner's scriptability line: a false warning on a valid
|
|
82
|
+
* flag must never break someone's script, and a warning already ends the silence.)
|
|
83
|
+
*/
|
|
84
|
+
const MINT_FLAGS = [...SHARED_WRITE_FLAGS, 'to', 'count', 'token-id', 'amount'];
|
|
85
|
+
const TRANSFER_FLAGS = [...SHARED_WRITE_FLAGS, 'to', 'token', 'token-id', 'amount', 'from'];
|
|
86
|
+
const SET_MAX_SUPPLY_FLAGS = [...SHARED_WRITE_FLAGS, 'token-id', 'cap'];
|
|
87
|
+
const MINTER_CONFIGURE_FLAGS = [...SHARED_WRITE_FLAGS, 'price', 'price-raw', 'allocation', 'erc20', 'token-id', 'minter-contract'];
|
|
88
|
+
const MINTER_SHOW_FLAGS = [...SHARED_WRITE_FLAGS, 'token-id', 'minter-contract'];
|
|
89
|
+
const MINTER_BUY_FLAGS = [...SHARED_WRITE_FLAGS, 'to', 'token-id', 'quantity', 'minter-contract'];
|
|
67
90
|
/** Auto-detect the on-chain representation for an off-chain locator by its URI scheme. Returns null
|
|
68
91
|
* for anything that isn't a recognized durable/http locator (so `attach` can refuse it loudly
|
|
69
92
|
* rather than silently storing a bad value — the round-1 `--representation` guessing trap). */
|
|
@@ -196,7 +219,7 @@ export async function assertContractExists(address) {
|
|
|
196
219
|
* and canonically ENCODES the human input per its type (`#rrggbb`, decimals ×1e10,
|
|
197
220
|
* Select by label, …); `String`/`Bytes` schemas take the value as UTF-8 (or
|
|
198
221
|
* `--file <path>` for bytes) via the data path. The signer must satisfy the schema's
|
|
199
|
-
* auth (
|
|
222
|
+
* auth (Creator = contract owner, TokenOwner — delegate.xyz honored — or the named
|
|
200
223
|
* address); the chain enforces it either way.
|
|
201
224
|
*/
|
|
202
225
|
/** Positional args only — drops `--flags` AND the single token each value-taking flag consumes
|
|
@@ -214,7 +237,7 @@ export async function assertContractExists(address) {
|
|
|
214
237
|
* Nothing errored; the in-chain renderer read ASCII where it expected bytes and drew garbage. So a
|
|
215
238
|
* `Bytes` value must state its encoding: `0x…` hex, or `--file` for real binary. A bare string is
|
|
216
239
|
* refused rather than guessed at — there is no safe guess between "these characters" and "these
|
|
217
|
-
* bytes", and the failure is invisible until
|
|
240
|
+
* bytes", and the failure is invisible until a work renders wrong.
|
|
218
241
|
*/
|
|
219
242
|
export function encodePayloadParam(typeName, valueInput, key) {
|
|
220
243
|
if (typeName === 'String')
|
|
@@ -331,7 +354,7 @@ const HOOK_ROLES = ['configure', 'augment', 'transfer'];
|
|
|
331
354
|
const HOOK_NOTE = {
|
|
332
355
|
configure: 'write-time veto/validator (a configure tx reverts if it reverts)',
|
|
333
356
|
augment: 'read-time derivation folded into tokenData (live view reads chain per view)',
|
|
334
|
-
transfer: 'ownership-change
|
|
357
|
+
transfer: 'ownership-change VETO — its revert fails the transfer, and a mint too (mint = transfer from 0x0)',
|
|
335
358
|
};
|
|
336
359
|
const eqAddr = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
337
360
|
/** Parse a hook-address flag: a 0x address, or `none`/`zero`/`0`/`0x0` to clear that role. */
|
|
@@ -409,6 +432,13 @@ export async function cmdSetParamHooks(address, flags) {
|
|
|
409
432
|
const changed = !eqAddr(cur[r], next[r]);
|
|
410
433
|
console.log(` ${r.padEnd(10)} ${changed ? `${label(cur[r])} → ${label(next[r])}` : `${label(cur[r])} ${dim('(unchanged)')}`}`);
|
|
411
434
|
}
|
|
435
|
+
// Arming a transfer hook is the one hook change with a consequence for COLLECTORS, so say it at
|
|
436
|
+
// the moment of arming rather than only in the docs. It is not a warning against doing it — an
|
|
437
|
+
// owner-dependent work needs it — it is the disclosure that comes with it.
|
|
438
|
+
if (!eqAddr(next.transfer, zeroAddress) && !eqAddr(next.transfer, cur.transfer)) {
|
|
439
|
+
console.log(` ${bold('the transfer hook is a VETO:')} if it reverts, the transfer fails. It also runs on mint and burn (mint = transfer from 0x0), so a reverting hook stops minting for this project too — including through the shared minter.`);
|
|
440
|
+
console.log(dim(` that is a standing power over whether a collector can sell. Disclose it, and if you want to prove you will never arm one, \`abx lock-param-hooks ${contract}\` freezes all three addresses forever.`));
|
|
441
|
+
}
|
|
412
442
|
const owner = await read(contract, 'owner');
|
|
413
443
|
await runWrite(contract, prepareSetParamHooks({ contract, configureHook: next.configure, augmentHook: next.augment, transferHook: next.transfer, chainId: chainId() }), flags, owner);
|
|
414
444
|
}
|
|
@@ -469,8 +499,56 @@ export async function cmdLockDependencies(address, flags) {
|
|
|
469
499
|
const contract = requireAddress(address, 'abx lock-dependencies <address> [--sign|--unsigned]');
|
|
470
500
|
const owner = await read(contract, 'owner');
|
|
471
501
|
console.log(dim(' note: locking the dependency set is permanent and irreversible (list AND registry pointer freeze).'));
|
|
502
|
+
console.log(dim(" this pins WHICH library each ref means, not the library's bytes: a Registry ref is fetched from the registry on every read, so those bytes stay in the registry owner's hands."));
|
|
472
503
|
await runWrite(contract, prepareLockDependencies({ contract, chainId: chainId() }), flags, owner);
|
|
473
504
|
}
|
|
505
|
+
/** `abx lock-script <address>` — freeze the on-chain program (script chunks) forever. This is the
|
|
506
|
+
* lock that actually freezes the WORK of a code project: after it, `setScriptChunk` /
|
|
507
|
+
* `removeLastScriptChunk` revert. `lock-field`/`lock-uri` only freeze metadata; the full set for a
|
|
508
|
+
* code drop is lock-script + lock-dependencies + lock-field/lock-uri — which freezes what THIS
|
|
509
|
+
* contract stores, not necessarily what the token renders (params have no lock, and a Registry
|
|
510
|
+
* dependency's bytes live in the registry). */
|
|
511
|
+
export async function cmdLockScript(address, flags) {
|
|
512
|
+
const contract = requireAddress(address, 'abx lock-script <address> [--sign|--unsigned]');
|
|
513
|
+
const owner = await read(contract, 'owner');
|
|
514
|
+
console.log(dim(' note: locking the script is permanent and irreversible (the program bytes can never change again).'));
|
|
515
|
+
console.log(dim(' this freezes the WORK — pair with lock-dependencies and lock-field/lock-uri to freeze everything this contract stores.'));
|
|
516
|
+
console.log(dim(' that is not the same as a frozen OUTPUT: params stay writable and a Registry dependency is re-fetched live, so say "locked metadata" to a buyer, not "immutable".'));
|
|
517
|
+
await runWrite(contract, prepareLockScript({ contract, chainId: chainId() }), flags, owner);
|
|
518
|
+
}
|
|
519
|
+
/** `abx lock-param-hooks <address>` — freeze the three param-lifecycle hook addresses forever.
|
|
520
|
+
* The sibling of lock-script/lock-dependencies/lock-uri, and the only one aimed at a BUYER rather
|
|
521
|
+
* than at metadata: the transfer hook is a veto over transfers and mints, so an unlocked hook set
|
|
522
|
+
* is a standing power over whether a collector can sell. After this, `set-param-hooks` reverts
|
|
523
|
+
* ParamHooksLocked. Owner-only, any lane, guards --dry-run. */
|
|
524
|
+
export async function cmdLockParamHooks(address, flags) {
|
|
525
|
+
const contract = requireAddress(address, 'abx lock-param-hooks <address> [--sign|--unsigned] [--dry-run]');
|
|
526
|
+
// Read the current trio first: it is the SeriesCode/EditionCode guard (paramHooks() exists only on
|
|
527
|
+
// the ConfigurableParams extension) AND the thing being frozen — nobody should sign a permanent
|
|
528
|
+
// freeze without seeing exactly what it freezes.
|
|
529
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
530
|
+
let current;
|
|
531
|
+
try {
|
|
532
|
+
current = (await publicClient.readContract({ address: contract, abi: seriesCodeAbi, functionName: 'paramHooks' }));
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
await assertContractExists(contract);
|
|
536
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
537
|
+
throw new Error(`${contract} exposes no param hooks — they are a SeriesCode/EditionCode (PostParams) feature, ` +
|
|
538
|
+
`so there is nothing to freeze. ${kind.label} has no configurable params to hook.`);
|
|
539
|
+
}
|
|
540
|
+
const cur = { configure: current[0], augment: current[1], transfer: current[2] };
|
|
541
|
+
const label = (a) => (eqAddr(a, zeroAddress) ? dim('none') : a);
|
|
542
|
+
console.log(` freezing these three addresses on ${contract}, permanently:`);
|
|
543
|
+
for (const r of HOOK_ROLES)
|
|
544
|
+
console.log(` ${r.padEnd(10)} ${label(cur[r])}`);
|
|
545
|
+
console.log(dim(' note: this is permanent and irreversible — no hook address can ever be set, re-pointed, or cleared again.'));
|
|
546
|
+
console.log(dim(` you are giving up: arming a transfer veto (a hook that can block transfers and mints), arming a write-time configure veto, and re-pointing or clearing the augment hook. ` +
|
|
547
|
+
`${eqAddr(cur.transfer, zeroAddress) ? 'With no transfer hook set, freezing is how you PROVE you can never add one — the guarantee a buyer can check.' : 'The transfer hook already set stays live and keeps its veto: freezing the set is not disarming what is in it.'}`));
|
|
548
|
+
console.log(dim(' this freezes only the hooks. Schemas, param values, script, dependencies and URIs keep their own locks (`abx verify` lists them).'));
|
|
549
|
+
const owner = await read(contract, 'owner');
|
|
550
|
+
await runWrite(contract, prepareLockParamHooks({ contract, chainId: chainId() }), flags, owner);
|
|
551
|
+
}
|
|
474
552
|
// ── editions: shared small parsers ────────────────────────────────────────────
|
|
475
553
|
/** A non-negative-integer edition flag (`--token-id`, `--amount`, …) — the shared parse +
|
|
476
554
|
* bound-check so every edition command that takes "an id" or "a count" rejects the same way. */
|
|
@@ -514,6 +592,7 @@ export function parseTokenIdRange(raw) {
|
|
|
514
592
|
export async function cmdMint(address, flags) {
|
|
515
593
|
const usage = 'abx mint <address> [--to 0x…] [--count <n> | --token-id <id> --amount <n>] [--json] [--sign|--unsigned]';
|
|
516
594
|
const contract = requireAddress(address, usage);
|
|
595
|
+
warnStrayFlags(flags, new Set(MINT_FLAGS), 'mint');
|
|
517
596
|
// `--json`: the TOKEN ID is the value a program came for, and it was previously only obtainable by
|
|
518
597
|
// regex-scraping coloured prose (B19). It is read from the mint's own Transfer logs rather than by
|
|
519
598
|
// re-reading `nextTokenId` afterwards — a concurrent mint would make that answer wrong, and a
|
|
@@ -531,9 +610,9 @@ export async function cmdMint(address, flags) {
|
|
|
531
610
|
throw new Error(`--count is a 721 Series flag (mint N tokens in order) — ${contract} is a ${kind.label} (edition). Use --token-id <id> --amount <n> instead.`);
|
|
532
611
|
}
|
|
533
612
|
// OneOfOneEdition's id space is fixed to {0}, so --token-id defaults there; EditionImage/
|
|
534
|
-
// EditionCode ids are caller-named
|
|
613
|
+
// EditionCode ids are caller-named works with no sensible default — require it.
|
|
535
614
|
if (kind.kind !== '1of1-edition' && flags['token-id'] === undefined) {
|
|
536
|
-
throw new Error(`--token-id is required on a ${kind.label} — its ids are caller-named
|
|
615
|
+
throw new Error(`--token-id is required on a ${kind.label} — its ids are caller-named works, not a single default. ` +
|
|
537
616
|
`See existing ids with \`abx tokens ${contract}\`.`);
|
|
538
617
|
}
|
|
539
618
|
const tokenId = flags['token-id'] !== undefined ? parseEditionCountFlag(flags['token-id'], 'token-id') : 0n;
|
|
@@ -612,6 +691,17 @@ export async function cmdSetMaxInvocations(address, flags) {
|
|
|
612
691
|
const usage = 'abx set-max-invocations <address> --max <N> [--sign|--unsigned]';
|
|
613
692
|
const contract = requireAddress(address, usage);
|
|
614
693
|
const max = BigInt(requireFlag(flags, 'max', usage));
|
|
694
|
+
// The cap only ever decreases, but `--dry-run` used to print "Set max invocations → 20" and state
|
|
695
|
+
// the new value as fact for a contract capped at 16 — then the real send reverted. A creator (or an
|
|
696
|
+
// agent) reads a clean dry run as permission to send, so the preview has to know what the chain
|
|
697
|
+
// knows. Best-effort: an unreadable getter does NOT block, since the chain enforces it regardless.
|
|
698
|
+
// `readSeries`, not `read`: the 1/1 ABI that `read` uses has no `maxInvocations` at all, so reading
|
|
699
|
+
// through it always threw and this guard silently never fired.
|
|
700
|
+
const capNow = await readSeries(contract, 'maxInvocations').catch(() => null);
|
|
701
|
+
if (capNow !== null && max > capNow) {
|
|
702
|
+
throw new Error(`${contract}'s cap is already ${capNow} and maxInvocations can only DECREASE — ${max} would raise it, which the chain refuses. ` +
|
|
703
|
+
`Pass a number at or below ${capNow} (a supply cap is one-way by design: it can be tightened, never reopened).`);
|
|
704
|
+
}
|
|
615
705
|
const owner = await read(contract, 'owner');
|
|
616
706
|
await runWrite(contract, prepareSetMaxInvocations({ contract, maxInvocations: max, chainId: chainId() }), flags, owner);
|
|
617
707
|
}
|
|
@@ -627,6 +717,7 @@ export async function cmdSetMaxInvocations(address, flags) {
|
|
|
627
717
|
export async function cmdSetMaxSupply(address, flags) {
|
|
628
718
|
const usage = 'abx set-max-supply <address> --token-id <n> --cap <n> [--sign|--unsigned]';
|
|
629
719
|
const contract = requireAddress(address, usage);
|
|
720
|
+
warnStrayFlags(flags, new Set(SET_MAX_SUPPLY_FLAGS), 'set-max-supply');
|
|
630
721
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
631
722
|
const kind = await detectTokenKind(publicClient, contract);
|
|
632
723
|
if (!kind.isEdition) {
|
|
@@ -640,11 +731,29 @@ export async function cmdSetMaxSupply(address, flags) {
|
|
|
640
731
|
`(the --copies default from deploy) — there's nothing to set. To lower an existing cap, pass the number.`);
|
|
641
732
|
}
|
|
642
733
|
const cap = parseEditionCountFlag(capRaw, 'cap');
|
|
734
|
+
// Both ways this reverts on chain were reachable through `--dry-run` unchanged: the preview printed
|
|
735
|
+
// "#0 supply cap → 50" and stated the new cap as fact, then the real send reverted. A dry run that
|
|
736
|
+
// green-lights an impossible write is worse than no dry run — the creator's next step is to send it.
|
|
737
|
+
// `maxSupply(id) === 0` reads as "open" (the un-overridden --copies default), so any finite cap is a
|
|
738
|
+
// decrease from open and allowed. Reads are best-effort: if either read fails we do NOT block, since
|
|
739
|
+
// the chain still enforces the invariant and a guard must not turn an RPC miss into a refusal.
|
|
740
|
+
const [capNow, supplyNow] = await Promise.all([
|
|
741
|
+
readEdition(contract, 'maxSupply', [tokenId]).catch(() => null),
|
|
742
|
+
readEdition(contract, 'totalSupply', [tokenId]).catch(() => null),
|
|
743
|
+
]);
|
|
744
|
+
if (capNow !== null && capNow > 0n && cap > capNow) {
|
|
745
|
+
throw new Error(`#${tokenId}'s cap is already ${capNow} and a cap can only DECREASE — ${cap} would raise it, which the chain refuses. ` +
|
|
746
|
+
`Pass a number at or below ${capNow}${supplyNow !== null ? ` and at or above its ${supplyNow} live cop${supplyNow === 1n ? 'y' : 'ies'}` : ''}.`);
|
|
747
|
+
}
|
|
748
|
+
if (supplyNow !== null && cap < supplyNow) {
|
|
749
|
+
throw new Error(`#${tokenId} already has ${supplyNow} cop${supplyNow === 1n ? 'y' : 'ies'} minted, so a cap of ${cap} would sit BELOW live supply — the chain refuses that. ` +
|
|
750
|
+
`The lowest cap you can set is ${supplyNow} (which closes the id to further minting).`);
|
|
751
|
+
}
|
|
643
752
|
const owner = await read(contract, 'owner');
|
|
644
753
|
console.log(dim(` #${tokenId} supply cap → ${cap}`));
|
|
645
754
|
await runWrite(contract, prepareSetMaxSupply({ contract, tokenId, cap, chainId: chainId() }), flags, owner);
|
|
646
755
|
}
|
|
647
|
-
// ── ping-uri (editions only) —
|
|
756
|
+
// ── ping-uri (editions only) — the OWNER's URI re-emission ───────────────────────────────────────
|
|
648
757
|
/** How many ids one `pingURI` call carries per transaction — large enough that a normal collection
|
|
649
758
|
* fits in one tx, small enough that a genuinely huge id list doesn't build one unbounded call. */
|
|
650
759
|
const PING_URI_CHUNK_SIZE = 200;
|
|
@@ -663,13 +772,14 @@ export async function cmdPingUri(address, flags) {
|
|
|
663
772
|
const kind = await detectTokenKind(publicClient, contract);
|
|
664
773
|
if (!kind.isEdition) {
|
|
665
774
|
throw new Error(`ping-uri is edition-only — it re-emits the ERC-1155 URI event, which a ${kind.label} (721) doesn't have ` +
|
|
666
|
-
`(
|
|
775
|
+
`(both lanes emit ERC-4906 automatically on a URI change; this command is for indexers that ` +
|
|
776
|
+
`honor only the native 1155 event).`);
|
|
667
777
|
}
|
|
668
778
|
const ids = parseTokenIdRange(requireFlag(flags, 'token-ids', usage));
|
|
669
779
|
const chunks = [];
|
|
670
780
|
for (let i = 0; i < ids.length; i += PING_URI_CHUNK_SIZE)
|
|
671
781
|
chunks.push(ids.slice(i, i + PING_URI_CHUNK_SIZE));
|
|
672
|
-
console.log(dim(` re-emitting URI for ${ids.length} id${ids.length === 1 ? '' : 's'} —
|
|
782
|
+
console.log(dim(` re-emitting URI for ${ids.length} id${ids.length === 1 ? '' : 's'} — owner-only — ${chunks.length} tx${chunks.length === 1 ? '' : '(s)'} of up to ${PING_URI_CHUNK_SIZE} id(s) each`));
|
|
673
783
|
for (const [i, chunk] of chunks.entries()) {
|
|
674
784
|
if (chunks.length > 1)
|
|
675
785
|
console.log(dim(` chunk ${i + 1}/${chunks.length}: ${chunk.length} id(s) (#${chunk[0]}–#${chunk[chunk.length - 1]})`));
|
|
@@ -695,9 +805,9 @@ export async function cmdUnpause(address, flags) {
|
|
|
695
805
|
* scope, so one value covers the whole project — they project into `contractURI` on the resolver
|
|
696
806
|
* AND the on-chain renderer. Works for every token type (1/1 · Series · code). */
|
|
697
807
|
export const AUTHORSHIP_DEPLOY_FIELDS = [
|
|
698
|
-
['
|
|
808
|
+
['creator', F.creator],
|
|
699
809
|
['display-notes', F.displayNotes],
|
|
700
|
-
['
|
|
810
|
+
['creator-links', F.creatorLinks],
|
|
701
811
|
['license', F.license],
|
|
702
812
|
];
|
|
703
813
|
/** Build the on-chain (inline) collection fields for whichever authorship/rights deploy flags are
|
|
@@ -763,14 +873,29 @@ export async function cmdRefresh(address, flags) {
|
|
|
763
873
|
console.log(` ${dim('OpenSea ')}${osBase}/assets/${osChain}/${contract}/${tokenId}`);
|
|
764
874
|
if (explorer)
|
|
765
875
|
console.log(` ${dim('Etherscan')} ${explorer}/token/${contract}?a=${tokenId}`);
|
|
876
|
+
// Both lanes now emit ERC-4906 on a URI change, so this is one sentence again. The 1155 lane
|
|
877
|
+
// used to emit only its own config event, which is why this branched and why `ping-uri` exists;
|
|
878
|
+
// an audit found a collection-wide re-point on an edition had NO refresh signal at all, and the
|
|
879
|
+
// fix was to emit the O(1) ERC-4906 range form there too. `ping-uri` is still useful on an
|
|
880
|
+
// edition, but for a narrower reason: it re-emits the NATIVE ERC-1155 `URI` event for indexers
|
|
881
|
+
// that honor only that one.
|
|
766
882
|
console.log(dim(' (ERC-4906 already pings 4906-aware marketplaces on URI changes; this covers the genesis mint + the rest.)'));
|
|
767
883
|
}
|
|
768
884
|
// ── transfer ─────────────────────────────────────────────────────────────────
|
|
769
885
|
export async function cmdTransfer(address, flags) {
|
|
770
|
-
const usage = 'abx transfer <address> --to 0x… [--token 0] [--amount <n> --from 0x… (editions only)] [--sign|--unsigned]';
|
|
886
|
+
const usage = 'abx transfer <address> --to 0x… [--token 0 | --token-id 0] [--amount <n> --from 0x… (editions only)] [--sign|--unsigned]';
|
|
771
887
|
const contract = requireAddress(address, usage);
|
|
888
|
+
warnStrayFlags(flags, new Set(TRANSFER_FLAGS), 'transfer');
|
|
772
889
|
const to = requireFlag(flags, 'to', usage);
|
|
773
|
-
|
|
890
|
+
// `--token-id` is accepted as an alias for `--token`: EVERY sibling id-taking command (`mint`,
|
|
891
|
+
// `set-max-supply`, `minter …`) spells it `--token-id`, so an agent that learned the name there
|
|
892
|
+
// passed it here — where it was silently ignored and id 0 moved instead of the id they named.
|
|
893
|
+
// On an edition that means transferring the WRONG WORK with no warning. Both spellings work now;
|
|
894
|
+
// disagreeing values are refused rather than silently preferring one.
|
|
895
|
+
if (flags.token !== undefined && flags['token-id'] !== undefined && flags.token !== flags['token-id']) {
|
|
896
|
+
throw new Error(`--token ${flags.token} and --token-id ${flags['token-id']} disagree — pass one (they are aliases for the same id).`);
|
|
897
|
+
}
|
|
898
|
+
const tokenId = BigInt(flags['token-id'] ?? flags.token ?? '0');
|
|
774
899
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
775
900
|
const kind = await detectTokenKind(publicClient, contract);
|
|
776
901
|
if (kind.isEdition) {
|
|
@@ -937,21 +1062,26 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
937
1062
|
`Enrollment can never be added to a live collection; if enforcement is required, redeploy with ` +
|
|
938
1063
|
`--721c recommended (or --721c 0x…) on the deploy command.`);
|
|
939
1064
|
}
|
|
940
|
-
//
|
|
941
|
-
//
|
|
1065
|
+
// Unusable-validator guard: the contract refuses a non-zero validator that fails its own probe —
|
|
1066
|
+
// no code, OR a permissive fallback that would enforce nothing (a Safe, an uninitialised proxy, a
|
|
1067
|
+
// 7702-delegated EOA). Both revert `InvalidTransferValidator()`, so the preflight has to ask the
|
|
1068
|
+
// same question the chain asks; a bare has-code check passed a creator's Safe and then let the
|
|
1069
|
+
// send fail on chain with an error whose documented meaning ("no code") was false for their case.
|
|
942
1070
|
if (validator !== zeroAddress) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1071
|
+
const probe = await probeTransferValidator(publicClient, validator, { as: contract });
|
|
1072
|
+
if (probe.verdict === 'unreachable') {
|
|
1073
|
+
throw new Error(`couldn't verify the validator at ${validator} (${probe.error ?? 'RPC did not answer'}) — refusing to re-point blind; retry when the RPC answers.`);
|
|
946
1074
|
}
|
|
947
|
-
|
|
948
|
-
throw new Error(`couldn't verify the validator has code at ${validator} (${err.message}) — refusing to re-point blind; retry when the RPC answers.`);
|
|
949
|
-
}
|
|
950
|
-
if (!code || code === '0x') {
|
|
1075
|
+
if (probe.verdict !== 'ok') {
|
|
951
1076
|
const rec = resolveRecommendedTransferValidator(cid);
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1077
|
+
const recHint = rec ? ` (the recommended one: \`abx set-transfer-validator ${contract} recommended\` → ${rec})` : '';
|
|
1078
|
+
throw new Error(probe.verdict === 'no-code'
|
|
1079
|
+
? `no contract code at ${validator} on ${CHAIN} — the token would revert InvalidTransferValidator(). ` +
|
|
1080
|
+
`A transfer validator must be a DEPLOYED contract on this chain${recHint}.`
|
|
1081
|
+
: `${validator} has code on ${CHAIN}, but it is not a transfer validator — it answers ANY function call successfully ` +
|
|
1082
|
+
`(a Safe, an uninitialised proxy, or a 7702-delegated EOA does this), so every transfer would silently pass ` +
|
|
1083
|
+
`validation while ERC-165 and getTransferValidator() reported enforcement as ON. The token refuses it too ` +
|
|
1084
|
+
`(InvalidTransferValidator()). Pass a real validator contract${recHint}, or \`none\` to suspend enforcement.`);
|
|
955
1085
|
}
|
|
956
1086
|
}
|
|
957
1087
|
if (eqAddr(status.validator, validator)) {
|
|
@@ -963,6 +1093,167 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
963
1093
|
const owner = await read(contract, 'owner');
|
|
964
1094
|
await runWrite(contract, prepareSetTransferValidator({ contract, validator, chainId: cid }), flags, owner);
|
|
965
1095
|
}
|
|
1096
|
+
// ── the seed source — where a code project's mint randomness comes from ───────
|
|
1097
|
+
// The canonical `AbxSeedSource` is deliberately pseudorandom: replayable after the mint (what makes
|
|
1098
|
+
// generative output verifiable) and computable DURING it (so a buyer can decline an outcome for the
|
|
1099
|
+
// price of gas — and on an EDITION, where the buyer names the id and the id is in the preimage, they
|
|
1100
|
+
// can shop the unminted ids rather than merely decline; bounded by how many remain)
|
|
1101
|
+
// price of gas). We tell creators that the escape hatch for anything lottery-like is "point
|
|
1102
|
+
// `seedSource` at your own IAbxSeedSource over commit-reveal or a VRF oracle" — this is the surface
|
|
1103
|
+
// that makes that promise real instead of a doc claim: `--seed-source` at deploy, `abx
|
|
1104
|
+
// set-seed-source` after, and one probe standing in front of both.
|
|
1105
|
+
/** The canonical `AbxSeedSource` for a chain — the manifest entry (the source of truth) with the
|
|
1106
|
+
* CREATE2 prediction as the cross-chain fallback (the singleton is address-identical everywhere).
|
|
1107
|
+
* Deliberately does NOT go through `resolveSeedSource`, which reads `ABX_SEED_SOURCE`: that env var
|
|
1108
|
+
* names the source you have CONFIGURED, which is exactly the thing `canonical` exists to name the
|
|
1109
|
+
* alternative to. Asking for `canonical` and silently getting your custom override would be the
|
|
1110
|
+
* worst possible answer. */
|
|
1111
|
+
export function canonicalSeedSource(chainId) {
|
|
1112
|
+
return getDeployment(chainId).seedSource ?? predictSeedSource();
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Parse a seed-source choice — the shared grammar of the deploy flag (`--seed-source`) and the owner
|
|
1116
|
+
* op (`abx set-seed-source`):
|
|
1117
|
+
* - `canonical` (or a bare `--seed-source`) → the chain's canonical `AbxSeedSource`, i.e. the
|
|
1118
|
+
* default; spelled out so a creator can state it rather than rely on absence meaning it;
|
|
1119
|
+
* - a `0x…` address → checksum-validated (EIP-55) and returned canonicalized. The caller MUST
|
|
1120
|
+
* probe it (see {@link refuseUnusableSeedSource}) — a bad address here is silent until the
|
|
1121
|
+
* first buyer, and then every mint reverts;
|
|
1122
|
+
* - `none`/zero → `allowNone` decides: the owner op clears with it (future mints draw no seed);
|
|
1123
|
+
* the DEPLOY flag refuses it and points at `--no-seed`, which already means exactly that. Two
|
|
1124
|
+
* spellings of one thing is how a creator ends up unsure which they used.
|
|
1125
|
+
* Pure (no RPC) — mirrors `parseTransferValidatorValue`, deliberately, so the two knobs read the
|
|
1126
|
+
* same. Exported for the regression test.
|
|
1127
|
+
*/
|
|
1128
|
+
export function parseSeedSourceValue(raw, opts) {
|
|
1129
|
+
const s = raw.trim().toLowerCase();
|
|
1130
|
+
if (s === 'true' || s === '' || s === 'canonical' || s === 'default')
|
|
1131
|
+
return canonicalSeedSource(opts.chainId);
|
|
1132
|
+
if (s === 'none' || s === 'zero' || s === '0' || s === '0x0' || s === zeroAddress) {
|
|
1133
|
+
if (opts.allowNone)
|
|
1134
|
+
return zeroAddress;
|
|
1135
|
+
throw new Error(`--seed-source none is refused: "no mint-time seed" already has a flag — pass \`--no-seed\` instead. ` +
|
|
1136
|
+
`(One meaning, one spelling: --seed-source names WHERE the seed comes from, --no-seed says there isn't one.)`);
|
|
1137
|
+
}
|
|
1138
|
+
const trimmed = raw.trim();
|
|
1139
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
|
|
1140
|
+
throw new Error(`seed source must be 'canonical'${opts.allowNone ? ", 'none'," : ''} or a 0x address (0x + 40 hex); got '${raw}'`);
|
|
1141
|
+
}
|
|
1142
|
+
// Strict EIP-55 (see the identical note on parseTransferValidatorValue): `getAddress` alone only
|
|
1143
|
+
// normalizes, so a mis-cased paste would sail through — and a seed source pointed one nibble off
|
|
1144
|
+
// is a collection whose every mint reverts.
|
|
1145
|
+
if (!isAddress(trimmed, { strict: true })) {
|
|
1146
|
+
throw new Error(`seed source address failed its EIP-55 checksum: '${raw}' — paste it exactly (or all-lowercase).`);
|
|
1147
|
+
}
|
|
1148
|
+
return getAddress(trimmed);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Turn a non-`ok` {@link SeedSourceProbe} into the refusal a creator can act on. One place, because
|
|
1152
|
+
* the deploy flag and the owner op hit the identical failure shapes and must say the identical
|
|
1153
|
+
* thing; `context` only names which surface asked (so the fix names the right flag).
|
|
1154
|
+
*
|
|
1155
|
+
* Every branch is a REFUSAL, not a warning. A seed source is the one setting where "looks fine, is
|
|
1156
|
+
* broken" is the normal outcome of a mistake: `seedSource()` reads back exactly what you set, the
|
|
1157
|
+
* `SeedSourceSet` event fires, `abx state` shows it — and then the first mint reverts in the ABI
|
|
1158
|
+
* decode of a return that isn't 32 bytes. There is no later moment at which this gets easier to
|
|
1159
|
+
* notice, so it is caught here or it is caught by a buyer.
|
|
1160
|
+
*/
|
|
1161
|
+
export function refuseUnusableSeedSource(probe, context) {
|
|
1162
|
+
const { flag, chainLabel } = context;
|
|
1163
|
+
const head = `${flag}: ${probe.address} is not a usable seed source on ${chainLabel}`;
|
|
1164
|
+
const tail = `\n A seed source must answer \`seed(uint256 tokenId, address to)\` with 32 bytes — the token calls it ` +
|
|
1165
|
+
`SYNCHRONOUSLY inside every mint and decodes the result as \`bytes32\`, so anything else reverts the mint.`;
|
|
1166
|
+
switch (probe.verdict) {
|
|
1167
|
+
case 'no-code':
|
|
1168
|
+
throw new Error(`${head} — there is no contract code there.${tail}\n` +
|
|
1169
|
+
` (A Solidity call to a codeless address succeeds with empty returndata, so the revert would land in the ` +
|
|
1170
|
+
`decode, at mint time, for every buyer. Check the address and the chain — ${flag} is chain-specific.)`);
|
|
1171
|
+
case 'empty-return':
|
|
1172
|
+
throw new Error(`${head} — it has code, answered, and returned NOTHING.${tail}\n` +
|
|
1173
|
+
` This is the permissive-fallback shape: a Safe (its fallback returns empty for an unset handler), an ` +
|
|
1174
|
+
`uninitialised proxy, or an EIP-7702-delegated EOA. Pasting your own wallet/Safe here is the common way in. ` +
|
|
1175
|
+
`A seed source is a purpose-built contract implementing IAbxSeedSource — commit-reveal or a VRF oracle.`);
|
|
1176
|
+
case 'short-return':
|
|
1177
|
+
throw new Error(`${head} — it answered with only ${probe.returnedBytes} byte(s), not 32.${tail}\n` +
|
|
1178
|
+
` Whatever is deployed there has a different ABI; it is not an IAbxSeedSource.`);
|
|
1179
|
+
case 'reverted':
|
|
1180
|
+
throw new Error(`${head} — the call reverted (${probe.error ?? 'no reason returned'}).${tail}\n` +
|
|
1181
|
+
` Two ways in: (1) it isn't a seed source at all — a token, a renderer, a registry has no such function; ` +
|
|
1182
|
+
`(2) it IS one, but it refuses right now (gating callers, or not yet armed/committed). Both are refusals here: ` +
|
|
1183
|
+
`a source that cannot answer today cannot answer at mint either, and its revert would bubble through the mint. ` +
|
|
1184
|
+
`Arm the source first, then point at it.`);
|
|
1185
|
+
default:
|
|
1186
|
+
// `unreachable` and any verdict a future SDK adds: refuse rather than proceed. A `default` (not
|
|
1187
|
+
// a `case 'unreachable'`) so a new verdict cannot silently become "allowed" by falling through.
|
|
1188
|
+
throw new Error(`${head} — couldn't verify it (${probe.verdict}${probe.error ? `: ${probe.error}` : ''}). ` +
|
|
1189
|
+
`Refusing to configure a seed source blind; retry when the RPC answers.`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* `abx set-seed-source <address> <0x…|canonical|none>` — re-point (or clear) where a code project
|
|
1194
|
+
* draws its mint seeds. Owner-only; **future mints only**, because a seed settles the moment it is
|
|
1195
|
+
* assigned and nothing rewrites it. Refuses before any signing: a target with no Seed Source
|
|
1196
|
+
* extension (a 1/1 or an image Series — there is nothing to set), and any candidate the probe
|
|
1197
|
+
* rejects.
|
|
1198
|
+
*
|
|
1199
|
+
* This op is deliberately loud about one thing the chain cannot enforce: re-pointing mid-sale means
|
|
1200
|
+
* tokens minted before and after draw from **different** sources. That is legitimate (it is how you
|
|
1201
|
+
* fix a broken source, or hand over to a commit-reveal one), and it is public — `SeedSourceSet` is
|
|
1202
|
+
* on the event spine and `abx state` prints the current source — but a collector who bought early
|
|
1203
|
+
* has no way to know it will happen. So the print names the split when supply already exists.
|
|
1204
|
+
*/
|
|
1205
|
+
export async function cmdSetSeedSource(address, rest, flags) {
|
|
1206
|
+
const usage = 'abx set-seed-source <address> <0x…|canonical|none> [--sign|--unsigned] [--dry-run]';
|
|
1207
|
+
const contract = requireAddress(address, usage);
|
|
1208
|
+
const [raw] = positionalArgs(rest);
|
|
1209
|
+
if (!raw) {
|
|
1210
|
+
console.error(`usage: ${usage}\n`);
|
|
1211
|
+
process.exitCode = 1;
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const cid = chainId();
|
|
1215
|
+
const source = parseSeedSourceValue(raw, { chainId: cid, allowNone: true });
|
|
1216
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1217
|
+
await assertContractExists(contract);
|
|
1218
|
+
// Extension guard FIRST — a read, not a send. `seedSource()` is absent on a 1/1 / image Series, so
|
|
1219
|
+
// `setSeedSource` there is a call to a selector the contract doesn't have: it would revert with no
|
|
1220
|
+
// reason bytes at all. Say what the target actually is instead.
|
|
1221
|
+
const current = await readSeedSource(publicClient, contract);
|
|
1222
|
+
if (current === undefined) {
|
|
1223
|
+
throw new Error(`${contract} has no seed source to set — the Seed Source extension is composed only by CODE projects ` +
|
|
1224
|
+
`(SeriesCode / EditionCode, i.e. \`abx deploy-code\`). An image 1/1 or Series has no mint-time seed at all, ` +
|
|
1225
|
+
`and the extension can't be added to a live contract.`);
|
|
1226
|
+
}
|
|
1227
|
+
if (eqAddr(current, source)) {
|
|
1228
|
+
console.log(dim(` no change — the seed source is already ${source === zeroAddress ? 'cleared (0x0 — no mint-time seed)' : source}. Nothing sent.`));
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (source !== zeroAddress) {
|
|
1232
|
+
const probe = await probeSeedSource(publicClient, source, { as: contract });
|
|
1233
|
+
if (probe.verdict !== 'ok')
|
|
1234
|
+
refuseUnusableSeedSource(probe, { flag: 'set-seed-source', chainLabel: CHAIN });
|
|
1235
|
+
}
|
|
1236
|
+
const label = (a) => (eqAddr(a, zeroAddress) ? 'none (0x0 — no mint-time seed)' : eqAddr(a, canonicalSeedSource(cid)) ? `${a} (canonical AbxSeedSource)` : a);
|
|
1237
|
+
console.log(dim(` seed source: ${label(current)} → ${label(source)}`));
|
|
1238
|
+
// Seeds already assigned are settled — say so, and say what the split means. `totalSupply()` is a
|
|
1239
|
+
// 721 getter (an edition's is per-id), so the count is best-effort; the note itself is not, because
|
|
1240
|
+
// "future mints only" is the part a creator has to understand before sending.
|
|
1241
|
+
const minted = await readSeries(contract, 'totalSupply').catch(() => null);
|
|
1242
|
+
if (minted === null || minted > 0n) {
|
|
1243
|
+
console.log(dim(` ${minted === null ? 'Tokens' : `${minted} token(s)`} already minted carry a seed drawn from ${label(current)} — settled, and unchanged by this. ` +
|
|
1244
|
+
`This applies to FUTURE mints only, so a part-sold collection ends up spanning two sources. The change is public ` +
|
|
1245
|
+
`(SeedSourceSet on the event spine; \`abx state ${contract}\` prints the current one), but a collector who ` +
|
|
1246
|
+
`already bought won't be told — if the drop is live, consider pausing and saying so.`));
|
|
1247
|
+
}
|
|
1248
|
+
if (source === zeroAddress) {
|
|
1249
|
+
console.log(dim(' clearing means future mints draw NO seed at all — a generative program that expects one will render blank/identical.'));
|
|
1250
|
+
}
|
|
1251
|
+
else if (!eqAddr(source, canonicalSeedSource(cid))) {
|
|
1252
|
+
console.log(dim(" custom source — its randomness properties are now yours to state to buyers; ABX makes no claim about them."));
|
|
1253
|
+
}
|
|
1254
|
+
const owner = await read(contract, 'owner');
|
|
1255
|
+
await runWrite(contract, prepareSetSeedSource({ contract, seedSource: source, chainId: cid }), flags, owner);
|
|
1256
|
+
}
|
|
966
1257
|
// ── multi-chunk on-chain content (the `reader` path) ─────────────────────────
|
|
967
1258
|
// The plan math (chunk count + tx shape), the cost-model constants, and the actual
|
|
968
1259
|
// plan→resolve-store→stage→encode orchestration all live in the SDK's staging.ts, layered on
|
|
@@ -1026,13 +1317,55 @@ export function sessionStagingSender(session) {
|
|
|
1026
1317
|
return async (tx) => (await session.send(tx)).receipt;
|
|
1027
1318
|
}
|
|
1028
1319
|
const kb = (n) => `${n < 10 * 1024 ? (n / 1024).toFixed(1) : Math.round(n / 1024)}KB`;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1320
|
+
const mgas = (n) => `~${(n / 1_000_000).toFixed(0)}M gas`;
|
|
1321
|
+
/** The flag that overrides {@link guardOnChainSize}'s refusal — a creator who genuinely wants a
|
|
1322
|
+
* project no default RPC can read. Named for what accepting it means, not for the check it skips. */
|
|
1323
|
+
export const ALLOW_UNREADABLE_FLAG = 'allow-unreadable-onchain';
|
|
1324
|
+
/**
|
|
1325
|
+
* The size gate for content going on-chain, on the axis that actually binds: **render gas**, not
|
|
1326
|
+
* storage cost. `tokenURI` reassembles the whole document per call — measured at ~360,000-405,000 gas
|
|
1327
|
+
* per KB of on-chain content across 10-100 KB and climbing above that (the cost is superlinear: memory
|
|
1328
|
+
* expansion is quadratic), within ~1% for `inline` and `reader` up to 75 KB (it's the renderer's string
|
|
1329
|
+
* building, not the storage mechanism) — so the readable envelope is much tighter than the cost-sane one.
|
|
1330
|
+
*
|
|
1331
|
+
* - under ~40 KB: quiet, plus the cost note if it's past the point where off-chain is simply cheaper.
|
|
1332
|
+
* - from ~40 KB: **warn** — the read needs a deliberately high-gas RPC.
|
|
1333
|
+
* - past ~100 KB (~40M gas): **refuse** — a deliberate margin, not the wall. geth's own 50M
|
|
1334
|
+
* `--rpc.gascap` default holds to ~120 KB, but hosted providers commonly cap well below it, so past
|
|
1335
|
+
* 100 KB the token reads as a revert to marketplaces and indexers on ordinary RPCs.
|
|
1336
|
+
* `--allow-unreadable-onchain` ships it anyway.
|
|
1337
|
+
*
|
|
1338
|
+
* Deliberately a toolkit gate and not an on-chain one: bricking a contract over a read budget is
|
|
1339
|
+
* worse than needing a capable RPC, and this is the moment where the creator can still choose
|
|
1340
|
+
* differently. Returns true if it said anything.
|
|
1341
|
+
*/
|
|
1342
|
+
export function guardOnChainSize(bytesLen, label, allowUnreadable = false) {
|
|
1343
|
+
const verdict = classifyOnchainReadSize(bytesLen);
|
|
1344
|
+
const gas = mgas(tokenUriGasEstimate(bytesLen));
|
|
1345
|
+
if (verdict === 'refuse' && !allowUnreadable) {
|
|
1346
|
+
throw new CliError(`${label} is ${kb(bytesLen)} of on-chain content — reading tokenURI would cost ${gas}, past what a default node will serve ` +
|
|
1347
|
+
`(geth's --rpc.gascap defaults to 50M; hosted providers commonly cap lower). Marketplaces, indexers, and other contracts ` +
|
|
1348
|
+
`would see a REVERT rather than your token, so this is refused by default.\n` +
|
|
1349
|
+
` Three ways forward:\n` +
|
|
1350
|
+
` • Off-chain custody: --backend arweave (pay-once, permanent) or ipfs|cloud — the bytes live off-chain, the JSON still resolves on-chain, and the read stays small.\n` +
|
|
1351
|
+
` • Smaller SOURCE content: a leaner SVG rather than a raster. Note --compress fastlz does not help here — it cuts the write cost, and the renderer rebuilds the full uncompressed bytes on every read.\n` +
|
|
1352
|
+
` • Ship it unreadable on purpose: re-run with --${ALLOW_UNREADABLE_FLAG}. That is a real choice (maximum durability, nothing to run, ever) — and what you accept is that rendering needs a deliberately high-gas endpoint, and most marketplaces and indexers will show nothing at all.`);
|
|
1353
|
+
}
|
|
1354
|
+
if (verdict === 'refuse') {
|
|
1355
|
+
console.log(yellow(` ⚠ ${label} is ${kb(bytesLen)} on-chain — tokenURI costs ${gas} to read, past a default node's eth_call cap. Shipping anyway (--${ALLOW_UNREADABLE_FLAG}).`));
|
|
1356
|
+
console.log(dim(` What you're accepting: marketplaces and indexers on ordinary RPCs will see a revert, not your token; rendering needs a deliberately high-gas endpoint. Assemble from the generator's piecewise getters (\`document\`, \`tokenDataJson\`, \`dependencyTag\`) when one eth_call won't fit.`));
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
if (verdict === 'warn') {
|
|
1360
|
+
console.log(yellow(` ⚠ ${label} is ${kb(bytesLen)} on-chain — reading tokenURI costs ${gas} (~360-405k gas per KB of content and climbing, whatever the representation).`));
|
|
1361
|
+
console.log(dim(` That is served by a high-gas RPC but not by every hosted one, and it is past what another contract can spend inside a transaction. Under ~${kb(ONCHAIN_READ_WARN_BYTES)} of on-chain content per token is the routinely-readable range; off-chain custody (--backend arweave|ipfs|cloud) keeps the JSON on-chain and the read small.`));
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
if (exceedsOnchainSoftLimit(bytesLen)) {
|
|
1365
|
+
console.log(dim(` ${label} is ${kb(bytesLen)} — past ~${kb(ONCHAIN_IMAGE_SOFT_LIMIT)}/file on-chain costs more to write than off-chain (~200 gas/byte), so pick it for self-resolution, not for price. Reading tokenURI: ${gas}.`));
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
return false;
|
|
1036
1369
|
}
|
|
1037
1370
|
/**
|
|
1038
1371
|
* Pure staging plan for a piece of content — chunk count + transaction shape — with NO chain
|
|
@@ -1079,8 +1412,9 @@ function planTxLine(p, finalLabel) {
|
|
|
1079
1412
|
* These store writes use the env key (the data contracts are ownerless); the owner only
|
|
1080
1413
|
* signs the field set that references the manifest.
|
|
1081
1414
|
*/
|
|
1082
|
-
async function putContentOnChain(bytes, compress, field, send, finalLabel = '1 owner field-set', store)
|
|
1083
|
-
|
|
1415
|
+
async function putContentOnChain(bytes, compress, field, send, finalLabel = '1 owner field-set', store, // pre-resolved store (batch staging reuses one); else resolve/deploy here
|
|
1416
|
+
allowUnreadable = false) {
|
|
1417
|
+
guardOnChainSize(bytes.length, `'${field}'`, allowUnreadable);
|
|
1084
1418
|
const content = compress === 'gzip' ? new Uint8Array(gzipSync(bytes)) : new Uint8Array(bytes);
|
|
1085
1419
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1086
1420
|
return stageFieldContent({
|
|
@@ -1110,10 +1444,12 @@ async function putContentOnChain(bytes, compress, field, send, finalLabel = '1 o
|
|
|
1110
1444
|
* true count instead of leaving the agent to guess). The final tx here is the deploy that
|
|
1111
1445
|
* bakes in the reader field, so the count is staging tx(s) + the deploy.
|
|
1112
1446
|
*/
|
|
1113
|
-
export function previewImageStaging(imagePath, compress) {
|
|
1447
|
+
export function previewImageStaging(imagePath, compress, allowUnreadable = false) {
|
|
1114
1448
|
const bytes = readFileSync(resolvePath(imagePath));
|
|
1115
1449
|
const p = computeContentPlan(bytes, compress);
|
|
1116
|
-
|
|
1450
|
+
// The gate runs on the dry-run too: a refusal a creator only meets after the first staging tx has
|
|
1451
|
+
// landed is a refusal that already cost them gas.
|
|
1452
|
+
guardOnChainSize(bytes.length, basename(resolvePath(imagePath)), allowUnreadable);
|
|
1117
1453
|
return (`would stage ${basename(resolvePath(imagePath))} on-chain (chunk store): ` +
|
|
1118
1454
|
`${planSizeLine(bytes.length, p, compress)} as ${p.representation}; ` +
|
|
1119
1455
|
`${planTxLine(p, '1 deploy (bakes the reader field + mints)')}`);
|
|
@@ -1125,9 +1461,9 @@ export function previewImageStaging(imagePath, compress) {
|
|
|
1125
1461
|
* image" is just those staging tx(s) + the single deploy — no post-deploy `set-field`.
|
|
1126
1462
|
* The manifest is deployer-independent, so this is computed once and reused for any signer.
|
|
1127
1463
|
*/
|
|
1128
|
-
export async function stageImageField(imagePath, compress, send) {
|
|
1464
|
+
export async function stageImageField(imagePath, compress, send, allowUnreadable = false) {
|
|
1129
1465
|
const bytes = readFileSync(resolvePath(imagePath));
|
|
1130
|
-
const { value, representation } = await putContentOnChain(bytes, compress, 'image', send, '1 deploy (bakes the reader field + mints)');
|
|
1466
|
+
const { value, representation } = await putContentOnChain(bytes, compress, 'image', send, '1 deploy (bakes the reader field + mints)', undefined, allowUnreadable);
|
|
1131
1467
|
return {
|
|
1132
1468
|
field: { field: encodeTag(F.image), representation: encodeTag(representation), value },
|
|
1133
1469
|
note: `image: ${bytes.length}B staged ON-CHAIN via reader (${representation}) — baked into the deploy`,
|
|
@@ -1137,15 +1473,15 @@ export async function stageImageField(imagePath, compress, send) {
|
|
|
1137
1473
|
* Stage MANY images on-chain against a **single, shared** chunk store — the batch form of
|
|
1138
1474
|
* {@link stageImageField} for a Series. The store is resolved/deployed **once** (not per token),
|
|
1139
1475
|
* then every file is staged against it and returned as its own `reader`-backed `image` field.
|
|
1140
|
-
* Each field still points at its own manifest, so each token's
|
|
1476
|
+
* Each field still points at its own manifest, so each token's work is independent; they just
|
|
1141
1477
|
* share the store contract. Returns the fields in input order (token order).
|
|
1142
1478
|
*/
|
|
1143
|
-
export async function stageImageFieldsBatch(imagePaths, compress, send, storeOverride) {
|
|
1479
|
+
export async function stageImageFieldsBatch(imagePaths, compress, send, storeOverride, allowUnreadable = false) {
|
|
1144
1480
|
const store = await ensureChunkStore(send, storeOverride);
|
|
1145
1481
|
const fields = [];
|
|
1146
1482
|
for (const imagePath of imagePaths) {
|
|
1147
1483
|
const bytes = readFileSync(resolvePath(imagePath));
|
|
1148
|
-
const { value, representation } = await putContentOnChain(bytes, compress, 'image', send, '1 deploy (bakes the reader fields + mints)', store);
|
|
1484
|
+
const { value, representation } = await putContentOnChain(bytes, compress, 'image', send, '1 deploy (bakes the reader fields + mints)', store, allowUnreadable);
|
|
1149
1485
|
fields.push({ field: encodeTag(F.image), representation: encodeTag(representation), value });
|
|
1150
1486
|
}
|
|
1151
1487
|
return { fields, store };
|
|
@@ -1220,6 +1556,8 @@ export async function cmdSetField(address, flags) {
|
|
|
1220
1556
|
if (staging && isDryRun(flags)) {
|
|
1221
1557
|
const bytes = readFileSync(resolvePath(flags.file));
|
|
1222
1558
|
const p = computeContentPlan(bytes, parseCompress(flags.compress));
|
|
1559
|
+
// Same render-gas gate the real path applies, so a refusal costs a dry run rather than gas.
|
|
1560
|
+
guardOnChainSize(bytes.length, `'${field}'`, allowUnreadableOnchain(flags));
|
|
1223
1561
|
console.log(`\n ${bold('◆ set-field ' + field)} ${dim('(dry run — nothing staged or sent)')}`);
|
|
1224
1562
|
console.log(` ${dim('scope'.padEnd(12))} ${collection ? 'collection' : `token #${flags.token ?? '0'}`}`);
|
|
1225
1563
|
console.log(` ${planSizeLine(bytes.length, p, parseCompress(flags.compress))}`);
|
|
@@ -1251,7 +1589,7 @@ export async function cmdSetField(address, flags) {
|
|
|
1251
1589
|
});
|
|
1252
1590
|
try {
|
|
1253
1591
|
await session.connect();
|
|
1254
|
-
const { value, representation } = await putContentOnChain(bytes, compress, field, sessionStagingSender(session));
|
|
1592
|
+
const { value, representation } = await putContentOnChain(bytes, compress, field, sessionStagingSender(session), undefined, undefined, allowUnreadableOnchain(flags));
|
|
1255
1593
|
await session.send(buildTx(value, representation));
|
|
1256
1594
|
}
|
|
1257
1595
|
finally {
|
|
@@ -1265,7 +1603,7 @@ export async function cmdSetField(address, flags) {
|
|
|
1265
1603
|
let representation;
|
|
1266
1604
|
if (staging) {
|
|
1267
1605
|
const compress = parseCompress(flags.compress);
|
|
1268
|
-
({ value, representation } = await putContentOnChain(readFileSync(resolvePath(flags.file)), compress, field, envStagingSender()));
|
|
1606
|
+
({ value, representation } = await putContentOnChain(readFileSync(resolvePath(flags.file)), compress, field, envStagingSender(), undefined, undefined, allowUnreadableOnchain(flags)));
|
|
1269
1607
|
}
|
|
1270
1608
|
else if (flags.text && flags.text !== 'true') {
|
|
1271
1609
|
value = toHex(flags.text);
|
|
@@ -1396,13 +1734,13 @@ export async function cmdAttach(rest, flags) {
|
|
|
1396
1734
|
const uriBase = await read(contract, 'tokenURIBase').catch(() => '');
|
|
1397
1735
|
const artifactsPath = `/t/${chainId()}/${contract}/${flags.token ?? '0'}`;
|
|
1398
1736
|
if (uriBase && uriBase.trim() !== '') {
|
|
1399
|
-
console.log(dim(` → listed in this project's resolver artifacts (${artifactsPath} and /data/<key>); a bare on-chain tokenURI carries reserved fields
|
|
1737
|
+
console.log(dim(` → listed in this project's resolver artifacts (${artifactsPath} and /data/<key>); a bare on-chain tokenURI carries reserved fields only (params are chain state — read them with abx tokens).`));
|
|
1400
1738
|
}
|
|
1401
1739
|
else {
|
|
1402
1740
|
console.log(yellow(' ⚠ ') +
|
|
1403
|
-
`this project resolves ON-CHAIN (no resolver base baked in), and the on-chain document carries reserved fields
|
|
1741
|
+
`this project resolves ON-CHAIN (no resolver base baked in), and the on-chain document carries reserved fields only — ` +
|
|
1404
1742
|
`so ${pairs.map((p) => bold(p.key)).join(', ')} will NOT appear in ${bold('tokenURI')}. The bytes are stored and provable, but nothing surfaces them to a marketplace or wallet. ` +
|
|
1405
|
-
dim('(
|
|
1743
|
+
dim('(Params are chain state, readable with abx tokens and by any contract call — attachments are the surface that needs a resolver.)'));
|
|
1406
1744
|
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}.`));
|
|
1407
1745
|
}
|
|
1408
1746
|
// ONE transaction for the whole set. `batchOps` folds a same-target run into a `multicall` and
|
|
@@ -1427,7 +1765,7 @@ export async function cmdAttach(rest, flags) {
|
|
|
1427
1765
|
dim(` verify (a resolver serves the complete listing): `) +
|
|
1428
1766
|
`abx tokenuri ${contract}${id === '0' ? '' : ` --token ${id}`} --fetch` +
|
|
1429
1767
|
dim(` ${collection ? '' : `→ artifacts[].key ${pairs.map((p) => `"${p.key}"`).join(', ')}; /data/<key> fetches each`}\n`) +
|
|
1430
|
-
dim(` (The complete file listing is a resolver surface — the bare on-chain tokenURI enumerates reserved fields
|
|
1768
|
+
dim(` (The complete file listing is a resolver surface — the bare on-chain tokenURI enumerates reserved fields. Params never need a resolver, they are read straight off the contract; attachments do.)\n`));
|
|
1431
1769
|
}
|
|
1432
1770
|
}
|
|
1433
1771
|
// ── lock-field ───────────────────────────────────────────────────────────────
|
|
@@ -1435,9 +1773,9 @@ export async function cmdAttach(rest, flags) {
|
|
|
1435
1773
|
* Refuse `lock-field` on a name that is a declared PARAM key.
|
|
1436
1774
|
*
|
|
1437
1775
|
* Fields and params are two separate namespaces that may share a name, and `lock-field` only ever
|
|
1438
|
-
* locks the *field*. A tester welded `grid` — a `Bytes` param holding the
|
|
1776
|
+
* locks the *field*. A tester welded `grid` — a `Bytes` param holding the work — with
|
|
1439
1777
|
* `lock-field --field grid`, got "permanent", got `tokenFieldLocked(0,"grid") == true`, and then
|
|
1440
|
-
* overwrote the
|
|
1778
|
+
* overwrote the work with `configure-param` on the next call. Every individual statement the CLI
|
|
1441
1779
|
* made was true; together they promised a protection that did not exist. Permanence is the pitch, so
|
|
1442
1780
|
* this refuses rather than warns, and names the mechanism that actually welds a param.
|
|
1443
1781
|
*/
|
|
@@ -1502,13 +1840,14 @@ export async function cmdSetRenderer(address, flags) {
|
|
|
1502
1840
|
}
|
|
1503
1841
|
// ── lock-uri (freeze the URI config) ──────────────────────────────────────────
|
|
1504
1842
|
/** Freeze a scope's URI config (pointer + renderer) forever. With the fields also
|
|
1505
|
-
* locked, the
|
|
1506
|
-
*
|
|
1843
|
+
* locked, the stored metadata can never change again — the on-chain twin of a frozen
|
|
1844
|
+
* `tokenURI`. Not the same as a frozen OUTPUT: params have no lock and the renderer
|
|
1845
|
+
* projects them, and a Registry dependency resolves live. Scope: token or `--collection`. */
|
|
1507
1846
|
export async function cmdLockUri(address, flags) {
|
|
1508
1847
|
const contract = requireAddress(address, 'abx lock-uri <address> [--collection] [--sign|--unsigned]');
|
|
1509
1848
|
const collection = !!flags.collection;
|
|
1510
1849
|
const owner = await read(contract, 'owner');
|
|
1511
|
-
console.log(dim(` note: locking the ${collection ? 'contractURI' : 'tokenURI'} config is permanent — pointer + renderer can never change again.`));
|
|
1850
|
+
console.log(dim(` note: locking the ${collection ? 'contractURI' : 'tokenURI'} config is permanent — pointer + renderer can never change again. Locked metadata is not a locked output: params stay writable and the renderer serves them.`));
|
|
1512
1851
|
const tx = collection
|
|
1513
1852
|
? prepareLockContractURI({ contract, chainId: chainId() })
|
|
1514
1853
|
: prepareLockTokenURI({ contract, chainId: chainId() });
|
|
@@ -1631,12 +1970,13 @@ function parseSalePrice(flags, usage) {
|
|
|
1631
1970
|
* the token, set a primary payee) and whether the token is paused.
|
|
1632
1971
|
*
|
|
1633
1972
|
* On an EDITION (OneOfOneEdition/EditionImage/EditionCode), `--token-id` is REQUIRED (sales are
|
|
1634
|
-
* keyed `(token, id)` — a per-
|
|
1973
|
+
* keyed `(token, id)` — a per-work price, not one project-wide sale) and the sale routes to the
|
|
1635
1974
|
* shared `AbxFixedPriceMinter1155` instead. Refused on a 721 target.
|
|
1636
1975
|
*/
|
|
1637
1976
|
export async function cmdMinterConfigure(address, flags) {
|
|
1638
1977
|
const usage = 'abx minter configure <token> (--price <eth> | --price-raw <units>) --allocation <n> [--erc20 0x…] [--token-id <n> (editions)] [--minter-contract 0x…] [--sign|--unsigned]';
|
|
1639
1978
|
const token = requireAddress(address, usage);
|
|
1979
|
+
warnStrayFlags(flags, new Set(MINTER_CONFIGURE_FLAGS), 'minter configure');
|
|
1640
1980
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1641
1981
|
const kind = await detectTokenKind(publicClient, token);
|
|
1642
1982
|
const owner = await read(token, 'owner');
|
|
@@ -1673,10 +2013,15 @@ export async function cmdMinterConfigure(address, flags) {
|
|
|
1673
2013
|
console.log(dim(` price ${isEth ? `${formatEther(price)} ETH` : `${price} units of ${erc20}`} / copy`));
|
|
1674
2014
|
console.log(dim(` allocation ${allocation}`));
|
|
1675
2015
|
await runMinterWrite(prepareConfigureSale1155({ minter, token, tokenId, paymentToken: erc20, price, allocation, chainId: chainId() }), flags, owner);
|
|
2016
|
+
// Through the EDITION ABI, for the same reason `minter show` spells out: the generic `read()`
|
|
2017
|
+
// is the 721 1/1 ABI, which has no minter()/primaryPayee() at all, so the call throws
|
|
2018
|
+
// client-side and the `.catch` swallows it into zeroAddress. That printed "⚠ assign this
|
|
2019
|
+
// minter" + "⚠ set a primary payee" on an edition that already had both — the same false
|
|
2020
|
+
// "not assigned" bug as the show path, surviving here because only show got fixed in 2026-08-05.
|
|
1676
2021
|
const [assignedMinter, payee, paused] = await Promise.all([
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2022
|
+
readEdition(token, 'minter').catch(() => zeroAddress),
|
|
2023
|
+
readEdition(token, 'primaryPayee').catch(() => zeroAddress),
|
|
2024
|
+
readEdition(token, 'paused').catch(() => false),
|
|
1680
2025
|
]);
|
|
1681
2026
|
const assigned = assignedMinter.toLowerCase() === minter.toLowerCase();
|
|
1682
2027
|
console.log('');
|
|
@@ -1741,6 +2086,7 @@ export async function cmdMinterConfigure(address, flags) {
|
|
|
1741
2086
|
export async function cmdMinterShow(address, flags) {
|
|
1742
2087
|
const usage = 'abx minter show <token> [--token-id <n> (editions)] [--minter-contract 0x…]';
|
|
1743
2088
|
const token = requireAddress(address, usage);
|
|
2089
|
+
warnStrayFlags(flags, new Set(MINTER_SHOW_FLAGS), 'minter show');
|
|
1744
2090
|
// Refuse a nonexistent contract like the sibling owner-ops (state/unpause/minter configure) do —
|
|
1745
2091
|
// otherwise the minter singleton returns zero-config for the unconfigured token and we print a
|
|
1746
2092
|
// plausible-but-fake `paused: no (open)` readout for an address that has no project at all.
|
|
@@ -1834,6 +2180,7 @@ export async function cmdMinterShow(address, flags) {
|
|
|
1834
2180
|
export async function cmdMinterBuy(address, flags) {
|
|
1835
2181
|
const usage = 'abx minter buy <token> [--to 0x…] [--token-id <n> --quantity <n> (editions)] [--minter-contract 0x…] [--sign|--unsigned]';
|
|
1836
2182
|
const token = requireAddress(address, usage);
|
|
2183
|
+
warnStrayFlags(flags, new Set(MINTER_BUY_FLAGS), 'minter buy');
|
|
1837
2184
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1838
2185
|
const kind = await detectTokenKind(publicClient, token);
|
|
1839
2186
|
if (kind.isEdition) {
|
|
@@ -1849,15 +2196,17 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1849
2196
|
throw new Error(`no sale configured for ${token} #${tokenId} — run: abx minter configure ${token} --token-id ${tokenId} …`);
|
|
1850
2197
|
const isEth = sale.paymentToken === zeroAddress;
|
|
1851
2198
|
const to = flags.to && flags.to !== 'true' ? flags.to : undefined;
|
|
1852
|
-
// Payment math
|
|
1853
|
-
//
|
|
1854
|
-
|
|
2199
|
+
// Payment math AND the buyer's terms bound both come off `sale` inside the SDK op (one
|
|
2200
|
+
// computation of price × quantity, for the attached ETH and for `maxTotalPrice`) — the terms
|
|
2201
|
+
// just read are the terms the tx commits to, so a mid-flight `configure` reverts instead of
|
|
2202
|
+
// spending more. Shown in the confirm/dry-run readout below (runMinterWrite → gatedSend).
|
|
2203
|
+
const total = sale.price * quantity;
|
|
1855
2204
|
if (!isEth) {
|
|
1856
|
-
console.log(yellow(` ERC-20 sale: the buyer must have approved ${minter} to spend ${
|
|
2205
|
+
console.log(yellow(` ERC-20 sale: the buyer must have approved ${minter} to spend ${total} units of ${sale.paymentToken} first (else the tx reverts).`));
|
|
1857
2206
|
}
|
|
1858
2207
|
console.log(dim(` buying ${quantity} cop${quantity === 1n ? 'y' : 'ies'} of #${tokenId}${to ? ` → ${to}` : ' → signer'} for ` +
|
|
1859
|
-
`${isEth ? `${formatEther(
|
|
1860
|
-
await runMinterWrite(preparePurchase1155({ minter, token, tokenId, quantity,
|
|
2208
|
+
`${isEth ? `${formatEther(total)} ETH (${formatEther(sale.price)} × ${quantity})` : `${total} units (${sale.price} × ${quantity})`}`));
|
|
2209
|
+
await runMinterWrite(preparePurchase1155({ minter, token, tokenId, quantity, sale, to, chainId: chainId() }), flags);
|
|
1861
2210
|
console.log(dim(` next: \`abx refresh ${token}\` so marketplaces pick up the change.`));
|
|
1862
2211
|
return;
|
|
1863
2212
|
}
|
|
@@ -1872,12 +2221,13 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1872
2221
|
throw new Error(`no sale configured for ${token} — run: abx minter configure ${token} …`);
|
|
1873
2222
|
const isEth = sale.paymentToken === zeroAddress;
|
|
1874
2223
|
const to = flags.to && flags.to !== 'true' ? flags.to : undefined;
|
|
1875
|
-
const value = isEth ? sale.price : 0n;
|
|
1876
2224
|
if (!isEth) {
|
|
1877
2225
|
console.log(yellow(` ERC-20 sale: the buyer must have approved ${minter} to spend ${sale.price} units of ${sale.paymentToken} first (else the tx reverts).`));
|
|
1878
2226
|
}
|
|
1879
2227
|
console.log(dim(` buying 1 token${to ? ` → ${to}` : ' → signer'} for ${isEth ? `${formatEther(sale.price)} ETH` : `${sale.price} units`}`));
|
|
1880
|
-
|
|
2228
|
+
// The terms just read go into the tx as the buyer's bound (SDK op derives value + maxPrice from
|
|
2229
|
+
// `sale`), so an owner who re-`configure`s mid-flight gets a revert, not the buyer's allowance.
|
|
2230
|
+
await runMinterWrite(preparePurchase({ minter, token, sale, to, chainId: chainId() }), flags);
|
|
1881
2231
|
console.log(dim(` next: \`abx refresh ${token}\` so marketplaces pick up the new token.`));
|
|
1882
2232
|
}
|
|
1883
2233
|
// ── PostParam schemas, after deploy ──────────────────────────────────────────
|