@artblocks/abx-cli 0.1.0-alpha.16 → 0.1.0-alpha.18
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 +385 -0
- package/dist/commands/deploy.d.ts +181 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +2974 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/project.d.ts +134 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +919 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/reads.d.ts +64 -0
- package/dist/commands/reads.d.ts.map +1 -0
- package/dist/commands/reads.js +635 -0
- package/dist/commands/reads.js.map +1 -0
- package/dist/commands/scaffold.d.ts +55 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +470 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/service.d.ts +41 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +572 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/storage.d.ts +37 -0
- package/dist/commands/storage.d.ts.map +1 -0
- package/dist/commands/storage.js +335 -0
- package/dist/commands/storage.js.map +1 -0
- package/dist/config.d.ts +72 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +201 -4
- package/dist/config.js.map +1 -1
- package/dist/deps.d.ts +6 -39
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +4 -68
- package/dist/deps.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/flags.d.ts +34 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +74 -0
- package/dist/flags.js.map +1 -1
- package/dist/jsonout.d.ts +37 -0
- package/dist/jsonout.d.ts.map +1 -0
- package/dist/jsonout.js +68 -0
- package/dist/jsonout.js.map +1 -0
- package/dist/main.js +133 -6223
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +34 -40
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +137 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +504 -0
- package/dist/output.js.map +1 -0
- package/dist/ownerops.d.ts +37 -44
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +274 -233
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +8 -4
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +8 -17
- package/dist/preview.js.map +1 -1
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +10 -7
- package/dist/provision.js.map +1 -1
- package/dist/remote.d.ts +72 -7
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +211 -9
- package/dist/remote.js.map +1 -1
- package/dist/riskgate.d.ts +29 -0
- package/dist/riskgate.d.ts.map +1 -0
- package/dist/riskgate.js +91 -0
- package/dist/riskgate.js.map +1 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -16
- package/dist/schema.js.map +1 -1
- package/dist/served.d.ts +30 -0
- package/dist/served.d.ts.map +1 -0
- package/dist/served.js +112 -0
- package/dist/served.js.map +1 -0
- package/dist/signer.d.ts +13 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +42 -19
- package/dist/signer.js.map +1 -1
- package/dist/update-check.d.ts +27 -0
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +30 -0
- package/dist/update-check.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +9 -13
- package/skill/reference/code-projects.md +7 -6
- package/skill/reference/hosting.md +6 -6
- package/skill/reference/operating.md +2 -5
- package/skill/reference/setup.md +4 -3
- package/skill/reference/troubleshooting.md +14 -1
- package/dist/gas.d.ts +0 -63
- package/dist/gas.d.ts.map +0 -1
- package/dist/gas.js +0 -76
- package/dist/gas.js.map +0 -1
- package/dist/inspect.d.ts +0 -48
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -290
- package/dist/inspect.js.map +0 -1
- package/dist/migrate.d.ts +0 -65
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -180
- package/dist/migrate.js.map +0 -1
- package/dist/onchain-uri.d.ts +0 -81
- package/dist/onchain-uri.d.ts.map +0 -1
- package/dist/onchain-uri.js +0 -251
- package/dist/onchain-uri.js.map +0 -1
- package/dist/upload.d.ts +0 -28
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -41
- package/dist/upload.js.map +0 -1
package/dist/ownerops.js
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
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,
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { hasParamEnumeration } from './onchain-uri.js';
|
|
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';
|
|
10
|
+
import { encodeScalarParam, encodeTag as encodeTagSdk, isAccepted, prepareConfigureTokenParam, prepareConfigureTokenParamData, prepareSetContractParam, prepareSetContractParamData, seriesCodeAbi, tryReadContract, } from '@artblocks/abx-sdk';
|
|
11
|
+
import { hasParamEnumeration } from '@artblocks/abx-sdk';
|
|
13
12
|
import { decodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
14
13
|
import { contentTypeFromPath } from '@artblocks/abx-storage';
|
|
15
14
|
import { toHex as toHexSdk } from 'viem';
|
|
@@ -17,12 +16,18 @@ import { readFileSync } from 'node:fs';
|
|
|
17
16
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
18
17
|
import { gzipSync } from 'node:zlib';
|
|
19
18
|
import { formatEther, getAddress, isAddress, parseEther, toHex, zeroAddress } from 'viem';
|
|
20
|
-
import { fixedPriceMinterAddress } from './config.js';
|
|
21
|
-
import {
|
|
19
|
+
import { CHAIN, chainId, fixedPriceMinterAddress, localIndexer } from './config.js';
|
|
20
|
+
import { CliError } from './errors.js';
|
|
21
|
+
import { openWalletSession } from './signer.js';
|
|
22
|
+
import { gatedSend, laneFromFlags } from './riskgate.js';
|
|
23
|
+
import { withJson } from './jsonout.js';
|
|
22
24
|
import { resolveRemote, serviceClient } from './remote.js';
|
|
23
|
-
import { positionalArgs, unknownFlags } from './flags.js';
|
|
25
|
+
import { isDryRun, positionalArgs, unknownFlags } from './flags.js';
|
|
24
26
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
25
|
-
|
|
27
|
+
// Re-exported for main.ts (the on-chain-vs-off-chain cost model + the compression-mode type now
|
|
28
|
+
// live in the SDK's staging.ts, layered on planChunks/planContentTxs — see the comment at their
|
|
29
|
+
// definition below).
|
|
30
|
+
export { ONCHAIN_PROJECT_SOFT_LIMIT };
|
|
26
31
|
// ── ANSI (local) ─────────────────────────────────────────────────────────────
|
|
27
32
|
const C = { reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[38;5;115m', yellow: '\x1b[38;5;221m' };
|
|
28
33
|
const dim = (s) => `${C.dim}${s}${C.reset}`;
|
|
@@ -57,7 +62,7 @@ function fitsLiteralBytes32(value) {
|
|
|
57
62
|
return value.length <= 31 && /^[\x20-\x7e]+$/.test(value);
|
|
58
63
|
}
|
|
59
64
|
/** Flags `abx attach` recognizes — anything else warns (non-fatal), so a silent no-op flag surfaces. */
|
|
60
|
-
const ATTACH_FLAGS = ['file', 'compress', 'collection', 'token', 'send', 'sign', 'unsigned', 'yes', 'dry-run', 'port', 'sign-url-file', 'remote'];
|
|
65
|
+
const ATTACH_FLAGS = ['file', 'compress', 'collection', 'token', 'send', 'sign', 'unsigned', 'yes', 'dry-run', 'confirm', 'port', 'sign-url-file', 'remote'];
|
|
61
66
|
/** Auto-detect the on-chain representation for an off-chain locator by its URI scheme. Returns null
|
|
62
67
|
* for anything that isn't a recognized durable/http locator (so `attach` can refuse it loudly
|
|
63
68
|
* rather than silently storing a bad value — the round-1 `--representation` guessing trap). */
|
|
@@ -71,21 +76,10 @@ export function representationForLocator(uri) {
|
|
|
71
76
|
return R.url;
|
|
72
77
|
return null;
|
|
73
78
|
}
|
|
74
|
-
function chainId() {
|
|
75
|
-
return resolveChain(CHAIN).id;
|
|
76
|
-
}
|
|
77
|
-
/** Resolve the signing lane from flags: --unsigned (cold) · --sign (wallet) · default hot. */
|
|
78
|
-
export function laneFromFlags(flags) {
|
|
79
|
-
if (flags.unsigned)
|
|
80
|
-
return 'unsigned';
|
|
81
|
-
if (flags.sign)
|
|
82
|
-
return 'sign';
|
|
83
|
-
return 'send';
|
|
84
|
-
}
|
|
85
79
|
function requireAddress(address, usage) {
|
|
86
80
|
if (!address || address.startsWith('--')) {
|
|
87
81
|
console.error(`usage: ${usage}\n`);
|
|
88
|
-
|
|
82
|
+
throw new CliError('', 1, true); // already printed above — see CliError's alreadyPrinted doc
|
|
89
83
|
}
|
|
90
84
|
return address;
|
|
91
85
|
}
|
|
@@ -93,44 +87,32 @@ function requireFlag(flags, name, usage) {
|
|
|
93
87
|
const v = flags[name];
|
|
94
88
|
if (!v || v === 'true') {
|
|
95
89
|
console.error(`missing --${name}\nusage: ${usage}\n`);
|
|
96
|
-
|
|
90
|
+
throw new CliError('', 1, true); // already printed above — see CliError's alreadyPrinted doc
|
|
97
91
|
}
|
|
98
92
|
return v;
|
|
99
93
|
}
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
94
|
+
/**
|
|
95
|
+
* Every owner-op's send choke point. Returns the {@link SignResult} when a tx was broadcast, else
|
|
96
|
+
* `null` (dry run, or the cold lane which prints a tx instead of sending one).
|
|
97
|
+
*
|
|
98
|
+
* It used to return a bare boolean. It returns the result now because a caller sometimes needs the
|
|
99
|
+
* *receipt* — `abx mint --json` has to report which token id was actually minted, and the only
|
|
100
|
+
* authoritative answer is the Transfer log the mint emitted. Callers that only asked "did it send?"
|
|
101
|
+
* keep working: `null` is falsy, and a result object is truthy.
|
|
102
|
+
*
|
|
103
|
+
* The dry-run preview / `--confirm` prompt / lane selection live in {@link gatedSend} — the SAME
|
|
104
|
+
* choke point the deploy family's resumed-setup send routes through — so this is now just "gate,
|
|
105
|
+
* then re-index if the project is known."
|
|
106
|
+
*/
|
|
102
107
|
async function runWrite(address, provider, flags, expectedSigner) {
|
|
103
|
-
|
|
104
|
-
// owner-op, so --dry-run is uniform across them — it must never fall through to a real send just
|
|
105
|
-
// because the flag isn't wired per-command (a round-1 finding: `set-field --dry-run` silently
|
|
106
|
-
// reached the send lane and only stopped for lack of a key).
|
|
107
|
-
if (flags['dry-run']) {
|
|
108
|
-
const prepared = typeof provider === 'function' ? await provider(expectedSigner ?? zeroAddress) : provider;
|
|
109
|
-
console.log(`\n ${C.bold}◆ ${prepared.summary}${C.reset} ${dim('(dry run — nothing sent)')}`);
|
|
110
|
-
for (const [k, v] of Object.entries(prepared.fields))
|
|
111
|
-
console.log(` ${dim(k.padEnd(12))} ${v}`);
|
|
112
|
-
console.log(` ${dim('to'.padEnd(12))} ${prepared.to ?? dim('(contract deploy)')}`);
|
|
113
|
-
if (expectedSigner)
|
|
114
|
-
console.log(` ${dim('owner'.padEnd(12))} ${expectedSigner}`);
|
|
115
|
-
console.log(dim(`\n Re-run without --dry-run to send (lane: ${laneFromFlags(flags)}).\n`));
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
await assertChainId(CHAIN); // verify the RPC really is CHAIN before any irreversible write
|
|
119
|
-
const result = await signTx(provider, {
|
|
120
|
-
lane: laneFromFlags(flags),
|
|
121
|
-
chainKey: CHAIN,
|
|
122
|
-
expectedSigner,
|
|
123
|
-
yes: !!flags.yes,
|
|
124
|
-
port: flags.port ? Number(flags.port) : undefined,
|
|
125
|
-
signUrlFile: flags['sign-url-file'],
|
|
126
|
-
});
|
|
108
|
+
const result = await gatedSend(provider, flags, { chainKey: CHAIN, expectedSigner });
|
|
127
109
|
if (!result)
|
|
128
|
-
return
|
|
110
|
+
return null; // dry run, or the cold lane — nothing broadcast
|
|
129
111
|
await reindexIfKnown(address, flags);
|
|
130
|
-
return
|
|
112
|
+
return result;
|
|
131
113
|
}
|
|
132
114
|
async function reindexIfKnown(address, flags) {
|
|
133
|
-
const indexer =
|
|
115
|
+
const indexer = localIndexer();
|
|
134
116
|
if (indexer.store.getRegistration(address)) {
|
|
135
117
|
const { state, elapsedMs } = await indexer.reindex(address);
|
|
136
118
|
console.log(` ${green('✓')} re-indexed ${state.name ?? address}: ${state.eventCount} events in ${elapsedMs}ms`);
|
|
@@ -171,22 +153,22 @@ async function reindexIfKnown(address, flags) {
|
|
|
171
153
|
}
|
|
172
154
|
async function read(address, functionName, args = []) {
|
|
173
155
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
156
|
+
const result = await tryReadContract(publicClient, { address, abi: oneOfOneImageAbi, functionName, args });
|
|
157
|
+
if (result !== undefined)
|
|
158
|
+
return result;
|
|
159
|
+
// A read that "returned no data" is usually not a real revert but an address with NO CONTRACT —
|
|
160
|
+
// previewing before deploy, a typo'd address, or the wrong chain. Every owner-op reads the
|
|
161
|
+
// owner/ownerOf here FIRST, so this one guard turns the opaque viem error into an actionable one
|
|
162
|
+
// across all of them. Only runs on the error path (zero cost when the contract exists).
|
|
163
|
+
await assertContractExists(address);
|
|
164
|
+
// The contract DOES exist → a genuine revert. tryReadContract already swallowed that error, so
|
|
165
|
+
// re-read once (uncaught) to surface it rather than a generic "read failed".
|
|
166
|
+
return (await publicClient.readContract({
|
|
167
|
+
address,
|
|
168
|
+
abi: oneOfOneImageAbi,
|
|
169
|
+
functionName: functionName,
|
|
170
|
+
args: args,
|
|
171
|
+
}));
|
|
190
172
|
}
|
|
191
173
|
/** Throw a clear, actionable error when `address` has no contract on the active chain — the common
|
|
192
174
|
* cause of an owner-op's raw `returned no data ("0x")`. No-op when code is present or unknowable. */
|
|
@@ -257,7 +239,8 @@ export async function cmdConfigureParam(address, rest, flags) {
|
|
|
257
239
|
const valueInput = valueParts.join(' ');
|
|
258
240
|
if (!tokenIdRaw || !key || (!valueInput && !flags.file)) {
|
|
259
241
|
console.error(`usage: ${usage}\n`);
|
|
260
|
-
process.
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
261
244
|
}
|
|
262
245
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
263
246
|
const schema = (await publicClient.readContract({
|
|
@@ -440,7 +423,8 @@ export async function cmdSetDependency(address, rest, flags) {
|
|
|
440
423
|
const [indexRaw, refRaw] = positionalArgs(rest);
|
|
441
424
|
if (indexRaw === undefined || !refRaw) {
|
|
442
425
|
console.error(`usage: ${usage}\n`);
|
|
443
|
-
process.
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
return;
|
|
444
428
|
}
|
|
445
429
|
if (!/^\d+$/.test(indexRaw))
|
|
446
430
|
throw new Error(`<index> must be a non-negative integer (got '${indexRaw}') — the list is dense: index ≤ dependencyCount`);
|
|
@@ -464,7 +448,8 @@ export async function cmdSetDependencyRegistry(address, rest, flags) {
|
|
|
464
448
|
const [raw] = positionalArgs(rest);
|
|
465
449
|
if (!raw) {
|
|
466
450
|
console.error(`usage: ${usage}\n`);
|
|
467
|
-
process.
|
|
451
|
+
process.exitCode = 1;
|
|
452
|
+
return;
|
|
468
453
|
}
|
|
469
454
|
if (raw !== 'none' && !/^0x[0-9a-fA-F]{40}$/.test(raw)) {
|
|
470
455
|
throw new Error(`<registry> must be an address (0x + 40 hex) or 'none' to clear; got '${raw}'`);
|
|
@@ -487,24 +472,47 @@ export async function cmdLockDependencies(address, flags) {
|
|
|
487
472
|
// it mints the next sequential token id (metadata = token id): bare (one), or `--count <n>`
|
|
488
473
|
// (n in order). Owner or an authorized minter signs.
|
|
489
474
|
export async function cmdMint(address, flags) {
|
|
490
|
-
const usage = 'abx mint <address> [--to 0x…] [--count <n>] [--sign|--unsigned]';
|
|
475
|
+
const usage = 'abx mint <address> [--to 0x…] [--count <n>] [--json] [--sign|--unsigned]';
|
|
491
476
|
const contract = requireAddress(address, usage);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
477
|
+
// `--json`: the TOKEN ID is the value a program came for, and it was previously only obtainable by
|
|
478
|
+
// regex-scraping coloured prose (B19). It is read from the mint's own Transfer logs rather than by
|
|
479
|
+
// re-reading `nextTokenId` afterwards — a concurrent mint would make that answer wrong, and a
|
|
480
|
+
// number that is usually right is worse than no number.
|
|
481
|
+
return withJson(flags, async () => {
|
|
482
|
+
const owner = await read(contract, 'owner');
|
|
483
|
+
const to = flags.to ?? owner; // default: pre-mint to the admin
|
|
484
|
+
let tx;
|
|
485
|
+
let count = 1n;
|
|
486
|
+
if (flags.count !== undefined && flags.count !== 'true') {
|
|
487
|
+
count = BigInt(flags.count);
|
|
488
|
+
console.log(dim(` minting ${count} Series tokens in order → ${to}`));
|
|
489
|
+
tx = prepareSeriesMintMany({ contract, to, count, chainId: chainId() });
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// Bare mint: the `mint(address)` selector is shared by the 1/1 (token #0) and a
|
|
493
|
+
// Series (next sequential token) — one path serves both.
|
|
494
|
+
console.log(dim(` minting token → ${to}${flags.to ? '' : ' (owner — pass --to for a buyer)'}`));
|
|
495
|
+
tx = prepareMint({ contract, to, chainId: chainId() });
|
|
496
|
+
}
|
|
497
|
+
const result = await runWrite(contract, tx, flags, owner);
|
|
498
|
+
const tokenIds = result ? await mintedTokenIds(makePublicClient({ chainKey: CHAIN }), contract, result.txHash) : [];
|
|
499
|
+
if (result && tokenIds.length) {
|
|
500
|
+
console.log(` ${green('✓')} minted token${tokenIds.length > 1 ? 's' : ''} ${bold(tokenIds.map((t) => '#' + t).join(', '))} → ${to}`);
|
|
501
|
+
}
|
|
502
|
+
console.log(dim(` next: \`abx refresh ${contract}\` so marketplaces pick up the new token.`));
|
|
503
|
+
return {
|
|
504
|
+
contract,
|
|
505
|
+
chainId: chainId(),
|
|
506
|
+
to,
|
|
507
|
+
// `sent: false` is the dry-run and cold-lane answer — an empty tokenIds with no explanation
|
|
508
|
+
// would read as a failed mint.
|
|
509
|
+
sent: !!result,
|
|
510
|
+
txHash: result?.txHash ?? null,
|
|
511
|
+
blockNumber: result ? String(result.blockNumber) : null,
|
|
512
|
+
tokenIds,
|
|
513
|
+
requestedCount: Number(count),
|
|
514
|
+
};
|
|
515
|
+
});
|
|
508
516
|
}
|
|
509
517
|
// ── series owner ops (minter set · supply cap · primary payee) ────────────────
|
|
510
518
|
// The multi-token knobs. All owner-only; each runs through the same signing lane +
|
|
@@ -747,7 +755,8 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
747
755
|
const [raw] = positionalArgs(rest);
|
|
748
756
|
if (!raw) {
|
|
749
757
|
console.error(`usage: ${usage}\n`);
|
|
750
|
-
process.
|
|
758
|
+
process.exitCode = 1;
|
|
759
|
+
return;
|
|
751
760
|
}
|
|
752
761
|
const cid = chainId();
|
|
753
762
|
const validator = parseTransferValidatorValue(raw, { chainId: cid, chainLabel: CHAIN, allowNone: true });
|
|
@@ -789,16 +798,41 @@ export async function cmdSetTransferValidator(address, rest, flags) {
|
|
|
789
798
|
const owner = await read(contract, 'owner');
|
|
790
799
|
await runWrite(contract, prepareSetTransferValidator({ contract, validator, chainId: cid }), flags, owner);
|
|
791
800
|
}
|
|
801
|
+
// ── multi-chunk on-chain content (the `reader` path) ─────────────────────────
|
|
802
|
+
// The plan math (chunk count + tx shape), the cost-model constants, and the actual
|
|
803
|
+
// plan→resolve-store→stage→encode orchestration all live in the SDK's staging.ts, layered on
|
|
804
|
+
// planChunks/planContentTxs/stageContent/encodeReader/ensureChunkStore — see its module doc. What
|
|
805
|
+
// stays here is Node-only (reading the file, gzip) plus the narration: every console.log below is
|
|
806
|
+
// this file's onEvent handler for the SDK's `StagingEvent`, so the lines are unchanged from before
|
|
807
|
+
// the move.
|
|
792
808
|
export function parseCompress(v) {
|
|
793
809
|
const c = (v ?? 'none').toLowerCase();
|
|
794
810
|
if (c === 'none' || c === 'fastlz' || c === 'gzip')
|
|
795
811
|
return c;
|
|
796
812
|
throw new Error(`--compress must be none | fastlz | gzip (got '${v}')`);
|
|
797
813
|
}
|
|
814
|
+
/** The chunk-store bootstrap narration shared by every content-staging path below — one place so
|
|
815
|
+
* the lines are identical whether the store is resolved for a single field ({@link putContentOnChain}
|
|
816
|
+
* → the SDK's `stageFieldContent`) or once up front for a whole batch ({@link ensureChunkStore}). */
|
|
817
|
+
function printChunkStoreEvent(e) {
|
|
818
|
+
if (e.kind === 'stale')
|
|
819
|
+
console.log(yellow(` configured chunk store ${e.address} is a stale deployment (no writeContent) — deploying a current one`));
|
|
820
|
+
else if (e.kind === 'canonical')
|
|
821
|
+
console.log(dim(` using the canonical chunk store at its deterministic address ${e.address}`));
|
|
822
|
+
else if (e.kind === 'deploying')
|
|
823
|
+
console.log(dim(' deploying the canonical multi-chunk content store (AbxChunkStore) — CREATE2…'));
|
|
824
|
+
else {
|
|
825
|
+
console.log(` ${green('✓')} chunk store ${e.address}`);
|
|
826
|
+
console.log(dim(` not in the shipped manifest for ${CHAIN} — to reuse it set ABX_CHUNK_STORE=${e.address} (or add it to packages/sdk/src/deployments.ts)`));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
798
829
|
/**
|
|
799
830
|
* The chain's shared multi-chunk store/reader; deploy it once if missing, signing through
|
|
800
831
|
* `send` (the env key on the hot lane, or the connected wallet on the wallet lane) — the
|
|
801
|
-
* store is ownerless, so any funded signer can stand it up.
|
|
832
|
+
* store is ownerless, so any funded signer can stand it up. Used directly only by
|
|
833
|
+
* {@link stageImageFieldsBatch} (resolving ONE store up front for every token); a single-field
|
|
834
|
+
* stage ({@link putContentOnChain}) resolves its store through the SDK's `stageFieldContent`
|
|
835
|
+
* instead, which calls the very same SDK `ensureChunkStore` internally.
|
|
802
836
|
*/
|
|
803
837
|
async function ensureChunkStore(send, override) {
|
|
804
838
|
// The resolution logic lives in the SDK (`ensureChunkStore`) so an SDK integrator bootstraps
|
|
@@ -806,99 +840,46 @@ async function ensureChunkStore(send, override) {
|
|
|
806
840
|
// incapable store fails deep inside a mint, after transactions have landed. The CLI keeps only the
|
|
807
841
|
// narration: the SDK reports progress through `onEvent` rather than printing.
|
|
808
842
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
809
|
-
return sdkEnsureChunkStore(publicClient, send, {
|
|
810
|
-
chainId: chainId(),
|
|
811
|
-
override,
|
|
812
|
-
onEvent: (e) => {
|
|
813
|
-
if (e.kind === 'stale')
|
|
814
|
-
console.log(yellow(` configured chunk store ${e.address} is a stale deployment (no writeContent) — deploying a current one`));
|
|
815
|
-
else if (e.kind === 'canonical')
|
|
816
|
-
console.log(dim(` using the canonical chunk store at its deterministic address ${e.address}`));
|
|
817
|
-
else if (e.kind === 'deploying')
|
|
818
|
-
console.log(dim(' deploying the canonical multi-chunk content store (AbxChunkStore) — CREATE2…'));
|
|
819
|
-
else {
|
|
820
|
-
console.log(` ${green('✓')} chunk store ${e.address}`);
|
|
821
|
-
console.log(dim(` not in the shipped manifest for ${CHAIN} — to reuse it set ABX_CHUNK_STORE=${e.address} (or add it to packages/sdk/src/deployments.ts)`));
|
|
822
|
-
}
|
|
823
|
-
},
|
|
824
|
-
});
|
|
843
|
+
return sdkEnsureChunkStore(publicClient, send, { chainId: chainId(), override, onEvent: printChunkStoreEvent });
|
|
825
844
|
}
|
|
826
845
|
/**
|
|
827
|
-
* The hot-lane staging signer: the env key signs + broadcasts each chunk-store write
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
846
|
+
* The hot-lane staging signer: the env key signs + broadcasts each chunk-store write, via the
|
|
847
|
+
* SDK's `makeHotSender` (pinned nonce, pinned gas, typed revert). Built lazily so commands that
|
|
848
|
+
* never stage on-chain don't require a key. The wallet lane passes a session-backed sender
|
|
849
|
+
* instead (see {@link openWalletSession}); the cold lane can't stage interactively, so it's
|
|
850
|
+
* rejected up front by the deploy/set-field commands.
|
|
831
851
|
*/
|
|
832
852
|
export function envStagingSender() {
|
|
833
853
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
834
854
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
835
|
-
return
|
|
836
|
-
const hash = await wallet.sendTransaction({
|
|
837
|
-
account,
|
|
838
|
-
chain: wallet.chain,
|
|
839
|
-
to: tx.to ?? undefined,
|
|
840
|
-
data: tx.data,
|
|
841
|
-
value: 0n,
|
|
842
|
-
});
|
|
843
|
-
return publicClient.waitForTransactionReceipt({ hash });
|
|
844
|
-
};
|
|
855
|
+
return makeHotSender({ wallet, account, publicClient });
|
|
845
856
|
}
|
|
846
857
|
/** Wallet-lane staging signer: each chunk-store write is approved in the human's browser
|
|
847
|
-
* wallet through the open {@link WalletSession}, sharing one connection with the final tx.
|
|
858
|
+
* wallet through the open {@link WalletSession}, sharing one connection with the final tx. A
|
|
859
|
+
* {@link PreparedTx} already carries everything `session.send` needs, so this is a thin adapter. */
|
|
848
860
|
export function sessionStagingSender(session) {
|
|
849
|
-
|
|
850
|
-
return async (tx) => (await session.send({
|
|
851
|
-
op: 'stage',
|
|
852
|
-
to: tx.to,
|
|
853
|
-
data: tx.data,
|
|
854
|
-
value: '0x0',
|
|
855
|
-
chainId: id,
|
|
856
|
-
summary: tx.summary,
|
|
857
|
-
fields: tx.fields,
|
|
858
|
-
})).receipt;
|
|
861
|
+
return async (tx) => (await session.send(tx)).receipt;
|
|
859
862
|
}
|
|
860
|
-
/**
|
|
861
|
-
* Put `content` on-chain as SSTORE2 chunks behind the shared reader, and return the field's
|
|
862
|
-
* `(representation, value)`. `fastlz` compresses per chunk (reader decodes on read → stays
|
|
863
|
-
* on-chain renderable); `gzip` compresses the whole content (off-chain decode → the
|
|
864
|
-
* `reader-gzip` representation, not on-chain renderable).
|
|
865
|
-
*
|
|
866
|
-
* Staging is one atomic `writeContent` when the content fits a single tx's gas, else
|
|
867
|
-
* gas-bounded chunk-write `multicall`s plus a final manifest write — {@link stageContent}
|
|
868
|
-
* picks, and we print the tx plan up front so the human knows the count before signing.
|
|
869
|
-
* These store writes use the env key (the data contracts are ownerless); the owner only
|
|
870
|
-
* signs the field set that references the manifest.
|
|
871
|
-
*/
|
|
872
|
-
/**
|
|
873
|
-
* Pure staging plan for a piece of content — chunk count + transaction shape — with NO
|
|
874
|
-
* chain writes, no signer, no `ensureChunkStore`. Shared by {@link putContentOnChain}
|
|
875
|
-
* (which then actually stages) and the deploy dry-run preview, so the count the human is
|
|
876
|
-
* told up front is the same one the real staging will produce. `chunkSize` defaults to the
|
|
877
|
-
* SDK's {@link planChunks} default (currently 22 KB), so e.g. a 2.8 KB SVG is 1 chunk.
|
|
878
|
-
*/
|
|
879
|
-
// On-chain image storage costs ~200 gas/byte (SSTORE2 code deposit) + per-chunk tx overhead, so it
|
|
880
|
-
// is cost-effective ONLY for tiny art. Past these soft limits, off-chain (Arweave pay-once, or a
|
|
881
|
-
// hosted resolver + IPFS) is FAR cheaper — on-chain's remaining edge is self-resolution, not cost.
|
|
882
|
-
// Per-file limit ≈ one 22 KB storage chunk; project limit caps the whole collection's gas.
|
|
883
|
-
export const ONCHAIN_IMAGE_SOFT_LIMIT = 24 * 1024; // per file
|
|
884
|
-
export const ONCHAIN_PROJECT_SOFT_LIMIT = 256 * 1024; // whole collection
|
|
885
863
|
const kb = (n) => `${n < 10 * 1024 ? (n / 1024).toFixed(1) : Math.round(n / 1024)}KB`;
|
|
886
864
|
/** Warn (loudly) when content is too big for on-chain to be the cost-sane choice. Returns true if it warned. */
|
|
887
865
|
export function warnIfLargeForOnChain(bytesLen, label) {
|
|
888
|
-
if (bytesLen
|
|
866
|
+
if (!exceedsOnchainSoftLimit(bytesLen))
|
|
889
867
|
return false;
|
|
890
868
|
console.log(yellow(` ⚠ ${label} is ${kb(bytesLen)} — large for ON-CHAIN storage (~200 gas/byte). Past ~${kb(ONCHAIN_IMAGE_SOFT_LIMIT)}/file, on-chain is FAR more expensive than off-chain, not cheaper.`));
|
|
891
869
|
console.log(dim(` For art this size prefer off-chain: --backend arweave (pay-once, permanent) or a hosted resolver + IPFS. Use on-chain only if self-resolution is a hard requirement.`));
|
|
892
870
|
return true;
|
|
893
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Pure staging plan for a piece of content — chunk count + transaction shape — with NO chain
|
|
874
|
+
* writes, no signer, no `ensureChunkStore`. The CLI-facing wrapper over the SDK's pure
|
|
875
|
+
* {@link planStagedContent}: `gzip` is a Node-only transform, so it happens here (not in the SDK —
|
|
876
|
+
* see staging.ts's module doc) before handing the result off to the SDK's chunk/tx-shape math.
|
|
877
|
+
* Shared by {@link putContentOnChain} (which then actually stages) and the deploy dry-run preview,
|
|
878
|
+
* so the count the human is told up front is the same one the real staging will produce.
|
|
879
|
+
*/
|
|
894
880
|
export function computeContentPlan(bytes, compress) {
|
|
895
881
|
const content = compress === 'gzip' ? new Uint8Array(gzipSync(bytes)) : new Uint8Array(bytes);
|
|
896
|
-
|
|
897
|
-
const planned = planChunks(content, { fastlz });
|
|
898
|
-
const plan = planContentTxs(planned);
|
|
899
|
-
const representation = compress === 'gzip' ? R.readerGzip : R.reader;
|
|
900
|
-
const stagedBytes = planned.reduce((n, c) => n + c.data.length, 0);
|
|
901
|
-
return { content, fastlz, chunks: planned.length, stagedBytes, plan, representation };
|
|
882
|
+
return planStagedContent(content, compress);
|
|
902
883
|
}
|
|
903
884
|
/** Human-readable size line for a content plan, e.g. `2812B → 1 chunk [fastlz 2812→904B]`. */
|
|
904
885
|
function planSizeLine(bytes, p, compress) {
|
|
@@ -920,15 +901,43 @@ function planTxLine(p, finalLabel) {
|
|
|
920
901
|
? `plan: 1 staging tx (atomic writeContent) + ${finalLabel} = 2 transactions`
|
|
921
902
|
: `plan: ${p.plan.batches.length} chunk-batch tx(s) + 1 manifest tx + ${finalLabel} = ${p.plan.txCount + 1} transactions`;
|
|
922
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* Put `bytes` on-chain as SSTORE2 chunks behind the shared reader, and return the field's
|
|
906
|
+
* `(representation, value)`. `fastlz` compresses per chunk (reader decodes on read → stays
|
|
907
|
+
* on-chain renderable); `gzip` compresses the whole content (off-chain decode → the
|
|
908
|
+
* `reader-gzip` representation, not on-chain renderable) — done here, not in the SDK's
|
|
909
|
+
* `stageFieldContent` (a Node-only transform; see staging.ts's module doc).
|
|
910
|
+
*
|
|
911
|
+
* Staging is one atomic `writeContent` when the content fits a single tx's gas, else
|
|
912
|
+
* gas-bounded chunk-write `multicall`s plus a final manifest write — the SDK picks, and we
|
|
913
|
+
* print the tx plan up front (via its `onEvent`) so the human knows the count before signing.
|
|
914
|
+
* These store writes use the env key (the data contracts are ownerless); the owner only
|
|
915
|
+
* signs the field set that references the manifest.
|
|
916
|
+
*/
|
|
923
917
|
async function putContentOnChain(bytes, compress, field, send, finalLabel = '1 owner field-set', store) {
|
|
924
|
-
const p = computeContentPlan(bytes, compress);
|
|
925
918
|
warnIfLargeForOnChain(bytes.length, `'${field}'`);
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
919
|
+
const content = compress === 'gzip' ? new Uint8Array(gzipSync(bytes)) : new Uint8Array(bytes);
|
|
920
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
921
|
+
return stageFieldContent({
|
|
922
|
+
content,
|
|
923
|
+
compress,
|
|
924
|
+
field,
|
|
925
|
+
send,
|
|
926
|
+
publicClient,
|
|
927
|
+
chainId: chainId(),
|
|
928
|
+
store,
|
|
929
|
+
onEvent: (e) => {
|
|
930
|
+
if (e.kind === 'chunk-store')
|
|
931
|
+
printChunkStoreEvent(e.event);
|
|
932
|
+
else if (e.kind === 'planned') {
|
|
933
|
+
console.log(dim(` '${field}' on-chain: ${planSizeLine(bytes.length, e.contentPlan, compress)} via reader ${e.store}`));
|
|
934
|
+
console.log(dim(` ${planTxLine(e.contentPlan, finalLabel)}`));
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
console.log(dim(` staged in ${e.txHashes.length} tx(s); manifest → ${e.manifest}`));
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
});
|
|
932
941
|
}
|
|
933
942
|
/**
|
|
934
943
|
* Dry-run preview of staging an image fully on-chain — the REAL chunk + transaction plan,
|
|
@@ -1043,7 +1052,7 @@ export async function cmdSetField(address, flags) {
|
|
|
1043
1052
|
const staging = !!(flags.file && flags.file !== 'true'); // large content ON-CHAIN via chunk store/reader
|
|
1044
1053
|
// --file + --dry-run: preview the on-chain staging plan and store/send NOTHING. (The locator/text
|
|
1045
1054
|
// path flows through runWrite, which previews there; staging must short-circuit before any upload.)
|
|
1046
|
-
if (staging && flags
|
|
1055
|
+
if (staging && isDryRun(flags)) {
|
|
1047
1056
|
const bytes = readFileSync(resolvePath(flags.file));
|
|
1048
1057
|
const p = computeContentPlan(bytes, parseCompress(flags.compress));
|
|
1049
1058
|
console.log(`\n ${bold('◆ set-field ' + field)} ${dim('(dry run — nothing staged or sent)')}`);
|
|
@@ -1119,9 +1128,25 @@ export async function cmdSetField(address, flags) {
|
|
|
1119
1128
|
* - computed keys (`artifacts`, `abx_provenance`) are refused — they're the manifest, not inputs.
|
|
1120
1129
|
* Scope: token (`--token`, default 0) or `--collection`. Any signing lane. Owner-only.
|
|
1121
1130
|
*/
|
|
1131
|
+
/**
|
|
1132
|
+
* Attach one or more artifacts. Several `<key> <uri>` pairs in one invocation become **one
|
|
1133
|
+
* transaction**.
|
|
1134
|
+
*
|
|
1135
|
+
* That batching is a safety fix, not a convenience (backlog B20). The CLI's most-documented flow —
|
|
1136
|
+
* mint, then attach each artifact, then refresh — sent one transaction per step with no all-or-nothing
|
|
1137
|
+
* boundary, so a failure partway through left a permanently half-written token that cannot be
|
|
1138
|
+
* un-minted. An integrator hit exactly this and folded 8 operations into 1 transaction using the SDK's
|
|
1139
|
+
* `batchOps`, which the CLI already shipped and did not use. Now it does: N pairs are one
|
|
1140
|
+
* `multicall`, so either every artifact lands or none does.
|
|
1141
|
+
*
|
|
1142
|
+
* A single pair passes through `batchOps` untouched, so the one-artifact case sends the identical
|
|
1143
|
+
* transaction it always did.
|
|
1144
|
+
*/
|
|
1122
1145
|
export async function cmdAttach(rest, flags) {
|
|
1123
|
-
const usage = 'abx attach <address> <key> <ipfs://… | ar://… | https://…> [--file <path>] [--collection | --token 0] [--sign|--unsigned] [--dry-run]';
|
|
1124
|
-
const [address,
|
|
1146
|
+
const usage = 'abx attach <address> <key> <ipfs://… | ar://… | https://…> [<key> <uri> …] [--file <path>] [--collection | --token 0] [--sign|--unsigned] [--dry-run]';
|
|
1147
|
+
const [address, ...pairArgs] = positionalArgs(rest);
|
|
1148
|
+
const key = pairArgs[0];
|
|
1149
|
+
const uri = pairArgs[1];
|
|
1125
1150
|
// Warn (never throw) on an unrecognized flag — a typo or a hopeful `--mime-type` otherwise no-ops
|
|
1126
1151
|
// INVISIBLY (round-1: an agent passed `--mime-type` and it was silently swallowed). mimeType is
|
|
1127
1152
|
// declared from the URL extension, not a flag; say so.
|
|
@@ -1136,13 +1161,15 @@ export async function cmdAttach(rest, flags) {
|
|
|
1136
1161
|
const contract = requireAddress(address, usage);
|
|
1137
1162
|
if (!key) {
|
|
1138
1163
|
console.error(`usage: ${usage}\n the <key> is the name your file appears under in the manifest (e.g. print, certificate, stems, readme).\n`);
|
|
1139
|
-
process.
|
|
1164
|
+
process.exitCode = 1;
|
|
1165
|
+
return;
|
|
1140
1166
|
}
|
|
1141
1167
|
assertSettableField(key);
|
|
1142
1168
|
const hasFile = !!(flags.file && flags.file !== 'true');
|
|
1143
1169
|
if (!uri && !hasFile) {
|
|
1144
1170
|
console.error(`usage: ${usage}\n pass a locator URI (ipfs://… / ar://… / https://…) or --file <path> to store bytes on-chain.\n`);
|
|
1145
|
-
process.
|
|
1171
|
+
process.exitCode = 1;
|
|
1172
|
+
return;
|
|
1146
1173
|
}
|
|
1147
1174
|
if (uri && hasFile)
|
|
1148
1175
|
throw new Error('pass EITHER a locator URI OR --file <path>, not both.');
|
|
@@ -1156,19 +1183,44 @@ export async function cmdAttach(rest, flags) {
|
|
|
1156
1183
|
// Locator path (the common case): auto-detect the representation, refuse an unrecognized scheme
|
|
1157
1184
|
// loudly (never silently store a bad value) — fail fast before any RPC. Derive the declared
|
|
1158
1185
|
// mimeType from the extension.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1186
|
+
// Every `<key> <uri>` pair, validated BEFORE anything is sent — a batch that would revert partway
|
|
1187
|
+
// is exactly what this command now exists to prevent, so a bad locator in pair 5 must stop pair 1.
|
|
1188
|
+
if (pairArgs.length % 2 !== 0) {
|
|
1189
|
+
throw new Error(`attach takes <key> <uri> PAIRS; got ${pairArgs.length} positional argument(s) after the address. ` +
|
|
1190
|
+
`Last one seen: "${pairArgs[pairArgs.length - 1]}".`);
|
|
1191
|
+
}
|
|
1192
|
+
const pairs = [];
|
|
1193
|
+
for (let i = 0; i < pairArgs.length; i += 2) {
|
|
1194
|
+
const k = pairArgs[i];
|
|
1195
|
+
const u = pairArgs[i + 1];
|
|
1196
|
+
assertSettableField(k);
|
|
1197
|
+
const rep = representationForLocator(u);
|
|
1198
|
+
if (!rep) {
|
|
1199
|
+
throw new Error(`"${u}" isn't a recognized file locator. Use ipfs://… (pinned/IPFS), ar://… (Arweave), or https://… . ` +
|
|
1200
|
+
'To store literal text or raw bytes on-chain instead, use `abx set-field`.');
|
|
1201
|
+
}
|
|
1202
|
+
pairs.push({ key: k, uri: u, representation: rep, mimeType: contentTypeFromPath(u) });
|
|
1203
|
+
}
|
|
1204
|
+
// A key repeated within one batch would have the later write silently win — the same
|
|
1205
|
+
// full-column-upsert hazard that bit `register` and `lock-field`. Refuse instead.
|
|
1206
|
+
const dupes = pairs.map((p) => p.key).filter((k, i, a) => a.indexOf(k) !== i);
|
|
1207
|
+
if (dupes.length) {
|
|
1208
|
+
throw new Error(`the same key appears twice in one batch: ${[...new Set(dupes)].join(', ')}. ` +
|
|
1209
|
+
'Each field holds ONE active value, so the last write would silently win — attach them separately if that is really what you want.');
|
|
1163
1210
|
}
|
|
1211
|
+
const representation = pairs[0].representation;
|
|
1164
1212
|
const owner = await read(contract, 'owner');
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1213
|
+
for (const p of pairs) {
|
|
1214
|
+
if (p.mimeType === 'application/octet-stream') {
|
|
1215
|
+
console.log(yellow(' ⚠ ') +
|
|
1216
|
+
dim(`no file extension in "${p.uri.slice(0, 64)}" → declared type will be application/octet-stream. `) +
|
|
1217
|
+
dim('Point the URI at the file itself (…/master.tiff, …/coa.pdf) so collectors get the right type.'));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const mimeType = pairs[0].mimeType;
|
|
1221
|
+
for (const p of pairs) {
|
|
1222
|
+
console.log(` attaching ${bold(p.key)} ${dim(`(${p.mimeType}, ${p.representation})`)} to ${scope}: ${dim(p.uri)}`);
|
|
1170
1223
|
}
|
|
1171
|
-
console.log(` attaching ${bold(key)} ${dim(`(${mimeType}, ${representation})`)} to ${scope}: ${dim(uri)}`);
|
|
1172
1224
|
// Where it surfaces. This used to be one dim line, and it read as a footnote rather than as a
|
|
1173
1225
|
// dependency: an integrator attached five audio stems to a fully-on-chain token, paid to store
|
|
1174
1226
|
// them, and found `tokenURI` listed none of them — "paid for, stored on-chain, and invisible".
|
|
@@ -1179,25 +1231,37 @@ export async function cmdAttach(rest, flags) {
|
|
|
1179
1231
|
const uriBase = await read(contract, 'tokenURIBase').catch(() => '');
|
|
1180
1232
|
const artifactsPath = `/t/${chainId()}/${contract}/${flags.token ?? '0'}`;
|
|
1181
1233
|
if (uriBase && uriBase.trim() !== '') {
|
|
1182
|
-
console.log(dim(` → listed in this project's resolver artifacts (${artifactsPath} and /data
|
|
1234
|
+
console.log(dim(` → listed in this project's resolver artifacts (${artifactsPath} and /data/<key>); a bare on-chain tokenURI carries reserved fields plus abx_params only.`));
|
|
1183
1235
|
}
|
|
1184
1236
|
else {
|
|
1185
1237
|
console.log(yellow(' ⚠ ') +
|
|
1186
1238
|
`this project resolves ON-CHAIN (no resolver base baked in), and the on-chain document carries reserved fields plus the computed ${bold('abx_params')} block only — ` +
|
|
1187
|
-
`so ${bold(key)} will NOT appear in ${bold('tokenURI')}. The bytes are stored and provable, but nothing surfaces them to a marketplace or wallet. ` +
|
|
1239
|
+
`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. ` +
|
|
1188
1240
|
dim('(Configured params DO appear on-chain — attachments are the surface that needs a resolver.)'));
|
|
1189
1241
|
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}.`));
|
|
1190
1242
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1243
|
+
// ONE transaction for the whole set. `batchOps` folds a same-target run into a `multicall` and
|
|
1244
|
+
// passes a lone op through untouched, so a single attach is byte-identical to before.
|
|
1245
|
+
const ops = pairs.map((p) => collection
|
|
1246
|
+
? prepareSetContractField({ contract, field: p.key, representation: p.representation, value: toHex(p.uri), chainId: chainId() })
|
|
1247
|
+
: prepareSetTokenField({ contract, tokenId: BigInt(flags.token ?? '0'), field: p.key, representation: p.representation, value: toHex(p.uri), chainId: chainId() }));
|
|
1248
|
+
const batched = batchOps(ops);
|
|
1249
|
+
if (batched.length !== 1) {
|
|
1250
|
+
// Defensive: every op here targets the same contract and carries no value, so batchOps must
|
|
1251
|
+
// return exactly one tx. If that ever changes, fail loudly rather than send a partial set.
|
|
1252
|
+
throw new Error(`attach expected to batch ${ops.length} op(s) into one transaction, got ${batched.length}`);
|
|
1253
|
+
}
|
|
1254
|
+
if (pairs.length > 1) {
|
|
1255
|
+
console.log(dim(` ${pairs.length} artifacts → ONE transaction (all-or-nothing: a revert lands none of them, so no half-written token).`));
|
|
1256
|
+
}
|
|
1257
|
+
const sent = await runWrite(contract, batched[0], flags, owner);
|
|
1195
1258
|
if (sent) {
|
|
1196
1259
|
const id = flags.token ?? '0';
|
|
1197
|
-
|
|
1260
|
+
const names = pairs.map((p) => bold(p.key)).join(', ');
|
|
1261
|
+
console.log(` ${green('✓')} attached — ${names} join${pairs.length > 1 ? '' : 's'} ${scope}'s ${bold('artifacts')} manifest (stored on-chain, anchored).\n` +
|
|
1198
1262
|
dim(` verify (a resolver serves the complete listing): `) +
|
|
1199
|
-
`
|
|
1200
|
-
dim(` ${collection ? '' : `→ artifacts[].key "${key}"; /data
|
|
1263
|
+
`abx tokenuri ${contract}${id === '0' ? '' : ` --token ${id}`} --fetch` +
|
|
1264
|
+
dim(` ${collection ? '' : `→ artifacts[].key ${pairs.map((p) => `"${p.key}"`).join(', ')}; /data/<key> fetches each`}\n`) +
|
|
1201
1265
|
dim(` (The complete file listing is a resolver surface — the bare on-chain tokenURI enumerates reserved fields plus abx_params. Params never need a resolver; attachments do.)\n`));
|
|
1202
1266
|
}
|
|
1203
1267
|
}
|
|
@@ -1306,16 +1370,6 @@ async function readSeries(token, fn, args = []) {
|
|
|
1306
1370
|
args: args,
|
|
1307
1371
|
}));
|
|
1308
1372
|
}
|
|
1309
|
-
async function readSale(minter, token) {
|
|
1310
|
-
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1311
|
-
const [configured, paymentToken, price, allocation, sold] = (await publicClient.readContract({
|
|
1312
|
-
address: minter,
|
|
1313
|
-
abi: abxFixedPriceMinterAbi,
|
|
1314
|
-
functionName: 'sales',
|
|
1315
|
-
args: [token],
|
|
1316
|
-
}));
|
|
1317
|
-
return { configured, paymentToken, price, allocation, sold };
|
|
1318
|
-
}
|
|
1319
1373
|
/** Resolve the shared minter for the chain (flag → env → manifest), or deploy it if none exists.
|
|
1320
1374
|
* It's ownerless, so any funded signer can stand it up — the sibling of `ensureChunkStore`. */
|
|
1321
1375
|
async function ensureFixedPriceMinter(override) {
|
|
@@ -1338,38 +1392,23 @@ async function ensureFixedPriceMinter(override) {
|
|
|
1338
1392
|
}
|
|
1339
1393
|
}
|
|
1340
1394
|
const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
|
|
1395
|
+
const send = makeHotSender({ wallet, account, publicClient });
|
|
1341
1396
|
console.log(dim(' deploying the canonical fixed-price minter (shared, ownerless) — CREATE2…'));
|
|
1342
|
-
const { minter, txHash } = await deployFixedPriceMinter(
|
|
1397
|
+
const { minter, txHash } = await deployFixedPriceMinter(send, publicClient);
|
|
1343
1398
|
console.log(` ${green('✓')} fixed-price minter ${minter}`);
|
|
1344
1399
|
console.log(dim(` tx ${txHash}`));
|
|
1345
1400
|
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)`));
|
|
1346
1401
|
return minter;
|
|
1347
1402
|
}
|
|
1348
1403
|
/** A minter write (configure/buy). No re-index — the sale lives on the minter, not the token
|
|
1349
|
-
* projection; the token's own state is unchanged by a sale config.
|
|
1404
|
+
* projection; the token's own state is unchanged by a sale config.
|
|
1405
|
+
*
|
|
1406
|
+
* Used to hand-roll its own dry-run guard — "the minter path does not go through runWrite, so it
|
|
1407
|
+
* needs its OWN" — which is exactly the duplication {@link gatedSend} exists to end: this now shares
|
|
1408
|
+
* the identical choke point `runWrite` does, so `minter configure`/`minter buy` also pick up
|
|
1409
|
+
* `--confirm` (previously only the deploy family had it) with no extra code here. */
|
|
1350
1410
|
async function runMinterWrite(provider, flags, expectedSigner) {
|
|
1351
|
-
|
|
1352
|
-
// through runWrite, so it needs its OWN dry-run guard — without it, `minter configure --dry-run`
|
|
1353
|
-
// silently signs + broadcasts a real sale (a critical footgun: a mainnet creator "previewing"
|
|
1354
|
-
// their sale would send it). Mirrors runWrite's guard.
|
|
1355
|
-
if (flags['dry-run']) {
|
|
1356
|
-
const prepared = typeof provider === 'function' ? await provider(expectedSigner ?? zeroAddress) : provider;
|
|
1357
|
-
console.log(`\n ${bold('◆ ' + prepared.summary)} ${dim('(dry run — nothing sent)')}`);
|
|
1358
|
-
for (const [k, v] of Object.entries(prepared.fields))
|
|
1359
|
-
console.log(` ${dim(k.padEnd(12))} ${v}`);
|
|
1360
|
-
console.log(` ${dim('to'.padEnd(12))} ${prepared.to ?? dim('(contract deploy)')}`);
|
|
1361
|
-
console.log(dim(`\n Re-run without --dry-run to send (lane: ${laneFromFlags(flags)}).\n`));
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
await assertChainId(CHAIN);
|
|
1365
|
-
await signTx(provider, {
|
|
1366
|
-
lane: laneFromFlags(flags),
|
|
1367
|
-
chainKey: CHAIN,
|
|
1368
|
-
expectedSigner,
|
|
1369
|
-
yes: !!flags.yes,
|
|
1370
|
-
port: flags.port ? Number(flags.port) : undefined,
|
|
1371
|
-
signUrlFile: flags['sign-url-file'],
|
|
1372
|
-
});
|
|
1411
|
+
await gatedSend(provider, flags, { chainKey: CHAIN, expectedSigner });
|
|
1373
1412
|
}
|
|
1374
1413
|
/** Parse the sale price: `--price-raw <units>` (exact base units) or the friendly `--price <ether>`
|
|
1375
1414
|
* (18-decimals). ETH sales use `--price`; non-18-dp ERC-20s should use `--price-raw`. */
|
|
@@ -1379,7 +1418,7 @@ function parseSalePrice(flags, usage) {
|
|
|
1379
1418
|
if (flags.price && flags.price !== 'true')
|
|
1380
1419
|
return parseEther(flags.price);
|
|
1381
1420
|
console.error(`missing --price <ether> or --price-raw <units>\nusage: ${usage}\n`);
|
|
1382
|
-
|
|
1421
|
+
throw new CliError('', 1, true); // already printed above — see CliError's alreadyPrinted doc
|
|
1383
1422
|
}
|
|
1384
1423
|
/**
|
|
1385
1424
|
* `abx minter configure <token> --price <eth> --allocation <n> [--erc20 0x…]` — set the fixed-price
|
|
@@ -1396,7 +1435,7 @@ export async function cmdMinterConfigure(address, flags) {
|
|
|
1396
1435
|
const price = parseSalePrice(flags, usage);
|
|
1397
1436
|
// On --dry-run resolve the minter WITHOUT deploying — ensureFixedPriceMinter would deploy the
|
|
1398
1437
|
// shared singleton if absent, and a preview must never send. Fall back to a label if none exists.
|
|
1399
|
-
const dryRun =
|
|
1438
|
+
const dryRun = isDryRun(flags);
|
|
1400
1439
|
const knownMinter = fixedPriceMinterAddress(flags['minter-contract']);
|
|
1401
1440
|
if (dryRun && !knownMinter) {
|
|
1402
1441
|
console.log(yellow(` ⚠ no shared minter deployed on ${CHAIN} yet — a real run deploys it once (a separate tx) before configuring.`));
|
|
@@ -1452,7 +1491,7 @@ export async function cmdMinterShow(address, flags) {
|
|
|
1452
1491
|
console.log(yellow(` no shared minter known for ${CHAIN} — configure a sale (deploys it) or set ABX_FIXED_PRICE_MINTER`));
|
|
1453
1492
|
return;
|
|
1454
1493
|
}
|
|
1455
|
-
const sale = await
|
|
1494
|
+
const sale = await readSaleConfig(makePublicClient({ chainKey: CHAIN }), minter, token);
|
|
1456
1495
|
const [assignedMinter, payee, paused, max, supply] = await Promise.all([
|
|
1457
1496
|
readSeries(token, 'minter').catch(() => zeroAddress),
|
|
1458
1497
|
readSeries(token, 'primaryPayee').catch(() => zeroAddress),
|
|
@@ -1491,7 +1530,7 @@ export async function cmdMinterBuy(address, flags) {
|
|
|
1491
1530
|
const minter = fixedPriceMinterAddress(flags['minter-contract']);
|
|
1492
1531
|
if (!minter)
|
|
1493
1532
|
throw new Error(`no shared minter known for ${CHAIN} — configure a sale first, or set ABX_FIXED_PRICE_MINTER`);
|
|
1494
|
-
const sale = await
|
|
1533
|
+
const sale = await readSaleConfig(makePublicClient({ chainKey: CHAIN }), minter, token);
|
|
1495
1534
|
if (!sale.configured)
|
|
1496
1535
|
throw new Error(`no sale configured for ${token} — run: abx minter configure ${token} …`);
|
|
1497
1536
|
const isEth = sale.paymentToken === zeroAddress;
|
|
@@ -1539,7 +1578,8 @@ export async function cmdSetSchema(address, flags) {
|
|
|
1539
1578
|
const specs = parseSchemaSpecs(flags.schema);
|
|
1540
1579
|
if (specs.length !== 1) {
|
|
1541
1580
|
console.error(`usage: ${usage}\n\n One key per call — a schema write is a full-row upsert, so batching them hides which one changed.\n`);
|
|
1542
|
-
process.
|
|
1581
|
+
process.exitCode = 1;
|
|
1582
|
+
return;
|
|
1543
1583
|
}
|
|
1544
1584
|
const next = specs[0];
|
|
1545
1585
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
@@ -1611,7 +1651,8 @@ export async function cmdRetireParam(address, rest, flags) {
|
|
|
1611
1651
|
const [key] = positionalArgs(rest);
|
|
1612
1652
|
if (!key) {
|
|
1613
1653
|
console.error(`usage: ${usage}\n`);
|
|
1614
|
-
process.
|
|
1654
|
+
process.exitCode = 1;
|
|
1655
|
+
return;
|
|
1615
1656
|
}
|
|
1616
1657
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1617
1658
|
const owner = await read(contract, 'owner');
|