@artblocks/abx-cli 0.1.0-alpha.40 → 0.1.0-alpha.41
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 +145 -0
- package/assets/renderer-scaffold/README.md +12 -0
- package/assets/renderer-scaffold/foundry.toml +4 -0
- package/assets/renderer-scaffold/script/Preview.s.sol +99 -0
- package/dist/capabilities.d.ts +9 -4
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +13 -4
- package/dist/capabilities.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +731 -65
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/maintenance.d.ts +4 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +114 -0
- package/dist/commands/maintenance.js.map +1 -0
- package/dist/commands/project.d.ts +175 -3
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +333 -27
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts +21 -0
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +171 -4
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +5 -0
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/service.d.ts.map +1 -1
- package/dist/commands/service.js +7 -0
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +32 -3
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/submit-app.d.ts +43 -0
- package/dist/commands/submit-app.d.ts.map +1 -1
- package/dist/commands/submit-app.js +115 -6
- package/dist/commands/submit-app.js.map +1 -1
- package/dist/config.d.ts +16 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +73 -7
- package/dist/config.js.map +1 -1
- package/dist/deploy-plan.d.ts +150 -0
- package/dist/deploy-plan.d.ts.map +1 -0
- package/dist/deploy-plan.js +31 -0
- package/dist/deploy-plan.js.map +1 -0
- package/dist/flag-allowlists.d.ts.map +1 -1
- package/dist/flag-allowlists.js +5 -1
- package/dist/flag-allowlists.js.map +1 -1
- package/dist/flags.d.ts +6 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +14 -0
- package/dist/flags.js.map +1 -1
- package/dist/jsonout.d.ts.map +1 -1
- package/dist/jsonout.js +11 -0
- package/dist/jsonout.js.map +1 -1
- package/dist/main.js +46 -7
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +69 -0
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +221 -17
- package/dist/ownerops.js.map +1 -1
- package/dist/script-chunks.d.ts +1 -1
- package/dist/script-chunks.d.ts.map +1 -1
- package/dist/script-chunks.js +2 -2
- package/dist/script-chunks.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +6 -4
- package/skill/reference/code.md +17 -8
- package/skill/reference/deploy.md +9 -2
- package/skill/reference/diagnose.md +15 -2
- package/skill/reference/hosting.md +26 -0
- package/skill/reference/operate.md +43 -4
- package/skill/reference/setup.md +14 -0
package/dist/ownerops.js
CHANGED
|
@@ -10,15 +10,18 @@ import { MAX_ROYALTY_BPS, assertChainId, ensureChunkStore as sdkEnsureChunkStore
|
|
|
10
10
|
import { encodeScalarParam, encodeTag as encodeTagSdk, isAccepted, prepareConfigureTokenParam, prepareConfigureTokenParamData, prepareSetContractParam, prepareSetContractParamData, seriesCodeAbi, tryReadContract, } from '@artblocks/abx-sdk';
|
|
11
11
|
import { hasParamEnumeration, GATEWAY_FIELD, GATEWAY_FLOOR, gatewayPrefixFrom, readCollectionPolicy, readEnv } from '@artblocks/abx-sdk';
|
|
12
12
|
import { decodeFieldRenderer, encodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareLockParamHooks, prepareLockScript, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
|
|
13
|
+
import { planScriptReplace, prepareRemoveLastScriptChunk, prepareSetScriptChunk, verifyScriptReplace, } from '@artblocks/abx-sdk';
|
|
13
14
|
import { contentTypeFromPath } from '@artblocks/abx-storage';
|
|
14
15
|
import { toHex as toHexSdk } from 'viem';
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
17
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
17
18
|
import { gzipSync } from 'node:zlib';
|
|
18
19
|
import { formatEther, getAddress, isAddress, parseEther, toHex, zeroAddress } from 'viem';
|
|
19
20
|
import { backendResolution, CHAIN, chainId, fixedPriceMinterAddress, fixedPriceMinter1155Address, localIndexer } from './config.js';
|
|
20
21
|
import { CliError } from './errors.js';
|
|
21
|
-
import { detectTokenKind, isEditionContract, assertHasParamsSurface } from './kind.js';
|
|
22
|
+
import { detectTokenKind, isEditionContract, assertHasParamsSurface, hasParamsSurface } from './kind.js';
|
|
23
|
+
import { planOnChainScript } from './script-chunks.js';
|
|
24
|
+
import { fetchServedTokenUri, servedOk } from './served.js';
|
|
22
25
|
import { openWalletSession } from './signer.js';
|
|
23
26
|
import { gatedSend, laneFromFlags } from './riskgate.js';
|
|
24
27
|
import { withJson } from './jsonout.js';
|
|
@@ -30,11 +33,12 @@ import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
|
30
33
|
// definition below).
|
|
31
34
|
export { ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, ETH_CALL_GAS_FLOOR, tokenUriGasEstimate, readableBytesAtGas };
|
|
32
35
|
// ── ANSI (local) ─────────────────────────────────────────────────────────────
|
|
33
|
-
const C = { reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[38;5;115m', yellow: '\x1b[38;5;221m' };
|
|
36
|
+
const C = { reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[38;5;115m', yellow: '\x1b[38;5;221m', red: '\x1b[31m' };
|
|
34
37
|
const dim = (s) => `${C.dim}${s}${C.reset}`;
|
|
35
38
|
const green = (s) => `${C.green}${s}${C.reset}`;
|
|
36
39
|
const yellow = (s) => `${C.yellow}${s}${C.reset}`;
|
|
37
40
|
const bold = (s) => `${C.bold}${s}${C.reset}`;
|
|
41
|
+
const red = (s) => `${C.red}${s}${C.reset}`;
|
|
38
42
|
// ── data plane: attach files, and the keys you can't set by hand ──────────────
|
|
39
43
|
// A token anchors named, typed files ("artifacts"). The served JSON's `artifacts` list is the
|
|
40
44
|
// COMPLETE set — but it's COMPUTED by the resolver/renderer from the token's fields + effect
|
|
@@ -532,6 +536,139 @@ export async function cmdLockScript(address, flags) {
|
|
|
532
536
|
console.log(dim(' that is not the same as a frozen OUTPUT: params stay writable and a Registry dependency is re-fetched live, so say "locked metadata" to a buyer, not "immutable".'));
|
|
533
537
|
await runWrite(contract, prepareLockScript({ contract, chainId: chainId() }), flags, owner);
|
|
534
538
|
}
|
|
539
|
+
/** Flags `abx replace-script` recognizes — anything else warns (non-fatal). */
|
|
540
|
+
const REPLACE_SCRIPT_FLAGS = [...SHARED_WRITE_FLAGS, 'script', 'chunk-size'];
|
|
541
|
+
/** A `ScriptChunkReader` backed by a real chain read — the only place `replace-script` touches
|
|
542
|
+
* `seriesCodeAbi` directly, so `planScriptReplace`/`verifyScriptReplace` themselves stay chain-free
|
|
543
|
+
* and testable (see packages/sdk/src/script-chunks.ts). `scriptChunk` returns `null` on a revert
|
|
544
|
+
* (an index at/past the CURRENT on-chain count) rather than throwing — exactly what both SDK
|
|
545
|
+
* functions expect for "nothing stored here yet". */
|
|
546
|
+
function chainScriptReader(contract) {
|
|
547
|
+
const client = makePublicClient({ chainKey: CHAIN });
|
|
548
|
+
return {
|
|
549
|
+
scriptChunkCount: async () => Number(await client.readContract({ address: contract, abi: seriesCodeAbi, functionName: 'scriptChunkCount' })),
|
|
550
|
+
scriptChunk: async (index) => {
|
|
551
|
+
try {
|
|
552
|
+
return (await client.readContract({ address: contract, abi: seriesCodeAbi, functionName: 'scriptChunk', args: [BigInt(index)] }));
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* `abx replace-script <address> --script <path>` — the safe, first-class way to replace an
|
|
562
|
+
* UNLOCKED code project's on-chain program (#119). `OnChainScript.sol` permits `setScriptChunk`/
|
|
563
|
+
* `removeLastScriptChunk` right up until `lock-script` — but before this command the only writer
|
|
564
|
+
* was `deploy-code` (its initial setup, or its `--resume` leg for a setup that never landed).
|
|
565
|
+
* Neither is "replace a program that's already live and correct on-chain", which is the gap this
|
|
566
|
+
* closes: a creator iterating on a generative project pre-lock had no supported way to ship a fix.
|
|
567
|
+
*
|
|
568
|
+
* Safety properties, in the order they're enforced:
|
|
569
|
+
* 1. REFUSE outright — never warn-and-proceed — on a locked script or a non-code target. Both are
|
|
570
|
+
* "there is nothing here for this command to safely do", not "proceed with caution".
|
|
571
|
+
* 2. Diff by CONTENT against what's on-chain now ({@link planScriptReplace}, mirroring
|
|
572
|
+
* `resume.ts`'s script-chunk leg) so an index that already matches is never re-sent — SSTORE2
|
|
573
|
+
* deposit is 200 gas/byte, so a needless re-store is the most expensive way to be "safe".
|
|
574
|
+
* 3. Every write AND every remove rides in ONE atomic `multicall` (`batchOps`, the same primitive
|
|
575
|
+
* `abx attach` uses for its own all-or-nothing guarantee). This is what makes "half-applied"
|
|
576
|
+
* structurally impossible from this command's own send: a revert undoes everything, a success
|
|
577
|
+
* lands everything. Writes are ordered before removes — the retained indices get their correct
|
|
578
|
+
* content before the tail is trimmed, so the intermediate calldata sequence never shrinks the
|
|
579
|
+
* script before it's right (the two groups touch disjoint indices, so this is a documentation
|
|
580
|
+
* choice about intent, not a correctness requirement of the atomic call itself).
|
|
581
|
+
* 4. After a real send, READ THE SCRIPT BACK and verify it reassembles EXACTLY to the source file
|
|
582
|
+
* ({@link verifyScriptReplace}) before reporting success. A transaction not reverting proves the
|
|
583
|
+
* EVM accepted each call; it does not prove the final program is what was intended. This is the
|
|
584
|
+
* step that actually justifies the command existing — skip it and "replace-script" is just
|
|
585
|
+
* "send some txs and hope".
|
|
586
|
+
*
|
|
587
|
+
* `--dry-run`/`--sign`/`--unsigned` all route through the shared `runWrite`/`gatedSend` choke point
|
|
588
|
+
* (the same one every owner-op uses), so a dry run sends nothing, per the house invariant.
|
|
589
|
+
*/
|
|
590
|
+
export async function cmdReplaceScript(address, flags) {
|
|
591
|
+
const usage = 'abx replace-script <address> --script <path> [--chunk-size <bytes>] [--sign|--unsigned] [--dry-run]';
|
|
592
|
+
const contract = requireAddress(address, usage);
|
|
593
|
+
warnStrayFlags(flags, new Set(REPLACE_SCRIPT_FLAGS), 'replace-script');
|
|
594
|
+
const scriptPath = requireFlag(flags, 'script', usage);
|
|
595
|
+
if (!existsSync(scriptPath)) {
|
|
596
|
+
throw new Error(`no file at ${scriptPath} — pass the path to the replacement program (the same --script a deploy-code would take).`);
|
|
597
|
+
}
|
|
598
|
+
const source = readFileSync(scriptPath, 'utf8');
|
|
599
|
+
// REFUSE (not warn) on a target this command cannot safely act on — before any signing prompt.
|
|
600
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
601
|
+
const kind = await detectTokenKind(publicClient, contract);
|
|
602
|
+
if (!hasParamsSurface(kind)) {
|
|
603
|
+
throw new Error(`${contract} is a ${kind.label} — replace-script only applies to a SeriesCode/EditionCode on-chain program ` +
|
|
604
|
+
`(the OnChainScript extension). ${kind.label} has no script to replace.`);
|
|
605
|
+
}
|
|
606
|
+
const scriptLocked = (await publicClient.readContract({ address: contract, abi: seriesCodeAbi, functionName: 'scriptLocked' }));
|
|
607
|
+
if (scriptLocked) {
|
|
608
|
+
throw new Error(`${contract}'s script is LOCKED (\`abx lock-script\` was already sent) — setScriptChunk/removeLastScriptChunk ` +
|
|
609
|
+
`revert forever, on-chain, for everyone. There is no way to replace it; that refusal IS the safety property ` +
|
|
610
|
+
`lock-script exists to guarantee, so this command does not attempt a workaround.`);
|
|
611
|
+
}
|
|
612
|
+
const owner = await read(contract, 'owner');
|
|
613
|
+
const chunkSize = flags['chunk-size'] !== undefined ? Number(flags['chunk-size']) : undefined;
|
|
614
|
+
if (chunkSize !== undefined && (!Number.isFinite(chunkSize) || chunkSize < 1)) {
|
|
615
|
+
throw new Error(`--chunk-size must be a positive integer, got "${flags['chunk-size']}"`);
|
|
616
|
+
}
|
|
617
|
+
const nextChunks = planOnChainScript(source, chunkSize);
|
|
618
|
+
if (nextChunks.length === 0) {
|
|
619
|
+
console.log(yellow(' ⚠ ') + `${scriptPath} is empty — this will REMOVE every existing script chunk, leaving no program on-chain. Ctrl-C now if that's not what you meant.`);
|
|
620
|
+
}
|
|
621
|
+
const reader = chainScriptReader(contract);
|
|
622
|
+
const plan = await planScriptReplace(reader, nextChunks);
|
|
623
|
+
console.log(` script: ${plan.currentCount} chunk(s) on-chain → ${plan.targetCount} chunk(s) in ${scriptPath}`);
|
|
624
|
+
if (plan.noop) {
|
|
625
|
+
console.log(` ${green('✓')} no-op — the on-chain script already matches ${scriptPath} byte-for-byte. Nothing to send.`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (plan.toWrite.length) {
|
|
629
|
+
console.log(dim(` write ${plan.toWrite.length} chunk(s): index ${plan.toWrite.map((w) => w.index).join(', ')}`));
|
|
630
|
+
}
|
|
631
|
+
if (plan.toRemove) {
|
|
632
|
+
console.log(dim(` remove ${plan.toRemove} trailing chunk(s) — shrinking ${plan.currentCount} → ${plan.targetCount} (removeLastScriptChunk has no "remove N"; this is ${plan.toRemove} queued call(s))`));
|
|
633
|
+
}
|
|
634
|
+
// Writes before removes (see the function doc for why this ordering, not the reverse). Both
|
|
635
|
+
// groups fold into ONE multicall below — that's what makes a half-applied result impossible from
|
|
636
|
+
// this command's own send, not the ordering.
|
|
637
|
+
const ops = [
|
|
638
|
+
...plan.toWrite.map((w) => prepareSetScriptChunk({ contract, index: w.index, chunk: w.hex, chainId: chainId() })),
|
|
639
|
+
...Array.from({ length: plan.toRemove }, () => prepareRemoveLastScriptChunk({ contract, chainId: chainId() })),
|
|
640
|
+
];
|
|
641
|
+
const batched = batchOps(ops);
|
|
642
|
+
if (batched.length !== 1) {
|
|
643
|
+
// Defensive: every op above targets the SAME contract and carries no value, so batchOps must
|
|
644
|
+
// fold them to exactly one multicall. If that ever changes, fail loudly rather than send N
|
|
645
|
+
// separate transactions with no atomicity — the one property this command exists to guarantee.
|
|
646
|
+
throw new Error(`replace-script expected to batch ${ops.length} op(s) into one transaction, got ${batched.length}`);
|
|
647
|
+
}
|
|
648
|
+
// `batchOps`/`prepareMulticall` don't sum a folded gasFloor from their sub-ops (each PER-CHUNK
|
|
649
|
+
// gasFloor is real physics — see prepareSetScriptChunk's own doc — but is dropped once several
|
|
650
|
+
// ops merge into one multicall PreparedTx). Restate it here so the eth_estimateGas sanity check
|
|
651
|
+
// still fires for a replacement that stores real bytes.
|
|
652
|
+
const depositBytes = plan.toWrite.reduce((sum, w) => sum + (w.hex.length - 2) / 2, 0);
|
|
653
|
+
const tx = depositBytes > 0 ? { ...batched[0], gasFloor: `0x${(depositBytes * 200).toString(16)}` } : batched[0];
|
|
654
|
+
const sent = await runWrite(contract, tx, flags, owner);
|
|
655
|
+
if (!sent)
|
|
656
|
+
return; // dry run, or the cold lane — nothing landed to verify yet
|
|
657
|
+
const verify = await verifyScriptReplace(reader, source);
|
|
658
|
+
if (!verify.ok) {
|
|
659
|
+
console.error(`\n ${red('✗')} VERIFICATION FAILED — the on-chain script does NOT reassemble to ${scriptPath} after this write.\n` +
|
|
660
|
+
` on-chain now reports ${verify.chunkCount} chunk(s) (wanted ${plan.targetCount}). This can mean the write only\n` +
|
|
661
|
+
` partially landed, or something else wrote to this contract concurrently — either way, DO NOT assume the\n` +
|
|
662
|
+
` program is what you intended.\n` +
|
|
663
|
+
` Way forward: re-run \`abx replace-script ${contract} --script ${scriptPath}\` — it re-diffs against whatever\n` +
|
|
664
|
+
` is ACTUALLY on-chain now and sends only what's still missing or wrong; it will not resend what already matches.\n` +
|
|
665
|
+
` Inspect by hand: abx tokenuri ${contract} --fetch (or scriptChunkCount/scriptChunk directly).\n`);
|
|
666
|
+
process.exitCode = 1;
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
console.log(` ${green('✓')} verified — the on-chain script reassembles EXACTLY to ${scriptPath} (${verify.chunkCount} chunk(s), ${source.length} bytes).\n` +
|
|
670
|
+
dim(` this is the safety property \`abx lock-script ${contract}\` later makes permanent.`));
|
|
671
|
+
}
|
|
535
672
|
/** `abx lock-param-hooks <address>` — freeze the three param-lifecycle hook addresses forever.
|
|
536
673
|
* The sibling of lock-script/lock-dependencies/lock-uri, and the only one aimed at a BUYER rather
|
|
537
674
|
* than at metadata: the transfer hook is a veto over transfers and mints, so an unlocked hook set
|
|
@@ -1977,6 +2114,42 @@ async function reportUriAfterFieldWrite(contract, collection, flags) {
|
|
|
1977
2114
|
` a field renderer must NEVER revert.`);
|
|
1978
2115
|
}
|
|
1979
2116
|
}
|
|
2117
|
+
// ── attach: the canonical fetch URL + a real reachability probe (#127) ────────
|
|
2118
|
+
/**
|
|
2119
|
+
* The exact, directly-fetchable URL for one attached key — `{base}/{chainId}/{address}[/{tokenId}]
|
|
2120
|
+
* /data/{key}`, the token-api route grammar (`packages/token-api/src/server.ts`'s `/t` and `/c`
|
|
2121
|
+
* routes). `base` already carries its OWN `/t` or `/c` prefix (see `TokenURI.sol`/`ContractURI.sol`
|
|
2122
|
+
* — `deploy-code`/`deploy`/etc. bake `tokenURIBase = "${host}/t"`), so this never re-adds one.
|
|
2123
|
+
* Exported so a creator or an agent never hand-assembles this from the docs — the exact gap #127
|
|
2124
|
+
* reported (`attach` produced a bare path, not a URL anyone could actually GET).
|
|
2125
|
+
*/
|
|
2126
|
+
export function canonicalAttachmentUrl(opts) {
|
|
2127
|
+
const base = opts.uriBase.replace(/\/+$/, '');
|
|
2128
|
+
const coords = opts.tokenId === undefined ? `${opts.chainId}/${opts.contract}` : `${opts.chainId}/${opts.contract}/${opts.tokenId}`;
|
|
2129
|
+
return `${base}/${coords}/data/${encodeURIComponent(opts.key)}`;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Is ANY resolver actually answering for this project right now — the precondition for an
|
|
2133
|
+
* attachment to ever become fetchable, and the check `attach` never ran before #127 (it warned on
|
|
2134
|
+
* an EMPTY resolver base, but a non-empty one that answered nothing got a silent, confident "listed
|
|
2135
|
+
* in this project's resolver artifacts"). Reuses `cmdVerifyRemote`'s own GET-and-classify shape
|
|
2136
|
+
* (`served.ts`'s `fetchServedTokenUri`) rather than a second HTTP client.
|
|
2137
|
+
*
|
|
2138
|
+
* Deliberately probes the token's (or collection's) OWN metadata document — NEVER the specific key
|
|
2139
|
+
* being attached. That document already exists for any minted token; the key being attached here
|
|
2140
|
+
* does not exist server-side until the write this gates on lands and the resolver re-indexes, so
|
|
2141
|
+
* probing it directly would 404 on a perfectly healthy resolver and cry wolf on every attach.
|
|
2142
|
+
*/
|
|
2143
|
+
export async function probeResolverReachable(opts) {
|
|
2144
|
+
const base = opts.uriBase.replace(/\/+$/, '');
|
|
2145
|
+
const coords = opts.tokenId === undefined ? `${opts.chainId}/${opts.contract}` : `${opts.chainId}/${opts.contract}/${opts.tokenId}`;
|
|
2146
|
+
const url = `${base}/${coords}`;
|
|
2147
|
+
const served = await fetchServedTokenUri(url, { fetchFn: opts.fetchFn });
|
|
2148
|
+
if (servedOk(served))
|
|
2149
|
+
return { reachable: true, url, detail: `HTTP ${served.status}` };
|
|
2150
|
+
const detail = served.error ? `no response: ${served.error}` : served.status !== null ? `HTTP ${served.status}` : (served.skipped ?? 'unreachable');
|
|
2151
|
+
return { reachable: false, url, detail };
|
|
2152
|
+
}
|
|
1980
2153
|
// ── attach (the data-plane verb: put a named file on a token) ─────────────────
|
|
1981
2154
|
/**
|
|
1982
2155
|
* `abx attach <address> <key> <uri>` — attach a named, typed file to a token (or the collection),
|
|
@@ -2085,24 +2258,46 @@ export async function cmdAttach(rest, flags) {
|
|
|
2085
2258
|
for (const p of pairs) {
|
|
2086
2259
|
console.log(` attaching ${bold(p.key)} ${dim(`(${p.mimeType}, ${p.representation})`)} to ${scope}: ${dim(p.uri)}`);
|
|
2087
2260
|
}
|
|
2088
|
-
// Where it surfaces
|
|
2089
|
-
//
|
|
2090
|
-
//
|
|
2091
|
-
//
|
|
2092
|
-
//
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
|
|
2096
|
-
|
|
2261
|
+
// Where it surfaces — and whether anything can actually SERVE it (#127). This used to be one dim
|
|
2262
|
+
// line assuming a non-empty resolver base meant the artifact would be listed; it never checked.
|
|
2263
|
+
// An integrator attached five audio stems to a fully-on-chain token, paid to store them, and found
|
|
2264
|
+
// `tokenURI` listed none of them — "paid for, stored on-chain, and invisible". The on-chain
|
|
2265
|
+
// renderer deliberately omits locator-represented artifacts (they duplicate no on-chain type
|
|
2266
|
+
// information — see site/content/docs/protocol/data-plane.mdx), so the complete artifacts manifest
|
|
2267
|
+
// comes from a RESOLVER — and a resolver base baked on-chain is a CONFIGURATION, not proof one is
|
|
2268
|
+
// actually running and knows this project. `--collection` reads the collection-scope base
|
|
2269
|
+
// (`contractURIBase`); a token-scope attach reads `tokenURIBase` — these are two separate on-chain
|
|
2270
|
+
// fields (the earlier version of this always read `tokenURIBase`, silently wrong for --collection).
|
|
2271
|
+
const uriBase = await read(contract, collection ? 'contractURIBase' : 'tokenURIBase').catch(() => '');
|
|
2272
|
+
const tokenIdForUrl = collection ? undefined : BigInt(flags.token ?? '0');
|
|
2273
|
+
let reachability = null;
|
|
2097
2274
|
if (uriBase && uriBase.trim() !== '') {
|
|
2098
|
-
|
|
2275
|
+
// The canonical fetch URL for every key, printed directly — no hand-assembling a resolver
|
|
2276
|
+
// route from the docs. A PROMISE, not yet a fact: the key doesn't exist server-side until the
|
|
2277
|
+
// write below lands and the resolver re-indexes, so it is printed here, never probed here.
|
|
2278
|
+
for (const p of pairs) {
|
|
2279
|
+
console.log(dim(` → fetch: ${canonicalAttachmentUrl({ uriBase, chainId: chainId(), contract, tokenId: tokenIdForUrl, key: p.key })}`));
|
|
2280
|
+
}
|
|
2281
|
+
// What CAN be checked before any write: does ANY resolver answer for this project at all right
|
|
2282
|
+
// now — the precondition for the URL(s) above to ever resolve. Same GET-and-classify shape
|
|
2283
|
+
// `cmdVerifyRemote` uses for its own reachability check (served.ts).
|
|
2284
|
+
reachability = await probeResolverReachable({ uriBase, chainId: chainId(), contract, tokenId: tokenIdForUrl });
|
|
2285
|
+
if (reachability.reachable) {
|
|
2286
|
+
console.log(dim(` resolver answers for this project (${reachability.detail}) — the URL(s) above start serving once this write lands and re-indexes.`));
|
|
2287
|
+
}
|
|
2288
|
+
else {
|
|
2289
|
+
console.log(yellow(' ⚠ ') +
|
|
2290
|
+
`the resolver base baked on-chain (${uriBase}) does not currently answer for ${scope} (${reachability.detail}) — ` +
|
|
2291
|
+
`the on-chain FIELD WRITE below will still succeed, but nothing is known to be able to SERVE ${pairs.map((p) => bold(p.key)).join(', ')} right now. ` +
|
|
2292
|
+
`Local: is ${bold('abx serve')} running, and is this project ${bold('abx add')}-ed + indexed? Hosted: ${bold(`abx add ${contract} --remote <name>`)} then ${bold(`abx index ${contract} --remote <name>`)}.`);
|
|
2293
|
+
}
|
|
2099
2294
|
}
|
|
2100
2295
|
else {
|
|
2101
2296
|
console.log(yellow(' ⚠ ') +
|
|
2102
|
-
`this project resolves ON-CHAIN (no resolver base baked in),
|
|
2103
|
-
`so ${pairs.map((p) => bold(p.key)).join(', ')} will
|
|
2297
|
+
`this project resolves ON-CHAIN (no resolver base baked in) — the on-chain FIELD WRITE below will still succeed, but there is NO off-chain serving path at all: the on-chain document carries reserved fields only, ` +
|
|
2298
|
+
`so ${pairs.map((p) => bold(p.key)).join(', ')} will NEVER appear in ${bold('tokenURI')} for ANY resolver. The bytes are stored and provable, but nothing surfaces them to a marketplace or wallet. ` +
|
|
2104
2299
|
dim('(Params are chain state, readable with abx tokens and by any contract call — attachments are the surface that needs a resolver.)'));
|
|
2105
|
-
console.log(dim(` to make attached artifacts
|
|
2300
|
+
console.log(dim(` to make attached artifacts servable at all, point the project at a resolver (${bold('abx deploy-resolver')}, or a managed one via ${bold('abx add <addr> --remote <name>')}) — then re-run this.`));
|
|
2106
2301
|
}
|
|
2107
2302
|
// ONE transaction for the whole set. `batchOps` folds a same-target run into a `multicall` and
|
|
2108
2303
|
// passes a lone op through untouched, so a single attach is byte-identical to before.
|
|
@@ -2122,7 +2317,16 @@ export async function cmdAttach(rest, flags) {
|
|
|
2122
2317
|
if (sent) {
|
|
2123
2318
|
const id = flags.token ?? '0';
|
|
2124
2319
|
const names = pairs.map((p) => bold(p.key)).join(', ');
|
|
2125
|
-
|
|
2320
|
+
// The distinction #127 asked for: the on-chain FIELD WRITE succeeding (this line, always true
|
|
2321
|
+
// when we get here) is not the same fact as the artifact being SERVABLE — say both, separately,
|
|
2322
|
+
// rather than one "✓ attached" that a reader takes to mean "and it's live".
|
|
2323
|
+
const servingLine = !uriBase || uriBase.trim() === ''
|
|
2324
|
+
? dim(` serving: NONE — this project resolves fully on-chain (see the warning above); the bytes are stored and provable, but no resolver will ever list them.\n`)
|
|
2325
|
+
: reachability?.reachable
|
|
2326
|
+
? dim(` serving: a resolver answered for ${scope} just before this write (see above); fetch the URL(s) above once it re-indexes to confirm.\n`)
|
|
2327
|
+
: dim(` serving: NOT CONFIRMED — no resolver answered for ${scope} just before this write (see the warning above). The FIELD is on-chain now; SERVING it is a separate, still-unmet, concern.\n`);
|
|
2328
|
+
console.log(` ${green('✓')} on-chain field write succeeded — ${names} join${pairs.length > 1 ? '' : 's'} ${scope}'s ${bold('artifacts')} manifest (stored on-chain, anchored).\n` +
|
|
2329
|
+
servingLine +
|
|
2126
2330
|
dim(` verify (a resolver serves the complete listing): `) +
|
|
2127
2331
|
`abx tokenuri ${contract}${id === '0' ? '' : ` --token ${id}`} --fetch` +
|
|
2128
2332
|
dim(` ${collection ? '' : `→ artifacts[].key ${pairs.map((p) => `"${p.key}"`).join(', ')}; /data/<key> fetches each`}\n`) +
|