@artblocks/abx-cli 0.1.0-alpha.21 → 0.1.0-alpha.23
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 +906 -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 +234 -97
- 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 +150 -10
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +8 -4
- 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 +64 -20
- 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/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.map +1 -1
- package/dist/flag-allowlists.js +5 -1
- package/dist/flag-allowlists.js.map +1 -1
- package/dist/flags.d.ts +4 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +23 -0
- package/dist/flags.js.map +1 -1
- package/dist/main.js +125 -37
- 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 +357 -89
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +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.map +1 -1
- package/dist/update-check.js +9 -3
- package/dist/update-check.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +62 -32
- package/skill/reference/code-projects.md +78 -24
- package/skill/reference/creator-token.md +16 -3
- package/skill/reference/decisions.md +59 -13
- package/skill/reference/hosting.md +1 -1
- package/skill/reference/operating.md +33 -13
- 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';
|
|
@@ -18,17 +18,17 @@ import { gzipSync } from 'node:zlib';
|
|
|
18
18
|
import { formatEther, getAddress, isAddress, parseEther, toHex, zeroAddress } from 'viem';
|
|
19
19
|
import { CHAIN, chainId, fixedPriceMinterAddress, fixedPriceMinter1155Address, localIndexer } from './config.js';
|
|
20
20
|
import { CliError } from './errors.js';
|
|
21
|
-
import { detectTokenKind
|
|
21
|
+
import { detectTokenKind } from './kind.js';
|
|
22
22
|
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, warnStrayFlags } 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'
|
|
@@ -218,7 +219,7 @@ export async function assertContractExists(address) {
|
|
|
218
219
|
* and canonically ENCODES the human input per its type (`#rrggbb`, decimals ×1e10,
|
|
219
220
|
* Select by label, …); `String`/`Bytes` schemas take the value as UTF-8 (or
|
|
220
221
|
* `--file <path>` for bytes) via the data path. The signer must satisfy the schema's
|
|
221
|
-
* auth (
|
|
222
|
+
* auth (Creator = contract owner, TokenOwner — delegate.xyz honored — or the named
|
|
222
223
|
* address); the chain enforces it either way.
|
|
223
224
|
*/
|
|
224
225
|
/** Positional args only — drops `--flags` AND the single token each value-taking flag consumes
|
|
@@ -236,7 +237,7 @@ export async function assertContractExists(address) {
|
|
|
236
237
|
* Nothing errored; the in-chain renderer read ASCII where it expected bytes and drew garbage. So a
|
|
237
238
|
* `Bytes` value must state its encoding: `0x…` hex, or `--file` for real binary. A bare string is
|
|
238
239
|
* refused rather than guessed at — there is no safe guess between "these characters" and "these
|
|
239
|
-
* bytes", and the failure is invisible until
|
|
240
|
+
* bytes", and the failure is invisible until a work renders wrong.
|
|
240
241
|
*/
|
|
241
242
|
export function encodePayloadParam(typeName, valueInput, key) {
|
|
242
243
|
if (typeName === 'String')
|
|
@@ -353,7 +354,7 @@ const HOOK_ROLES = ['configure', 'augment', 'transfer'];
|
|
|
353
354
|
const HOOK_NOTE = {
|
|
354
355
|
configure: 'write-time veto/validator (a configure tx reverts if it reverts)',
|
|
355
356
|
augment: 'read-time derivation folded into tokenData (live view reads chain per view)',
|
|
356
|
-
transfer: 'ownership-change
|
|
357
|
+
transfer: 'ownership-change VETO — its revert fails the transfer, and a mint too (mint = transfer from 0x0)',
|
|
357
358
|
};
|
|
358
359
|
const eqAddr = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
359
360
|
/** Parse a hook-address flag: a 0x address, or `none`/`zero`/`0`/`0x0` to clear that role. */
|
|
@@ -431,6 +432,13 @@ export async function cmdSetParamHooks(address, flags) {
|
|
|
431
432
|
const changed = !eqAddr(cur[r], next[r]);
|
|
432
433
|
console.log(` ${r.padEnd(10)} ${changed ? `${label(cur[r])} → ${label(next[r])}` : `${label(cur[r])} ${dim('(unchanged)')}`}`);
|
|
433
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
|
+
}
|
|
434
442
|
const owner = await read(contract, 'owner');
|
|
435
443
|
await runWrite(contract, prepareSetParamHooks({ contract, configureHook: next.configure, augmentHook: next.augment, transferHook: next.transfer, chainId: chainId() }), flags, owner);
|
|
436
444
|
}
|
|
@@ -491,8 +499,56 @@ export async function cmdLockDependencies(address, flags) {
|
|
|
491
499
|
const contract = requireAddress(address, 'abx lock-dependencies <address> [--sign|--unsigned]');
|
|
492
500
|
const owner = await read(contract, 'owner');
|
|
493
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."));
|
|
494
503
|
await runWrite(contract, prepareLockDependencies({ contract, chainId: chainId() }), flags, owner);
|
|
495
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
|
+
}
|
|
496
552
|
// ── editions: shared small parsers ────────────────────────────────────────────
|
|
497
553
|
/** A non-negative-integer edition flag (`--token-id`, `--amount`, …) — the shared parse +
|
|
498
554
|
* bound-check so every edition command that takes "an id" or "a count" rejects the same way. */
|
|
@@ -554,9 +610,9 @@ export async function cmdMint(address, flags) {
|
|
|
554
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.`);
|
|
555
611
|
}
|
|
556
612
|
// OneOfOneEdition's id space is fixed to {0}, so --token-id defaults there; EditionImage/
|
|
557
|
-
// EditionCode ids are caller-named
|
|
613
|
+
// EditionCode ids are caller-named works with no sensible default — require it.
|
|
558
614
|
if (kind.kind !== '1of1-edition' && flags['token-id'] === undefined) {
|
|
559
|
-
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. ` +
|
|
560
616
|
`See existing ids with \`abx tokens ${contract}\`.`);
|
|
561
617
|
}
|
|
562
618
|
const tokenId = flags['token-id'] !== undefined ? parseEditionCountFlag(flags['token-id'], 'token-id') : 0n;
|
|
@@ -697,7 +753,7 @@ export async function cmdSetMaxSupply(address, flags) {
|
|
|
697
753
|
console.log(dim(` #${tokenId} supply cap → ${cap}`));
|
|
698
754
|
await runWrite(contract, prepareSetMaxSupply({ contract, tokenId, cap, chainId: chainId() }), flags, owner);
|
|
699
755
|
}
|
|
700
|
-
// ── ping-uri (editions only) —
|
|
756
|
+
// ── ping-uri (editions only) — the OWNER's URI re-emission ───────────────────────────────────────
|
|
701
757
|
/** How many ids one `pingURI` call carries per transaction — large enough that a normal collection
|
|
702
758
|
* fits in one tx, small enough that a genuinely huge id list doesn't build one unbounded call. */
|
|
703
759
|
const PING_URI_CHUNK_SIZE = 200;
|
|
@@ -716,13 +772,14 @@ export async function cmdPingUri(address, flags) {
|
|
|
716
772
|
const kind = await detectTokenKind(publicClient, contract);
|
|
717
773
|
if (!kind.isEdition) {
|
|
718
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 ` +
|
|
719
|
-
`(
|
|
775
|
+
`(both lanes emit ERC-4906 automatically on a URI change; this command is for indexers that ` +
|
|
776
|
+
`honor only the native 1155 event).`);
|
|
720
777
|
}
|
|
721
778
|
const ids = parseTokenIdRange(requireFlag(flags, 'token-ids', usage));
|
|
722
779
|
const chunks = [];
|
|
723
780
|
for (let i = 0; i < ids.length; i += PING_URI_CHUNK_SIZE)
|
|
724
781
|
chunks.push(ids.slice(i, i + PING_URI_CHUNK_SIZE));
|
|
725
|
-
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`));
|
|
726
783
|
for (const [i, chunk] of chunks.entries()) {
|
|
727
784
|
if (chunks.length > 1)
|
|
728
785
|
console.log(dim(` chunk ${i + 1}/${chunks.length}: ${chunk.length} id(s) (#${chunk[0]}–#${chunk[chunk.length - 1]})`));
|
|
@@ -748,9 +805,9 @@ export async function cmdUnpause(address, flags) {
|
|
|
748
805
|
* scope, so one value covers the whole project — they project into `contractURI` on the resolver
|
|
749
806
|
* AND the on-chain renderer. Works for every token type (1/1 · Series · code). */
|
|
750
807
|
export const AUTHORSHIP_DEPLOY_FIELDS = [
|
|
751
|
-
['
|
|
808
|
+
['creator', F.creator],
|
|
752
809
|
['display-notes', F.displayNotes],
|
|
753
|
-
['
|
|
810
|
+
['creator-links', F.creatorLinks],
|
|
754
811
|
['license', F.license],
|
|
755
812
|
];
|
|
756
813
|
/** Build the on-chain (inline) collection fields for whichever authorship/rights deploy flags are
|
|
@@ -816,24 +873,13 @@ export async function cmdRefresh(address, flags) {
|
|
|
816
873
|
console.log(` ${dim('OpenSea ')}${osBase}/assets/${osChain}/${contract}/${tokenId}`);
|
|
817
874
|
if (explorer)
|
|
818
875
|
console.log(` ${dim('Etherscan')} ${explorer}/token/${contract}?a=${tokenId}`);
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
826
|
-
const isEdition = await isEditionContract(makePublicClient({ chainKey: CHAIN }), contract).catch(() => null);
|
|
827
|
-
if (isEdition === true) {
|
|
828
|
-
console.log(dim(' (an ERC-1155 edition emits NO ERC-4906 — nothing auto-pinged. After a URI/renderer re-point also run:'));
|
|
829
|
-
console.log(dim(` \`abx ping-uri ${contract} --token-ids ${tokenId}\` to re-emit the native URI event for indexers that honor it.)`));
|
|
830
|
-
}
|
|
831
|
-
else if (isEdition === false) {
|
|
832
|
-
console.log(dim(' (ERC-4906 already pings 4906-aware marketplaces on URI changes; this covers the genesis mint + the rest.)'));
|
|
833
|
-
}
|
|
834
|
-
else {
|
|
835
|
-
console.log(dim(' (on a 721, ERC-4906 already pinged 4906-aware marketplaces; on an ERC-1155 edition nothing did — run `abx ping-uri` there.)'));
|
|
836
|
-
}
|
|
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.
|
|
882
|
+
console.log(dim(' (ERC-4906 already pings 4906-aware marketplaces on URI changes; this covers the genesis mint + the rest.)'));
|
|
837
883
|
}
|
|
838
884
|
// ── transfer ─────────────────────────────────────────────────────────────────
|
|
839
885
|
export async function cmdTransfer(address, flags) {
|
|
@@ -844,7 +890,7 @@ export async function cmdTransfer(address, flags) {
|
|
|
844
890
|
// `--token-id` is accepted as an alias for `--token`: EVERY sibling id-taking command (`mint`,
|
|
845
891
|
// `set-max-supply`, `minter …`) spells it `--token-id`, so an agent that learned the name there
|
|
846
892
|
// passed it here — where it was silently ignored and id 0 moved instead of the id they named.
|
|
847
|
-
// On an edition that means transferring the WRONG
|
|
893
|
+
// On an edition that means transferring the WRONG WORK with no warning. Both spellings work now;
|
|
848
894
|
// disagreeing values are refused rather than silently preferring one.
|
|
849
895
|
if (flags.token !== undefined && flags['token-id'] !== undefined && flags.token !== flags['token-id']) {
|
|
850
896
|
throw new Error(`--token ${flags.token} and --token-id ${flags['token-id']} disagree — pass one (they are aliases for the same id).`);
|
|
@@ -1016,21 +1062,26 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
1016
1062
|
`Enrollment can never be added to a live collection; if enforcement is required, redeploy with ` +
|
|
1017
1063
|
`--721c recommended (or --721c 0x…) on the deploy command.`);
|
|
1018
1064
|
}
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
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.
|
|
1021
1070
|
if (validator !== zeroAddress) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
catch (err) {
|
|
1027
|
-
throw new Error(`couldn't verify the validator has code at ${validator} (${err.message}) — refusing to re-point blind; retry when the RPC answers.`);
|
|
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.`);
|
|
1028
1074
|
}
|
|
1029
|
-
if (
|
|
1075
|
+
if (probe.verdict !== 'ok') {
|
|
1030
1076
|
const rec = resolveRecommendedTransferValidator(cid);
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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.`);
|
|
1034
1085
|
}
|
|
1035
1086
|
}
|
|
1036
1087
|
if (eqAddr(status.validator, validator)) {
|
|
@@ -1042,6 +1093,167 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
1042
1093
|
const owner = await read(contract, 'owner');
|
|
1043
1094
|
await runWrite(contract, prepareSetTransferValidator({ contract, validator, chainId: cid }), flags, owner);
|
|
1044
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
|
+
}
|
|
1045
1257
|
// ── multi-chunk on-chain content (the `reader` path) ─────────────────────────
|
|
1046
1258
|
// The plan math (chunk count + tx shape), the cost-model constants, and the actual
|
|
1047
1259
|
// plan→resolve-store→stage→encode orchestration all live in the SDK's staging.ts, layered on
|
|
@@ -1105,13 +1317,55 @@ export function sessionStagingSender(session) {
|
|
|
1105
1317
|
return async (tx) => (await session.send(tx)).receipt;
|
|
1106
1318
|
}
|
|
1107
1319
|
const kb = (n) => `${n < 10 * 1024 ? (n / 1024).toFixed(1) : Math.round(n / 1024)}KB`;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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;
|
|
1115
1369
|
}
|
|
1116
1370
|
/**
|
|
1117
1371
|
* Pure staging plan for a piece of content — chunk count + transaction shape — with NO chain
|
|
@@ -1158,8 +1412,9 @@ function planTxLine(p, finalLabel) {
|
|
|
1158
1412
|
* These store writes use the env key (the data contracts are ownerless); the owner only
|
|
1159
1413
|
* signs the field set that references the manifest.
|
|
1160
1414
|
*/
|
|
1161
|
-
async function putContentOnChain(bytes, compress, field, send, finalLabel = '1 owner field-set', store)
|
|
1162
|
-
|
|
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);
|
|
1163
1418
|
const content = compress === 'gzip' ? new Uint8Array(gzipSync(bytes)) : new Uint8Array(bytes);
|
|
1164
1419
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1165
1420
|
return stageFieldContent({
|
|
@@ -1189,10 +1444,12 @@ async function putContentOnChain(bytes, compress, field, send, finalLabel = '1 o
|
|
|
1189
1444
|
* true count instead of leaving the agent to guess). The final tx here is the deploy that
|
|
1190
1445
|
* bakes in the reader field, so the count is staging tx(s) + the deploy.
|
|
1191
1446
|
*/
|
|
1192
|
-
export function previewImageStaging(imagePath, compress) {
|
|
1447
|
+
export function previewImageStaging(imagePath, compress, allowUnreadable = false) {
|
|
1193
1448
|
const bytes = readFileSync(resolvePath(imagePath));
|
|
1194
1449
|
const p = computeContentPlan(bytes, compress);
|
|
1195
|
-
|
|
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);
|
|
1196
1453
|
return (`would stage ${basename(resolvePath(imagePath))} on-chain (chunk store): ` +
|
|
1197
1454
|
`${planSizeLine(bytes.length, p, compress)} as ${p.representation}; ` +
|
|
1198
1455
|
`${planTxLine(p, '1 deploy (bakes the reader field + mints)')}`);
|
|
@@ -1204,9 +1461,9 @@ export function previewImageStaging(imagePath, compress) {
|
|
|
1204
1461
|
* image" is just those staging tx(s) + the single deploy — no post-deploy `set-field`.
|
|
1205
1462
|
* The manifest is deployer-independent, so this is computed once and reused for any signer.
|
|
1206
1463
|
*/
|
|
1207
|
-
export async function stageImageField(imagePath, compress, send) {
|
|
1464
|
+
export async function stageImageField(imagePath, compress, send, allowUnreadable = false) {
|
|
1208
1465
|
const bytes = readFileSync(resolvePath(imagePath));
|
|
1209
|
-
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);
|
|
1210
1467
|
return {
|
|
1211
1468
|
field: { field: encodeTag(F.image), representation: encodeTag(representation), value },
|
|
1212
1469
|
note: `image: ${bytes.length}B staged ON-CHAIN via reader (${representation}) — baked into the deploy`,
|
|
@@ -1216,15 +1473,15 @@ export async function stageImageField(imagePath, compress, send) {
|
|
|
1216
1473
|
* Stage MANY images on-chain against a **single, shared** chunk store — the batch form of
|
|
1217
1474
|
* {@link stageImageField} for a Series. The store is resolved/deployed **once** (not per token),
|
|
1218
1475
|
* then every file is staged against it and returned as its own `reader`-backed `image` field.
|
|
1219
|
-
* 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
|
|
1220
1477
|
* share the store contract. Returns the fields in input order (token order).
|
|
1221
1478
|
*/
|
|
1222
|
-
export async function stageImageFieldsBatch(imagePaths, compress, send, storeOverride) {
|
|
1479
|
+
export async function stageImageFieldsBatch(imagePaths, compress, send, storeOverride, allowUnreadable = false) {
|
|
1223
1480
|
const store = await ensureChunkStore(send, storeOverride);
|
|
1224
1481
|
const fields = [];
|
|
1225
1482
|
for (const imagePath of imagePaths) {
|
|
1226
1483
|
const bytes = readFileSync(resolvePath(imagePath));
|
|
1227
|
-
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);
|
|
1228
1485
|
fields.push({ field: encodeTag(F.image), representation: encodeTag(representation), value });
|
|
1229
1486
|
}
|
|
1230
1487
|
return { fields, store };
|
|
@@ -1299,6 +1556,8 @@ export async function cmdSetField(address, flags) {
|
|
|
1299
1556
|
if (staging && isDryRun(flags)) {
|
|
1300
1557
|
const bytes = readFileSync(resolvePath(flags.file));
|
|
1301
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));
|
|
1302
1561
|
console.log(`\n ${bold('◆ set-field ' + field)} ${dim('(dry run — nothing staged or sent)')}`);
|
|
1303
1562
|
console.log(` ${dim('scope'.padEnd(12))} ${collection ? 'collection' : `token #${flags.token ?? '0'}`}`);
|
|
1304
1563
|
console.log(` ${planSizeLine(bytes.length, p, parseCompress(flags.compress))}`);
|
|
@@ -1330,7 +1589,7 @@ export async function cmdSetField(address, flags) {
|
|
|
1330
1589
|
});
|
|
1331
1590
|
try {
|
|
1332
1591
|
await session.connect();
|
|
1333
|
-
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));
|
|
1334
1593
|
await session.send(buildTx(value, representation));
|
|
1335
1594
|
}
|
|
1336
1595
|
finally {
|
|
@@ -1344,7 +1603,7 @@ export async function cmdSetField(address, flags) {
|
|
|
1344
1603
|
let representation;
|
|
1345
1604
|
if (staging) {
|
|
1346
1605
|
const compress = parseCompress(flags.compress);
|
|
1347
|
-
({ 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)));
|
|
1348
1607
|
}
|
|
1349
1608
|
else if (flags.text && flags.text !== 'true') {
|
|
1350
1609
|
value = toHex(flags.text);
|
|
@@ -1475,13 +1734,13 @@ export async function cmdAttach(rest, flags) {
|
|
|
1475
1734
|
const uriBase = await read(contract, 'tokenURIBase').catch(() => '');
|
|
1476
1735
|
const artifactsPath = `/t/${chainId()}/${contract}/${flags.token ?? '0'}`;
|
|
1477
1736
|
if (uriBase && uriBase.trim() !== '') {
|
|
1478
|
-
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).`));
|
|
1479
1738
|
}
|
|
1480
1739
|
else {
|
|
1481
1740
|
console.log(yellow(' ⚠ ') +
|
|
1482
|
-
`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 — ` +
|
|
1483
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. ` +
|
|
1484
|
-
dim('(
|
|
1743
|
+
dim('(Params are chain state, readable with abx tokens and by any contract call — attachments are the surface that needs a resolver.)'));
|
|
1485
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}.`));
|
|
1486
1745
|
}
|
|
1487
1746
|
// ONE transaction for the whole set. `batchOps` folds a same-target run into a `multicall` and
|
|
@@ -1506,7 +1765,7 @@ export async function cmdAttach(rest, flags) {
|
|
|
1506
1765
|
dim(` verify (a resolver serves the complete listing): `) +
|
|
1507
1766
|
`abx tokenuri ${contract}${id === '0' ? '' : ` --token ${id}`} --fetch` +
|
|
1508
1767
|
dim(` ${collection ? '' : `→ artifacts[].key ${pairs.map((p) => `"${p.key}"`).join(', ')}; /data/<key> fetches each`}\n`) +
|
|
1509
|
-
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`));
|
|
1510
1769
|
}
|
|
1511
1770
|
}
|
|
1512
1771
|
// ── lock-field ───────────────────────────────────────────────────────────────
|
|
@@ -1514,9 +1773,9 @@ export async function cmdAttach(rest, flags) {
|
|
|
1514
1773
|
* Refuse `lock-field` on a name that is a declared PARAM key.
|
|
1515
1774
|
*
|
|
1516
1775
|
* Fields and params are two separate namespaces that may share a name, and `lock-field` only ever
|
|
1517
|
-
* 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
|
|
1518
1777
|
* `lock-field --field grid`, got "permanent", got `tokenFieldLocked(0,"grid") == true`, and then
|
|
1519
|
-
* overwrote the
|
|
1778
|
+
* overwrote the work with `configure-param` on the next call. Every individual statement the CLI
|
|
1520
1779
|
* made was true; together they promised a protection that did not exist. Permanence is the pitch, so
|
|
1521
1780
|
* this refuses rather than warns, and names the mechanism that actually welds a param.
|
|
1522
1781
|
*/
|
|
@@ -1581,13 +1840,14 @@ export async function cmdSetRenderer(address, flags) {
|
|
|
1581
1840
|
}
|
|
1582
1841
|
// ── lock-uri (freeze the URI config) ──────────────────────────────────────────
|
|
1583
1842
|
/** Freeze a scope's URI config (pointer + renderer) forever. With the fields also
|
|
1584
|
-
* locked, the
|
|
1585
|
-
*
|
|
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`. */
|
|
1586
1846
|
export async function cmdLockUri(address, flags) {
|
|
1587
1847
|
const contract = requireAddress(address, 'abx lock-uri <address> [--collection] [--sign|--unsigned]');
|
|
1588
1848
|
const collection = !!flags.collection;
|
|
1589
1849
|
const owner = await read(contract, 'owner');
|
|
1590
|
-
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.`));
|
|
1591
1851
|
const tx = collection
|
|
1592
1852
|
? prepareLockContractURI({ contract, chainId: chainId() })
|
|
1593
1853
|
: prepareLockTokenURI({ contract, chainId: chainId() });
|
|
@@ -1710,7 +1970,7 @@ function parseSalePrice(flags, usage) {
|
|
|
1710
1970
|
* the token, set a primary payee) and whether the token is paused.
|
|
1711
1971
|
*
|
|
1712
1972
|
* On an EDITION (OneOfOneEdition/EditionImage/EditionCode), `--token-id` is REQUIRED (sales are
|
|
1713
|
-
* keyed `(token, id)` — a per-
|
|
1973
|
+
* keyed `(token, id)` — a per-work price, not one project-wide sale) and the sale routes to the
|
|
1714
1974
|
* shared `AbxFixedPriceMinter1155` instead. Refused on a 721 target.
|
|
1715
1975
|
*/
|
|
1716
1976
|
export async function cmdMinterConfigure(address, flags) {
|
|
@@ -1753,10 +2013,15 @@ export async function cmdMinterConfigure(address, flags) {
|
|
|
1753
2013
|
console.log(dim(` price ${isEth ? `${formatEther(price)} ETH` : `${price} units of ${erc20}`} / copy`));
|
|
1754
2014
|
console.log(dim(` allocation ${allocation}`));
|
|
1755
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.
|
|
1756
2021
|
const [assignedMinter, payee, paused] = await Promise.all([
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2022
|
+
readEdition(token, 'minter').catch(() => zeroAddress),
|
|
2023
|
+
readEdition(token, 'primaryPayee').catch(() => zeroAddress),
|
|
2024
|
+
readEdition(token, 'paused').catch(() => false),
|
|
1760
2025
|
]);
|
|
1761
2026
|
const assigned = assignedMinter.toLowerCase() === minter.toLowerCase();
|
|
1762
2027
|
console.log('');
|
|
@@ -1931,15 +2196,17 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1931
2196
|
throw new Error(`no sale configured for ${token} #${tokenId} — run: abx minter configure ${token} --token-id ${tokenId} …`);
|
|
1932
2197
|
const isEth = sale.paymentToken === zeroAddress;
|
|
1933
2198
|
const to = flags.to && flags.to !== 'true' ? flags.to : undefined;
|
|
1934
|
-
// Payment math
|
|
1935
|
-
//
|
|
1936
|
-
|
|
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;
|
|
1937
2204
|
if (!isEth) {
|
|
1938
|
-
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).`));
|
|
1939
2206
|
}
|
|
1940
2207
|
console.log(dim(` buying ${quantity} cop${quantity === 1n ? 'y' : 'ies'} of #${tokenId}${to ? ` → ${to}` : ' → signer'} for ` +
|
|
1941
|
-
`${isEth ? `${formatEther(
|
|
1942
|
-
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);
|
|
1943
2210
|
console.log(dim(` next: \`abx refresh ${token}\` so marketplaces pick up the change.`));
|
|
1944
2211
|
return;
|
|
1945
2212
|
}
|
|
@@ -1954,12 +2221,13 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1954
2221
|
throw new Error(`no sale configured for ${token} — run: abx minter configure ${token} …`);
|
|
1955
2222
|
const isEth = sale.paymentToken === zeroAddress;
|
|
1956
2223
|
const to = flags.to && flags.to !== 'true' ? flags.to : undefined;
|
|
1957
|
-
const value = isEth ? sale.price : 0n;
|
|
1958
2224
|
if (!isEth) {
|
|
1959
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).`));
|
|
1960
2226
|
}
|
|
1961
2227
|
console.log(dim(` buying 1 token${to ? ` → ${to}` : ' → signer'} for ${isEth ? `${formatEther(sale.price)} ETH` : `${sale.price} units`}`));
|
|
1962
|
-
|
|
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);
|
|
1963
2231
|
console.log(dim(` next: \`abx refresh ${token}\` so marketplaces pick up the new token.`));
|
|
1964
2232
|
}
|
|
1965
2233
|
// ── PostParam schemas, after deploy ──────────────────────────────────────────
|