@artblocks/abx-cli 0.1.0-alpha.19 → 0.1.0-alpha.20
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 +68 -0
- package/dist/commands/deploy.d.ts +32 -7
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1079 -49
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/project.d.ts +107 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +141 -16
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +52 -20
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts +13 -11
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +73 -37
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/kind.d.ts +48 -0
- package/dist/kind.d.ts.map +1 -0
- package/dist/kind.js +69 -0
- package/dist/kind.js.map +1 -0
- package/dist/main.js +107 -23
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts +14 -1
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +166 -4
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +11 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +133 -3
- package/dist/output.js.map +1 -1
- package/dist/ownerops.d.ts +33 -2
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +356 -19
- package/dist/ownerops.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +5 -3
- package/skill/reference/creator-token.md +40 -25
- package/skill/reference/hosting.md +1 -1
- package/skill/reference/operating.md +20 -0
- package/skill/reference/troubleshooting.md +7 -0
package/dist/ownerops.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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, makeHotSender, makePublicClient, makeWalletClient, oneOfOneImageAbi, seriesImageAbi, deployFixedPriceMinter, prepareConfigureSale, preparePurchase, readSaleConfig, 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, 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';
|
|
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
12
|
import { decodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
@@ -16,8 +16,9 @@ import { readFileSync } from 'node:fs';
|
|
|
16
16
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
17
17
|
import { gzipSync } from 'node:zlib';
|
|
18
18
|
import { formatEther, getAddress, isAddress, parseEther, toHex, zeroAddress } from 'viem';
|
|
19
|
-
import { CHAIN, chainId, fixedPriceMinterAddress, localIndexer } from './config.js';
|
|
19
|
+
import { CHAIN, chainId, fixedPriceMinterAddress, fixedPriceMinter1155Address, localIndexer } from './config.js';
|
|
20
20
|
import { CliError } from './errors.js';
|
|
21
|
+
import { detectTokenKind } from './kind.js';
|
|
21
22
|
import { openWalletSession } from './signer.js';
|
|
22
23
|
import { gatedSend, laneFromFlags } from './riskgate.js';
|
|
23
24
|
import { withJson } from './jsonout.js';
|
|
@@ -375,8 +376,12 @@ export async function cmdSetParamHooks(address, flags) {
|
|
|
375
376
|
}
|
|
376
377
|
catch (err) {
|
|
377
378
|
await assertContractExists(contract); // no contract at all → the actionable no-contract error
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
// Name the ACTUAL kind rather than assuming 721 — a 1/1-edition or EditionImage has no param
|
|
380
|
+
// hooks either (only EditionCode composes ConfigurableParams, mirroring SeriesCode), so the old
|
|
381
|
+
// blanket "a 1/1 or a plain Series" message misnamed an edition target's real kind.
|
|
382
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
383
|
+
throw new Error(`${contract} exposes no param hooks — the configure/augment/transfer hooks are a SeriesCode/EditionCode ` +
|
|
384
|
+
`(PostParams) feature. ${kind.label} has no configurable params to hook.`);
|
|
380
385
|
}
|
|
381
386
|
const cur = { configure: current[0], augment: current[1], transfer: current[2] };
|
|
382
387
|
const label = (a) => (eqAddr(a, zeroAddress) ? dim('none') : a);
|
|
@@ -466,21 +471,96 @@ export async function cmdLockDependencies(address, flags) {
|
|
|
466
471
|
console.log(dim(' note: locking the dependency set is permanent and irreversible (list AND registry pointer freeze).'));
|
|
467
472
|
await runWrite(contract, prepareLockDependencies({ contract, chainId: chainId() }), flags, owner);
|
|
468
473
|
}
|
|
474
|
+
// ── editions: shared small parsers ────────────────────────────────────────────
|
|
475
|
+
/** A non-negative-integer edition flag (`--token-id`, `--amount`, …) — the shared parse +
|
|
476
|
+
* bound-check so every edition command that takes "an id" or "a count" rejects the same way. */
|
|
477
|
+
export function parseEditionCountFlag(raw, flag) {
|
|
478
|
+
if (raw === 'true' || !/^\d+$/.test(raw.trim()))
|
|
479
|
+
throw new Error(`--${flag} must be a non-negative integer; got '${raw}'.`);
|
|
480
|
+
return BigInt(raw.trim());
|
|
481
|
+
}
|
|
482
|
+
/** `--token-ids <csv|range>` — the shape `ping-uri` (and any future batch-by-id command) takes: a
|
|
483
|
+
* comma-separated list of ids and/or `lo-hi` ranges (`0-9,20,25-30`), de-duplicated and sorted. */
|
|
484
|
+
export function parseTokenIdRange(raw) {
|
|
485
|
+
const ids = new Set();
|
|
486
|
+
for (const part of raw.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
487
|
+
const range = part.match(/^(\d+)-(\d+)$/);
|
|
488
|
+
if (range) {
|
|
489
|
+
const [lo, hi] = [BigInt(range[1]), BigInt(range[2])];
|
|
490
|
+
if (lo > hi)
|
|
491
|
+
throw new Error(`--token-ids: range '${part}' has lo > hi.`);
|
|
492
|
+
for (let i = lo; i <= hi; i++)
|
|
493
|
+
ids.add(i);
|
|
494
|
+
}
|
|
495
|
+
else if (/^\d+$/.test(part)) {
|
|
496
|
+
ids.add(BigInt(part));
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
throw new Error(`--token-ids: '${part}' isn't a token id or a 'lo-hi' range.`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!ids.size)
|
|
503
|
+
throw new Error("--token-ids needs at least one id (e.g. '0,1,2' or '0-99').");
|
|
504
|
+
return [...ids].sort((a, b) => Number(a - b));
|
|
505
|
+
}
|
|
469
506
|
// ── mint ─────────────────────────────────────────────────────────────────────
|
|
470
507
|
// Issue a token — a deferred mint (deploy → warm resolver → mint) or a primary sale
|
|
471
508
|
// (mint straight to the buyer). For a 1/1 this is the one-shot token #0. For a Series
|
|
472
509
|
// it mints the next sequential token id (metadata = token id): bare (one), or `--count <n>`
|
|
473
510
|
// (n in order). Owner or an authorized minter signs.
|
|
511
|
+
// For an EDITION (OneOfOneEdition/EditionImage/EditionCode) this mints copies of ONE id:
|
|
512
|
+
// --token-id (required unless the target is a 1/1-edition, whose id space is fixed to {0}) +
|
|
513
|
+
// --amount (default 1). --count is the 721 Series primitive and is refused on an edition.
|
|
474
514
|
export async function cmdMint(address, flags) {
|
|
475
|
-
const usage = 'abx mint <address> [--to 0x…] [--count <n>] [--json] [--sign|--unsigned]';
|
|
515
|
+
const usage = 'abx mint <address> [--to 0x…] [--count <n> | --token-id <id> --amount <n>] [--json] [--sign|--unsigned]';
|
|
476
516
|
const contract = requireAddress(address, usage);
|
|
477
517
|
// `--json`: the TOKEN ID is the value a program came for, and it was previously only obtainable by
|
|
478
518
|
// regex-scraping coloured prose (B19). It is read from the mint's own Transfer logs rather than by
|
|
479
519
|
// re-reading `nextTokenId` afterwards — a concurrent mint would make that answer wrong, and a
|
|
480
520
|
// number that is usually right is worse than no number.
|
|
481
521
|
return withJson(flags, async () => {
|
|
522
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
523
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
482
524
|
const owner = await read(contract, 'owner');
|
|
483
525
|
const to = flags.to ?? owner; // default: pre-mint to the admin
|
|
526
|
+
if (kind.isEdition) {
|
|
527
|
+
// `--count` is the 721 Series primitive (N sequential ids); an edition mint always names its
|
|
528
|
+
// id and an amount — there is no "next in order" to count through. Refuse, don't silently
|
|
529
|
+
// reinterpret it as something else.
|
|
530
|
+
if (flags.count !== undefined) {
|
|
531
|
+
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
|
+
}
|
|
533
|
+
// OneOfOneEdition's id space is fixed to {0}, so --token-id defaults there; EditionImage/
|
|
534
|
+
// EditionCode ids are caller-named artworks with no sensible default — require it.
|
|
535
|
+
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 artworks, not a single default. ` +
|
|
537
|
+
`See existing ids with \`abx tokens ${contract}\`.`);
|
|
538
|
+
}
|
|
539
|
+
const tokenId = flags['token-id'] !== undefined ? parseEditionCountFlag(flags['token-id'], 'token-id') : 0n;
|
|
540
|
+
const amount = flags.amount !== undefined ? parseEditionCountFlag(flags.amount, 'amount') : 1n;
|
|
541
|
+
console.log(dim(` minting ${amount} cop${amount === 1n ? 'y' : 'ies'} of #${tokenId} → ${to}${flags.to ? '' : ' (owner — pass --to for a buyer)'}`));
|
|
542
|
+
const tx = prepareEditionMint({ contract, to, tokenId, amount, chainId: chainId() });
|
|
543
|
+
const result = await runWrite(contract, tx, flags, owner);
|
|
544
|
+
if (result)
|
|
545
|
+
console.log(` ${green('✓')} minted ${amount} cop${amount === 1n ? 'y' : 'ies'} of ${bold('#' + tokenId)} → ${to}`);
|
|
546
|
+
console.log(dim(` next: \`abx refresh ${contract}\` so marketplaces pick up the change.`));
|
|
547
|
+
return {
|
|
548
|
+
contract,
|
|
549
|
+
chainId: chainId(),
|
|
550
|
+
to,
|
|
551
|
+
sent: !!result,
|
|
552
|
+
txHash: result?.txHash ?? null,
|
|
553
|
+
blockNumber: result ? String(result.blockNumber) : null,
|
|
554
|
+
tokenIds: [tokenId.toString()],
|
|
555
|
+
amount: amount.toString(),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// 721 path (unchanged): --token-id/--amount have no meaning here — refuse rather than silently
|
|
559
|
+
// ignore (the membrane rule: enforce, don't warn).
|
|
560
|
+
if (flags['token-id'] !== undefined || flags.amount !== undefined) {
|
|
561
|
+
throw new Error(`--token-id/--amount are edition-only (ERC-1155 copies) — ${contract} is a ${kind.label} (721). ` +
|
|
562
|
+
`Mint the next token with \`abx mint ${contract}\`${kind.kind !== '1of1' ? ' (or --count N for several in order)' : ''}.`);
|
|
563
|
+
}
|
|
484
564
|
let tx;
|
|
485
565
|
let count = 1n;
|
|
486
566
|
if (flags.count !== undefined && flags.count !== 'true') {
|
|
@@ -535,6 +615,67 @@ export async function cmdSetMaxInvocations(address, flags) {
|
|
|
535
615
|
const owner = await read(contract, 'owner');
|
|
536
616
|
await runWrite(contract, prepareSetMaxInvocations({ contract, maxInvocations: max, chainId: chainId() }), flags, owner);
|
|
537
617
|
}
|
|
618
|
+
// ── set-max-supply (editions only) — the per-id twin of set-max-invocations ──────────────────────
|
|
619
|
+
/**
|
|
620
|
+
* `abx set-max-supply <address> --token-id <n> --cap <n>` — owner overrides ONE id's ERC-1155
|
|
621
|
+
* supply cap (Edition Supply extension). Monotonically non-increasing once used for that id — the
|
|
622
|
+
* chain enforces it (`MaxSupplyIncreaseForbidden`/`MaxSupplyBelowFloor`), and `--cap open` is
|
|
623
|
+
* refused HERE, before any gas: the contract's own "0" means "closed forever" once an id has been
|
|
624
|
+
* overridden (not "reopen to uncapped" — `EditionSupply.sol`'s own doc calls this out), so accepting
|
|
625
|
+
* `open` as an alias for 0 would silently promise the opposite of what it does on a re-run.
|
|
626
|
+
*/
|
|
627
|
+
export async function cmdSetMaxSupply(address, flags) {
|
|
628
|
+
const usage = 'abx set-max-supply <address> --token-id <n> --cap <n> [--sign|--unsigned]';
|
|
629
|
+
const contract = requireAddress(address, usage);
|
|
630
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
631
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
632
|
+
if (!kind.isEdition) {
|
|
633
|
+
throw new Error(`set-max-supply is edition-only (a per-id ERC-1155 cap) — ${contract} is a ${kind.label} (721). Use \`abx set-max-invocations\` instead (the whole-project cap).`);
|
|
634
|
+
}
|
|
635
|
+
const tokenId = parseEditionCountFlag(requireFlag(flags, 'token-id', usage), 'token-id');
|
|
636
|
+
const capRaw = requireFlag(flags, 'cap', usage);
|
|
637
|
+
if (capRaw.trim().toLowerCase() === 'open') {
|
|
638
|
+
throw new Error(`--cap open is refused: once you set a cap for #${tokenId} it can only DECREASE (never increase back to uncapped) — ` +
|
|
639
|
+
`"open" would ask for exactly the increase the chain forbids. If this id has never been overridden, it's already open ` +
|
|
640
|
+
`(the --copies default from deploy) — there's nothing to set. To lower an existing cap, pass the number.`);
|
|
641
|
+
}
|
|
642
|
+
const cap = parseEditionCountFlag(capRaw, 'cap');
|
|
643
|
+
const owner = await read(contract, 'owner');
|
|
644
|
+
console.log(dim(` #${tokenId} supply cap → ${cap}`));
|
|
645
|
+
await runWrite(contract, prepareSetMaxSupply({ contract, tokenId, cap, chainId: chainId() }), flags, owner);
|
|
646
|
+
}
|
|
647
|
+
// ── ping-uri (editions only) — permissionless URI re-emission ────────────────────────────────────
|
|
648
|
+
/** How many ids one `pingURI` call carries per transaction — large enough that a normal collection
|
|
649
|
+
* fits in one tx, small enough that a genuinely huge id list doesn't build one unbounded call. */
|
|
650
|
+
const PING_URI_CHUNK_SIZE = 200;
|
|
651
|
+
/**
|
|
652
|
+
* `abx ping-uri <address> --token-ids <csv|range>` — re-emit the native ERC-1155 `URI` event for
|
|
653
|
+
* the given ids after a contract-wide re-point (`set-token-uri`/`set-renderer`), so marketplaces/
|
|
654
|
+
* indexers that only honor the native event re-index. Permissionless (anyone may call it — it only
|
|
655
|
+
* re-emits already-public, current truth), so there is no owner check and no `expectedSigner` to
|
|
656
|
+
* pin; whichever signer the active lane resolves sends it. Chunks a large id list into several
|
|
657
|
+
* transactions, each through the SAME `--dry-run`/`--confirm` gate as any other write.
|
|
658
|
+
*/
|
|
659
|
+
export async function cmdPingUri(address, flags) {
|
|
660
|
+
const usage = 'abx ping-uri <address> --token-ids <csv|range> [--sign|--unsigned]';
|
|
661
|
+
const contract = requireAddress(address, usage);
|
|
662
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
663
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
664
|
+
if (!kind.isEdition) {
|
|
665
|
+
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
|
+
`(721 emits ERC-4906 MetadataUpdate automatically on a URI change — nothing to re-emit by hand).`);
|
|
667
|
+
}
|
|
668
|
+
const ids = parseTokenIdRange(requireFlag(flags, 'token-ids', usage));
|
|
669
|
+
const chunks = [];
|
|
670
|
+
for (let i = 0; i < ids.length; i += PING_URI_CHUNK_SIZE)
|
|
671
|
+
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'} — permissionless (no owner check) — ${chunks.length} tx${chunks.length === 1 ? '' : '(s)'} of up to ${PING_URI_CHUNK_SIZE} id(s) each`));
|
|
673
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
674
|
+
if (chunks.length > 1)
|
|
675
|
+
console.log(dim(` chunk ${i + 1}/${chunks.length}: ${chunk.length} id(s) (#${chunk[0]}–#${chunk[chunk.length - 1]})`));
|
|
676
|
+
await runWrite(contract, preparePingURI({ contract, tokenIds: chunk, chainId: chainId() }), flags);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
538
679
|
/** Pause minting — restrict it to the owner (config/reserves) until unpaused. */
|
|
539
680
|
export async function cmdPause(address, flags) {
|
|
540
681
|
const contract = requireAddress(address, 'abx pause <address> [--sign|--unsigned]');
|
|
@@ -626,9 +767,33 @@ export async function cmdRefresh(address, flags) {
|
|
|
626
767
|
}
|
|
627
768
|
// ── transfer ─────────────────────────────────────────────────────────────────
|
|
628
769
|
export async function cmdTransfer(address, flags) {
|
|
629
|
-
const
|
|
630
|
-
const
|
|
770
|
+
const usage = 'abx transfer <address> --to 0x… [--token 0] [--amount <n> --from 0x… (editions only)] [--sign|--unsigned]';
|
|
771
|
+
const contract = requireAddress(address, usage);
|
|
772
|
+
const to = requireFlag(flags, 'to', usage);
|
|
631
773
|
const tokenId = BigInt(flags.token ?? '0');
|
|
774
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
775
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
776
|
+
if (kind.isEdition) {
|
|
777
|
+
const amount = flags.amount !== undefined ? parseEditionCountFlag(flags.amount, 'amount') : 1n;
|
|
778
|
+
// Unlike a 721 (one holder, read on-chain via ownerOf), an edition id can have MANY concurrent
|
|
779
|
+
// holders — there is no single "the owner" to move copies from, so the holder must be named.
|
|
780
|
+
const fromRaw = flags.from;
|
|
781
|
+
if (!fromRaw || fromRaw === 'true') {
|
|
782
|
+
throw new Error(`--from 0x.. is required on a ${kind.label} — an edition id can have many concurrent holders, so ` +
|
|
783
|
+
`there is no single current owner to read on-chain (unlike a 721). Pass the holder's address.`);
|
|
784
|
+
}
|
|
785
|
+
if (!isAddress(fromRaw))
|
|
786
|
+
throw new Error(`--from must be a 0x address; got '${fromRaw}'.`);
|
|
787
|
+
const from = getAddress(fromRaw);
|
|
788
|
+
const tx = prepareEditionTransfer({ contract, from, to, tokenId, amount, chainId: chainId() });
|
|
789
|
+
await runWrite(contract, tx, flags, from);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
// 721 path (unchanged): --amount has no meaning — a token transfers as a whole. Refuse rather
|
|
793
|
+
// than silently ignore.
|
|
794
|
+
if (flags.amount !== undefined) {
|
|
795
|
+
throw new Error(`--amount is edition-only (ERC-1155 copies) — ${contract} is a ${kind.label} (721), where a token transfers as a whole. Drop --amount.`);
|
|
796
|
+
}
|
|
632
797
|
const from = await read(contract, 'ownerOf', [tokenId]);
|
|
633
798
|
const tx = prepareTransfer({ contract, from, to, tokenId, chainId: chainId() });
|
|
634
799
|
await runWrite(contract, tx, flags, from);
|
|
@@ -726,7 +891,7 @@ export function parseTransferValidatorValue(raw, opts) {
|
|
|
726
891
|
if (s === 'none' || s === 'zero' || s === '0' || s === '0x0' || s === zeroAddress) {
|
|
727
892
|
if (opts.allowNone)
|
|
728
893
|
return zeroAddress;
|
|
729
|
-
throw new Error(`a zero transfer validator never enrolls — plain
|
|
894
|
+
throw new Error(`a zero transfer validator never enrolls — a plain (unenrolled) token is already the default, so drop --721c. ` +
|
|
730
895
|
`("enrolled but suspended" is not a deploy-time state: enroll with a real validator, then suspend ` +
|
|
731
896
|
`with \`abx set-transfer-validator <address> none\`.)`);
|
|
732
897
|
}
|
|
@@ -768,7 +933,7 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
768
933
|
await assertContractExists(contract);
|
|
769
934
|
const status = await readCreatorTokenStatus(publicClient, contract);
|
|
770
935
|
if (!status.enrolled) {
|
|
771
|
-
throw new Error(`${contract}
|
|
936
|
+
throw new Error(`${contract} did not enroll as a creator token — 721C/1155C enrollment is a deploy-time decision. ` +
|
|
772
937
|
`Enrollment can never be added to a live collection; if enforcement is required, redeploy with ` +
|
|
773
938
|
`--721c recommended (or --721c 0x…) on the deploy command.`);
|
|
774
939
|
}
|
|
@@ -1370,6 +1535,17 @@ async function readSeries(token, fn, args = []) {
|
|
|
1370
1535
|
args: args,
|
|
1371
1536
|
}));
|
|
1372
1537
|
}
|
|
1538
|
+
/** The edition twin of {@link readSeries} — reads a per-id getter (`totalSupply(id)`,
|
|
1539
|
+
* `maxSupply(id)`, `uri(id)`, …) via the narrowest-common-superset edition ABI. */
|
|
1540
|
+
async function readEdition(token, fn, args = []) {
|
|
1541
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1542
|
+
return (await publicClient.readContract({
|
|
1543
|
+
address: token,
|
|
1544
|
+
abi: oneOfOneEditionAbi,
|
|
1545
|
+
functionName: fn,
|
|
1546
|
+
args: args,
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1373
1549
|
/** Resolve the shared minter for the chain (flag → env → manifest), or deploy it if none exists.
|
|
1374
1550
|
* It's ownerless, so any funded signer can stand it up — the sibling of `ensureChunkStore`. */
|
|
1375
1551
|
async function ensureFixedPriceMinter(override) {
|
|
@@ -1400,6 +1576,34 @@ async function ensureFixedPriceMinter(override) {
|
|
|
1400
1576
|
console.log(dim(` not in the shipped manifest for ${CHAIN} — to reuse it set ${bold(`ABX_FIXED_PRICE_MINTER=${minter}`)} (or add it to packages/sdk/src/deployments.ts)`));
|
|
1401
1577
|
return minter;
|
|
1402
1578
|
}
|
|
1579
|
+
/** The edition twin of {@link ensureFixedPriceMinter} — resolves (or deploys) the chain's shared
|
|
1580
|
+
* `AbxFixedPriceMinter1155`, the Minter spine's per-`(token, id)` sale singleton. */
|
|
1581
|
+
async function ensureFixedPriceMinter1155(override) {
|
|
1582
|
+
const known = fixedPriceMinter1155Address(override);
|
|
1583
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1584
|
+
if (known) {
|
|
1585
|
+
const code = await publicClient.getCode({ address: known });
|
|
1586
|
+
if (code && code !== '0x')
|
|
1587
|
+
return known;
|
|
1588
|
+
console.log(yellow(` configured edition minter ${known} has no code on ${CHAIN} — deploying a fresh one`));
|
|
1589
|
+
}
|
|
1590
|
+
const predicted = predictFixedPriceMinter1155();
|
|
1591
|
+
if (!known || known.toLowerCase() !== predicted.toLowerCase()) {
|
|
1592
|
+
const pcode = await publicClient.getCode({ address: predicted });
|
|
1593
|
+
if (pcode && pcode !== '0x') {
|
|
1594
|
+
console.log(dim(` using the canonical fixed-price edition minter at its deterministic address ${predicted}`));
|
|
1595
|
+
return predicted;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
1599
|
+
const send = makeHotSender({ wallet, account, publicClient });
|
|
1600
|
+
console.log(dim(' deploying the canonical fixed-price edition minter (shared, ownerless) — CREATE2…'));
|
|
1601
|
+
const { minter, txHash } = await deployFixedPriceMinter1155(send, publicClient);
|
|
1602
|
+
console.log(` ${green('✓')} fixed-price edition minter ${minter}`);
|
|
1603
|
+
console.log(dim(` tx ${txHash}`));
|
|
1604
|
+
console.log(dim(` not in the shipped manifest for ${CHAIN} — to reuse it set ${bold(`ABX_FIXED_PRICE_MINTER_1155=${minter}`)} (or add it to packages/sdk/src/deployments.ts)`));
|
|
1605
|
+
return minter;
|
|
1606
|
+
}
|
|
1403
1607
|
/** A minter write (configure/buy). No re-index — the sale lives on the minter, not the token
|
|
1404
1608
|
* projection; the token's own state is unchanged by a sale config.
|
|
1405
1609
|
*
|
|
@@ -1425,23 +1629,76 @@ function parseSalePrice(flags, usage) {
|
|
|
1425
1629
|
* sale for a project on the shared minter. Defers to the token owner. Resolves (or deploys) the
|
|
1426
1630
|
* shared minter, then reports the two remaining grants the owner still needs (assign the minter on
|
|
1427
1631
|
* the token, set a primary payee) and whether the token is paused.
|
|
1632
|
+
*
|
|
1633
|
+
* On an EDITION (OneOfOneEdition/EditionImage/EditionCode), `--token-id` is REQUIRED (sales are
|
|
1634
|
+
* keyed `(token, id)` — a per-artwork price, not one project-wide sale) and the sale routes to the
|
|
1635
|
+
* shared `AbxFixedPriceMinter1155` instead. Refused on a 721 target.
|
|
1428
1636
|
*/
|
|
1429
1637
|
export async function cmdMinterConfigure(address, flags) {
|
|
1430
|
-
const usage = 'abx minter configure <token> (--price <eth> | --price-raw <units>) --allocation <n> [--erc20 0x…] [--minter-contract 0x…] [--sign|--unsigned]';
|
|
1638
|
+
const usage = 'abx minter configure <token> (--price <eth> | --price-raw <units>) --allocation <n> [--erc20 0x…] [--token-id <n> (editions)] [--minter-contract 0x…] [--sign|--unsigned]';
|
|
1431
1639
|
const token = requireAddress(address, usage);
|
|
1640
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1641
|
+
const kind = await detectTokenKind(publicClient, token);
|
|
1432
1642
|
const owner = await read(token, 'owner');
|
|
1433
1643
|
const erc20 = flags.erc20 && flags.erc20 !== 'true' ? flags.erc20 : zeroAddress;
|
|
1434
1644
|
const allocation = BigInt(requireFlag(flags, 'allocation', usage));
|
|
1435
1645
|
const price = parseSalePrice(flags, usage);
|
|
1436
|
-
|
|
1437
|
-
//
|
|
1646
|
+
const isEth = erc20 === zeroAddress;
|
|
1647
|
+
// On --dry-run resolve the minter WITHOUT deploying — ensure* would deploy the shared singleton
|
|
1648
|
+
// if absent, and a preview must never send. Fall back to a label if none exists.
|
|
1438
1649
|
const dryRun = isDryRun(flags);
|
|
1650
|
+
if (kind.isEdition) {
|
|
1651
|
+
const tokenId = parseEditionCountFlag(requireFlag(flags, 'token-id', usage), 'token-id');
|
|
1652
|
+
const knownMinter = fixedPriceMinter1155Address(flags['minter-contract']);
|
|
1653
|
+
if (dryRun && !knownMinter) {
|
|
1654
|
+
console.log(yellow(` ⚠ no shared edition minter deployed on ${CHAIN} yet — a real run deploys it once (a separate tx) before configuring.`));
|
|
1655
|
+
}
|
|
1656
|
+
const minter = dryRun ? (knownMinter ?? zeroAddress) : await ensureFixedPriceMinter1155(flags['minter-contract']);
|
|
1657
|
+
// Sanity-check the allocation against what THIS id can still mint. `maxSupply(id) === 0` reads
|
|
1658
|
+
// as "open" (the un-overridden --copies default, or an id nobody has ever capped) — the same
|
|
1659
|
+
// convention `tokens.ts`'s TokenRow.maxSupply documents; see set-max-supply's own note on why a
|
|
1660
|
+
// bare 0 can't be told apart from "explicitly closed" without more than this one read.
|
|
1661
|
+
const [maxSupply, supplyNow] = await Promise.all([
|
|
1662
|
+
readEdition(token, 'maxSupply', [tokenId]).catch(() => 0n),
|
|
1663
|
+
readEdition(token, 'totalSupply', [tokenId]).catch(() => 0n),
|
|
1664
|
+
]);
|
|
1665
|
+
const remaining = maxSupply > 0n ? (maxSupply > supplyNow ? maxSupply - supplyNow : 0n) : null;
|
|
1666
|
+
if (remaining !== null && allocation > remaining) {
|
|
1667
|
+
console.log(yellow(` ⚠ allocation ${allocation} exceeds the ${remaining} #${tokenId} can still mint `) +
|
|
1668
|
+
dim(`(cap ${maxSupply} − ${supplyNow} minted). Only ${remaining} will actually sell.`));
|
|
1669
|
+
console.log(dim(` To sell ${allocation}, raise this id's cap is not possible (caps only decrease) — set --allocation ${remaining} (or less, to hold reserves).`));
|
|
1670
|
+
}
|
|
1671
|
+
console.log(dim(` configuring sale on ${minter}`));
|
|
1672
|
+
console.log(dim(` token ${token} · id #${tokenId}`));
|
|
1673
|
+
console.log(dim(` price ${isEth ? `${formatEther(price)} ETH` : `${price} units of ${erc20}`} / copy`));
|
|
1674
|
+
console.log(dim(` allocation ${allocation}`));
|
|
1675
|
+
await runMinterWrite(prepareConfigureSale1155({ minter, token, tokenId, paymentToken: erc20, price, allocation, chainId: chainId() }), flags, owner);
|
|
1676
|
+
const [assignedMinter, payee, paused] = await Promise.all([
|
|
1677
|
+
read(token, 'minter').catch(() => zeroAddress),
|
|
1678
|
+
read(token, 'primaryPayee').catch(() => zeroAddress),
|
|
1679
|
+
read(token, 'paused').catch(() => false),
|
|
1680
|
+
]);
|
|
1681
|
+
const assigned = assignedMinter.toLowerCase() === minter.toLowerCase();
|
|
1682
|
+
console.log('');
|
|
1683
|
+
console.log(assigned ? dim(' ✓ minter is assigned on the token') : yellow(` ⚠ assign this minter on the token: abx set-minter ${token} --minter ${minter}`));
|
|
1684
|
+
if (payee === zeroAddress)
|
|
1685
|
+
console.log(yellow(` ⚠ set a primary payee (sales revert without one): abx set-primary-payee ${token} --payee 0x…`));
|
|
1686
|
+
else
|
|
1687
|
+
console.log(dim(` ✓ proceeds → ${payee}`));
|
|
1688
|
+
if (paused)
|
|
1689
|
+
console.log(yellow(` • token is paused — open the sale when ready: abx unpause ${token}`));
|
|
1690
|
+
console.log(dim(` buyers then run: abx minter buy ${token} --token-id ${tokenId} --quantity <n>`));
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
// 721 path (unchanged): --token-id has no meaning — a 721 sale is one project-wide price.
|
|
1694
|
+
if (flags['token-id'] !== undefined) {
|
|
1695
|
+
throw new Error(`--token-id is edition-only (per-(token,id) sales) — ${token} is a ${kind.label} (721), sold as a single project-wide sale. Drop --token-id.`);
|
|
1696
|
+
}
|
|
1439
1697
|
const knownMinter = fixedPriceMinterAddress(flags['minter-contract']);
|
|
1440
1698
|
if (dryRun && !knownMinter) {
|
|
1441
1699
|
console.log(yellow(` ⚠ no shared minter deployed on ${CHAIN} yet — a real run deploys it once (a separate tx) before configuring.`));
|
|
1442
1700
|
}
|
|
1443
1701
|
const minter = dryRun ? (knownMinter ?? zeroAddress) : await ensureFixedPriceMinter(flags['minter-contract']);
|
|
1444
|
-
const isEth = erc20 === zeroAddress;
|
|
1445
1702
|
// Sanity-check the allocation against what the contract can actually mint. A creator who sets
|
|
1446
1703
|
// --allocation 100 on a 16-supply Series would only ever sell the remainder (maxInvocations binds
|
|
1447
1704
|
// tighter than the minter's allocation) and discover it when mint #16 reverts. Warn loudly — but
|
|
@@ -1479,19 +1736,67 @@ export async function cmdMinterConfigure(address, flags) {
|
|
|
1479
1736
|
console.log(dim(` buyers then run: abx minter buy ${token}`));
|
|
1480
1737
|
}
|
|
1481
1738
|
/** `abx minter show <token>` — the sale terms + the token's readiness (assigned? payee? paused?).
|
|
1482
|
-
* Read-only; the agent runs it to see exactly what's configured before/after a sale.
|
|
1739
|
+
* Read-only; the agent runs it to see exactly what's configured before/after a sale. On an
|
|
1740
|
+
* edition, `--token-id` is required and the readout is per-id (copies, not a whole-contract cap). */
|
|
1483
1741
|
export async function cmdMinterShow(address, flags) {
|
|
1484
|
-
const
|
|
1742
|
+
const usage = 'abx minter show <token> [--token-id <n> (editions)] [--minter-contract 0x…]';
|
|
1743
|
+
const token = requireAddress(address, usage);
|
|
1485
1744
|
// Refuse a nonexistent contract like the sibling owner-ops (state/unpause/minter configure) do —
|
|
1486
1745
|
// otherwise the minter singleton returns zero-config for the unconfigured token and we print a
|
|
1487
1746
|
// plausible-but-fake `paused: no (open)` readout for an address that has no project at all.
|
|
1488
1747
|
await assertContractExists(token);
|
|
1748
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1749
|
+
const kind = await detectTokenKind(publicClient, token);
|
|
1750
|
+
if (kind.isEdition) {
|
|
1751
|
+
const tokenId = parseEditionCountFlag(requireFlag(flags, 'token-id', usage), 'token-id');
|
|
1752
|
+
const minter = fixedPriceMinter1155Address(flags['minter-contract']);
|
|
1753
|
+
if (!minter) {
|
|
1754
|
+
console.log(yellow(` no shared edition minter known for ${CHAIN} — configure a sale (deploys it) or set ABX_FIXED_PRICE_MINTER_1155`));
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const sale = await readSaleConfig1155(publicClient, minter, token, tokenId);
|
|
1758
|
+
// Every read here goes through the EDITION ABI: the generic `read()` is the 721 1/1 ABI,
|
|
1759
|
+
// which has no minter()/primaryPayee() at all — a swallowed client-side throw there printed
|
|
1760
|
+
// a false "not assigned / no payee" for every correctly-configured edition (caught live by
|
|
1761
|
+
// the Sepolia fixture sweep, 2026-08-05).
|
|
1762
|
+
const [assignedMinter, payee, paused, maxSupply, supply] = await Promise.all([
|
|
1763
|
+
readEdition(token, 'minter').catch(() => zeroAddress),
|
|
1764
|
+
readEdition(token, 'primaryPayee').catch(() => zeroAddress),
|
|
1765
|
+
readEdition(token, 'paused').catch(() => false),
|
|
1766
|
+
readEdition(token, 'maxSupply', [tokenId]).catch(() => 0n),
|
|
1767
|
+
readEdition(token, 'totalSupply', [tokenId]).catch(() => 0n),
|
|
1768
|
+
]);
|
|
1769
|
+
const isEth = sale.paymentToken === zeroAddress;
|
|
1770
|
+
const assigned = assignedMinter.toLowerCase() === minter.toLowerCase();
|
|
1771
|
+
console.log(bold(`\n minter sale — ${token} #${tokenId}`));
|
|
1772
|
+
console.log(` shared minter: ${minter}`);
|
|
1773
|
+
if (!sale.configured) {
|
|
1774
|
+
console.log(yellow(` not configured — abx minter configure ${token} --token-id ${tokenId} --price <eth> --allocation <n>`));
|
|
1775
|
+
}
|
|
1776
|
+
else {
|
|
1777
|
+
console.log(` price: ${isEth ? `${formatEther(sale.price)} ETH` : `${sale.price} units of ${sale.paymentToken}`}`);
|
|
1778
|
+
console.log(` allocation: ${sale.sold}/${sale.allocation} sold`);
|
|
1779
|
+
}
|
|
1780
|
+
console.log(` assigned on token: ${assigned ? green('yes') : yellow(`no — abx set-minter ${token} --minter ${minter}`)}`);
|
|
1781
|
+
console.log(` primary payee: ${payee === zeroAddress ? yellow('none — abx set-primary-payee …') : payee}`);
|
|
1782
|
+
console.log(` paused: ${paused ? yellow(`yes — abx unpause ${token}`) : green('no (open)')}`);
|
|
1783
|
+
console.log(` copies: ${supply}${maxSupply > 0n ? `/${maxSupply}` : dim(' (open — no cap)')}`);
|
|
1784
|
+
const remaining = maxSupply > 0n ? (maxSupply > supply ? maxSupply - supply : 0n) : null;
|
|
1785
|
+
if (sale.configured && remaining !== null && sale.allocation > remaining) {
|
|
1786
|
+
console.log(yellow(` ⚠ allocation ${sale.allocation} exceeds the ${remaining} still mintable for #${tokenId} (cap ${maxSupply} − ${supply} minted) — only ${remaining} can actually sell.`));
|
|
1787
|
+
}
|
|
1788
|
+
console.log('');
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (flags['token-id'] !== undefined) {
|
|
1792
|
+
throw new Error(`--token-id is edition-only — ${token} is a ${kind.label} (721). Drop --token-id.`);
|
|
1793
|
+
}
|
|
1489
1794
|
const minter = fixedPriceMinterAddress(flags['minter-contract']);
|
|
1490
1795
|
if (!minter) {
|
|
1491
1796
|
console.log(yellow(` no shared minter known for ${CHAIN} — configure a sale (deploys it) or set ABX_FIXED_PRICE_MINTER`));
|
|
1492
1797
|
return;
|
|
1493
1798
|
}
|
|
1494
|
-
const sale = await readSaleConfig(
|
|
1799
|
+
const sale = await readSaleConfig(publicClient, minter, token);
|
|
1495
1800
|
const [assignedMinter, payee, paused, max, supply] = await Promise.all([
|
|
1496
1801
|
readSeries(token, 'minter').catch(() => zeroAddress),
|
|
1497
1802
|
readSeries(token, 'primaryPayee').catch(() => zeroAddress),
|
|
@@ -1523,14 +1828,46 @@ export async function cmdMinterShow(address, flags) {
|
|
|
1523
1828
|
console.log('');
|
|
1524
1829
|
}
|
|
1525
1830
|
/** `abx minter buy <token> [--to 0x…]` — buy one token through the shared minter (public; any
|
|
1526
|
-
* funded signer). ETH sales attach the price; ERC-20 sales need a prior approval to the minter.
|
|
1831
|
+
* funded signer). ETH sales attach the price; ERC-20 sales need a prior approval to the minter.
|
|
1832
|
+
* On an edition, `--token-id` is required and `--quantity` (default 1) buys several copies in the
|
|
1833
|
+
* same purchase — the total ETH attached is `price × quantity`. */
|
|
1527
1834
|
export async function cmdMinterBuy(address, flags) {
|
|
1528
|
-
const usage = 'abx minter buy <token> [--to 0x…] [--minter-contract 0x…] [--sign|--unsigned]';
|
|
1835
|
+
const usage = 'abx minter buy <token> [--to 0x…] [--token-id <n> --quantity <n> (editions)] [--minter-contract 0x…] [--sign|--unsigned]';
|
|
1529
1836
|
const token = requireAddress(address, usage);
|
|
1837
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1838
|
+
const kind = await detectTokenKind(publicClient, token);
|
|
1839
|
+
if (kind.isEdition) {
|
|
1840
|
+
const tokenId = parseEditionCountFlag(requireFlag(flags, 'token-id', usage), 'token-id');
|
|
1841
|
+
const quantity = flags.quantity !== undefined ? parseEditionCountFlag(flags.quantity, 'quantity') : 1n;
|
|
1842
|
+
if (quantity === 0n)
|
|
1843
|
+
throw new Error('--quantity must be at least 1 (0 copies is not a purchase).');
|
|
1844
|
+
const minter = fixedPriceMinter1155Address(flags['minter-contract']);
|
|
1845
|
+
if (!minter)
|
|
1846
|
+
throw new Error(`no shared edition minter known for ${CHAIN} — configure a sale first, or set ABX_FIXED_PRICE_MINTER_1155`);
|
|
1847
|
+
const sale = await readSaleConfig1155(publicClient, minter, token, tokenId);
|
|
1848
|
+
if (!sale.configured)
|
|
1849
|
+
throw new Error(`no sale configured for ${token} #${tokenId} — run: abx minter configure ${token} --token-id ${tokenId} …`);
|
|
1850
|
+
const isEth = sale.paymentToken === zeroAddress;
|
|
1851
|
+
const to = flags.to && flags.to !== 'true' ? flags.to : undefined;
|
|
1852
|
+
// Payment math: price × quantity, computed ONCE and shown in the confirm/dry-run readout below
|
|
1853
|
+
// (via runMinterWrite → gatedSend's own preview) — the exact ETH the tx attaches.
|
|
1854
|
+
const value = isEth ? sale.price * quantity : 0n;
|
|
1855
|
+
if (!isEth) {
|
|
1856
|
+
console.log(yellow(` ERC-20 sale: the buyer must have approved ${minter} to spend ${sale.price * quantity} units of ${sale.paymentToken} first (else the tx reverts).`));
|
|
1857
|
+
}
|
|
1858
|
+
console.log(dim(` buying ${quantity} cop${quantity === 1n ? 'y' : 'ies'} of #${tokenId}${to ? ` → ${to}` : ' → signer'} for ` +
|
|
1859
|
+
`${isEth ? `${formatEther(value)} ETH (${formatEther(sale.price)} × ${quantity})` : `${value} units (${sale.price} × ${quantity})`}`));
|
|
1860
|
+
await runMinterWrite(preparePurchase1155({ minter, token, tokenId, quantity, to, value, chainId: chainId() }), flags);
|
|
1861
|
+
console.log(dim(` next: \`abx refresh ${token}\` so marketplaces pick up the change.`));
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (flags['token-id'] !== undefined || flags.quantity !== undefined) {
|
|
1865
|
+
throw new Error(`--token-id/--quantity are edition-only — ${token} is a ${kind.label} (721), where a sale buys exactly one token. Drop them.`);
|
|
1866
|
+
}
|
|
1530
1867
|
const minter = fixedPriceMinterAddress(flags['minter-contract']);
|
|
1531
1868
|
if (!minter)
|
|
1532
1869
|
throw new Error(`no shared minter known for ${CHAIN} — configure a sale first, or set ABX_FIXED_PRICE_MINTER`);
|
|
1533
|
-
const sale = await readSaleConfig(
|
|
1870
|
+
const sale = await readSaleConfig(publicClient, minter, token);
|
|
1534
1871
|
if (!sale.configured)
|
|
1535
1872
|
throw new Error(`no sale configured for ${token} — run: abx minter configure ${token} …`);
|
|
1536
1873
|
const isEth = sale.paymentToken === zeroAddress;
|