@artblocks/abx-cli 0.1.0-alpha.29 → 0.1.0-alpha.30

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/dist/ownerops.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import { MAX_ROYALTY_BPS, 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, prepareReduceMaxRoyaltyBps, prepareSetTokenField, batchOps, prepareSetTokenURIBase, prepareSetTokenURIOverride, prepareSetTokenURIRenderer, prepareSetTransferValidator, prepareSetSeedSource, probeSeedSource, readSeedSource, predictSeedSource, getDeployment, prepareTransfer, prepareTransferOwnership, probeTransferValidator, readCreatorTokenStatus, resolveRecommendedTransferValidator, RECOMMENDED_TRANSFER_VALIDATOR, KNOWN_CHAIN_KEYS, resolveChain, resolveRpcUrl, redactRpcUrl, encodeTag, METADATA_FIELD as F, METADATA_REPRESENTATION as R, stageFieldContent, planStagedContent, exceedsOnchainSoftLimit, classifyOnchainReadSize, tokenUriGasEstimate, ONCHAIN_IMAGE_SOFT_LIMIT, ONCHAIN_PROJECT_SOFT_LIMIT, ONCHAIN_READ_WARN_BYTES, ONCHAIN_READ_REFUSE_BYTES, } from '@artblocks/abx-sdk';
10
10
  import { encodeScalarParam, encodeTag as encodeTagSdk, isAccepted, prepareConfigureTokenParam, prepareConfigureTokenParamData, prepareSetContractParam, prepareSetContractParamData, seriesCodeAbi, tryReadContract, } from '@artblocks/abx-sdk';
11
11
  import { hasParamEnumeration, GATEWAY_FIELD, GATEWAY_FLOOR, gatewayPrefixFrom, readCollectionPolicy, readEnv } from '@artblocks/abx-sdk';
12
- import { decodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareLockParamHooks, prepareLockScript, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
12
+ import { decodeFieldRenderer, encodeFieldRenderer, resolveGenerator, DEP_RESOLUTION, parseDependencyRef, prepareLockDependencies, prepareLockParamHooks, prepareLockScript, prepareRemoveLastDependency, prepareSetDependency, prepareSetDependencyRegistry, } from '@artblocks/abx-sdk';
13
13
  import { contentTypeFromPath } from '@artblocks/abx-storage';
14
14
  import { toHex as toHexSdk } from 'viem';
15
15
  import { readFileSync } from 'node:fs';
@@ -18,7 +18,7 @@ import { gzipSync } from 'node:zlib';
18
18
  import { formatEther, getAddress, isAddress, parseEther, toHex, zeroAddress } from 'viem';
19
19
  import { backendResolution, CHAIN, chainId, fixedPriceMinterAddress, fixedPriceMinter1155Address, localIndexer } from './config.js';
20
20
  import { CliError } from './errors.js';
21
- import { detectTokenKind, assertHasParamsSurface } from './kind.js';
21
+ import { detectTokenKind, isEditionContract, assertHasParamsSurface } from './kind.js';
22
22
  import { openWalletSession } from './signer.js';
23
23
  import { gatedSend, laneFromFlags } from './riskgate.js';
24
24
  import { withJson } from './jsonout.js';
@@ -1670,6 +1670,101 @@ export async function stageImageFieldsBatch(imagePaths, compress, send, storeOve
1670
1670
  // failure class this toolkit refuses rather than warns about, so pointing a legacy token at the
1671
1671
  // current generator is REFUSED. (Repointing the metadata RENDERER is safe and unguarded: a v4
1672
1672
  // renderer on a legacy token just emits no params block.)
1673
+ // ── structured representations: `--value` is not raw bytes ───────────────────
1674
+ // Two representations carry a STRUCTURE rather than content, and the metadata renderer decodes
1675
+ // them before it can resolve the field: `renderer` is `abi.encode(address fieldRenderer)` (32
1676
+ // bytes) and `reader`/`reader-gzip` is `abi.encode(address reader, address pointer)` (64 bytes).
1677
+ //
1678
+ // `--value` used to be written to chain VERBATIM. So the natural input — a bare 20-byte address,
1679
+ // exactly what `--image-renderer` takes at deploy — landed as 20 bytes, and `abi.decode(v,
1680
+ // (address))` reverts on anything shorter than a word. The field itself read back fine, which is
1681
+ // what made this so hard to see from outside: `contractField("image")` returned the right
1682
+ // representation and the right-looking address, and `tokenURI` reverted for EVERY token in the
1683
+ // collection. Re-pointing at the renderer the collection was deployed with didn't fix it either —
1684
+ // every post-deploy write had the same wrong shape, so it looked permanent and looked like the
1685
+ // protocol's fault. Reported from the field on alpha.29 after four builds on Base Sepolia.
1686
+ //
1687
+ // Enforce, don't warn: normalize what is unambiguous (a bare address IS the field renderer),
1688
+ // refuse what is not, and never let a shape the renderer cannot decode reach the chain.
1689
+ /** Canonical `abi.encode(address)` — 24 zero nibbles then the 40 address nibbles. */
1690
+ const ABI_WORD_PAD = '0'.repeat(24);
1691
+ /**
1692
+ * The on-chain bytes for `--value` under `representation`. For a structured representation this
1693
+ * normalizes (a bare address → `abi.encode(address)`) and REFUSES anything the renderer could not
1694
+ * decode; every other representation carries raw bytes and passes through untouched.
1695
+ */
1696
+ export function encodeStructuredFieldValue(representation, value) {
1697
+ const hex = value.trim();
1698
+ if (representation === R.renderer) {
1699
+ // A bare 20-byte address: the shape `--image-renderer` takes, and what anyone reading
1700
+ // `contractField` back sees. Encode it rather than making the creator pad it by hand.
1701
+ if (/^0x[0-9a-fA-F]{40}$/.test(hex))
1702
+ return encodeFieldRenderer(getAddress(hex));
1703
+ if (/^0x[0-9a-fA-F]{64}$/.test(hex)) {
1704
+ if (hex.slice(2, 26).toLowerCase() !== ABI_WORD_PAD) {
1705
+ throw new CliError(`--representation renderer needs abi.encode(address) — a 32-byte word whose first 12 bytes are zero.\n` +
1706
+ ` ${hex} is 32 bytes but its high bytes are not zero, so it decodes to a garbage address and every\n` +
1707
+ ` tokenURI in the collection would revert. Pass the field renderer's plain 0x address instead.`);
1708
+ }
1709
+ return hex.toLowerCase();
1710
+ }
1711
+ throw new CliError(`--representation renderer expects the field renderer's 0x address (20 bytes) — not ${byteLen(hex)}.\n` +
1712
+ ` The chain stores this field as abi.encode(address) and the metadata renderer abi.decode()s it before\n` +
1713
+ ` it can call render(); any other length reverts tokenURI for the WHOLE collection.\n` +
1714
+ ` Example: abx set-field <token> --field image --value 0xYourFieldRenderer --representation renderer --collection`);
1715
+ }
1716
+ if (representation === R.reader || representation === R.readerGzip) {
1717
+ const ok = /^0x[0-9a-fA-F]{128}$/.test(hex) &&
1718
+ hex.slice(2, 26).toLowerCase() === ABI_WORD_PAD &&
1719
+ hex.slice(66, 90).toLowerCase() === ABI_WORD_PAD;
1720
+ if (!ok) {
1721
+ throw new CliError(`--representation ${representation} expects abi.encode(address reader, address pointer) — 64 bytes, two\n` +
1722
+ ` zero-padded address words — not ${byteLen(hex)}. Hand-encoding this is rarely what you want:\n` +
1723
+ ` \`abx set-field <token> --field <name> --file <path>\` stages the bytes on chain and writes the\n` +
1724
+ ` reader value for you, correctly.`);
1725
+ }
1726
+ return hex.toLowerCase();
1727
+ }
1728
+ return hex;
1729
+ }
1730
+ /** "20 bytes" / "not hex" — the half of the message that says what the input actually was. */
1731
+ function byteLen(hex) {
1732
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(hex))
1733
+ return `'${hex}' (not an even-length 0x hex string)`;
1734
+ const n = (hex.length - 2) / 2;
1735
+ return `${n} byte${n === 1 ? '' : 's'}`;
1736
+ }
1737
+ /**
1738
+ * Refuse a `renderer` field that points at an address with NO CODE — the same guard
1739
+ * `deploy-code --image-renderer` applies, now on the post-deploy path it was missing from. A
1740
+ * codeless target passes every static check and still reverts every tokenURI.
1741
+ */
1742
+ export async function assertFieldRendererDeployed(value) {
1743
+ let target;
1744
+ try {
1745
+ target = decodeFieldRenderer(value);
1746
+ }
1747
+ catch {
1748
+ return; // not decodable as an address — encodeStructuredFieldValue already refused those
1749
+ }
1750
+ if (target === zeroAddress) {
1751
+ throw new CliError('a `renderer` field pointing at the zero address reverts every tokenURI. To stop computing this field on\n' +
1752
+ ' chain, set it to a real representation (--text / --value with `inline`, `url`, `ipfs`, …) instead.');
1753
+ }
1754
+ const publicClient = makePublicClient({ chainKey: CHAIN });
1755
+ let code;
1756
+ try {
1757
+ code = await publicClient.getCode({ address: target });
1758
+ }
1759
+ catch {
1760
+ return; // node unreachable — the owner read upstream already proved it was, so don't invent a failure
1761
+ }
1762
+ if (!code || code === '0x') {
1763
+ throw new CliError(`${target} has NO code on '${CHAIN}' — that is not a deployed IAbxFieldRenderer, and pointing a field at it\n` +
1764
+ ` reverts tokenURI for every token in the collection. Deploy the renderer first (\`abx scaffold-renderer\`\n` +
1765
+ ` → forge test → forge script), then pass the address it printed.`);
1766
+ }
1767
+ }
1673
1768
  /** The field-renderer address a `--value` names: the canonical `abi.encode(address)` (32 bytes),
1674
1769
  * or a bare 20-byte address. Null when it is neither. */
1675
1770
  function fieldRendererTarget(value) {
@@ -1795,7 +1890,62 @@ export async function cmdSetField(address, flags) {
1795
1890
  value = requireFlag(flags, 'value', 'abx set-field <address> --field <name> --text … | --value 0x…');
1796
1891
  representation = flags.representation ?? R.keccak256;
1797
1892
  }
1798
- await runWrite(contract, buildTx(value, representation), flags, owner);
1893
+ // A structured representation is decoded on chain before the field can resolve — normalize the
1894
+ // shape and refuse an undecodable one HERE, where it costs nothing, rather than on the read path
1895
+ // where it costs the whole collection's tokenURI. (Staged content already arrives correctly
1896
+ // encoded from putContentOnChain; this only ever changes a hand-passed --value.)
1897
+ value = encodeStructuredFieldValue(representation, value);
1898
+ if (representation === R.renderer)
1899
+ await assertFieldRendererDeployed(value);
1900
+ const sent = await runWrite(contract, buildTx(value, representation), flags, owner);
1901
+ // Then prove the thing the creator actually cares about: that the served document still reads.
1902
+ // A field renderer is the project's OWN contract — it can revert for reasons no static check
1903
+ // sees — so the honest confirmation is to call tokenURI once, after the write.
1904
+ if (sent)
1905
+ await reportUriAfterFieldWrite(contract, collection, flags);
1906
+ }
1907
+ /**
1908
+ * After a field write lands, staticcall what a marketplace calls and say plainly whether it still
1909
+ * resolves. The write is already on chain, so this never throws — a reverting `tokenURI` is
1910
+ * reported with the read that proves it and the write that undoes it.
1911
+ *
1912
+ * This is the check that would have caught the `renderer` encoding bug on the first collection
1913
+ * instead of the fourth: `abx verify` surfaced the reverting tokenURI, but only when it was next
1914
+ * run, and nothing tied it back to the write that caused it.
1915
+ */
1916
+ async function reportUriAfterFieldWrite(contract, collection, flags) {
1917
+ const publicClient = makePublicClient({ chainKey: CHAIN });
1918
+ const tokenId = BigInt(flags.token ?? '0');
1919
+ // An ERC-1155 edition serves `uri(id)`, not `tokenURI(id)` — read the one this contract has, or the
1920
+ // check would report a healthy edition as broken.
1921
+ const edition = await isEditionContract(publicClient, contract).catch(() => false);
1922
+ const fn = edition ? 'uri' : 'tokenURI';
1923
+ const abi = edition ? oneOfOneEditionAbi : oneOfOneImageAbi;
1924
+ const uri = await tryReadContract(publicClient, { address: contract, abi, functionName: fn, args: [tokenId] });
1925
+ if (uri !== undefined) {
1926
+ console.log(` ${green('✓')} ${fn}(${tokenId}) still resolves`);
1927
+ return;
1928
+ }
1929
+ {
1930
+ // A collection with nothing minted yet has no token to read — not a failure, just nothing to
1931
+ // prove. Separate that from a real revert before alarming anyone.
1932
+ const supply = await tryReadContract(publicClient, {
1933
+ address: contract,
1934
+ abi: oneOfOneImageAbi,
1935
+ functionName: 'totalSupply',
1936
+ args: [],
1937
+ });
1938
+ if (supply === 0n) {
1939
+ console.log(dim(` (no tokens minted yet — nothing to read back; run \`abx verify ${contract}\` after the first mint)`));
1940
+ return;
1941
+ }
1942
+ console.log(yellow(' ⚠ ') +
1943
+ `${fn}(${tokenId}) REVERTS after this write — the field is on chain but the served document no longer reads.\n` +
1944
+ ` confirm: abx tokenuri ${contract} ${tokenId}\n` +
1945
+ ` This is recoverable: the field is not locked until you \`abx lock-field\` it, so re-setting '${flags.field}'\n` +
1946
+ ` to a working value${collection ? ' --collection' : ''} restores it. Check the renderer's own render() first —\n` +
1947
+ ` a field renderer must NEVER revert.`);
1948
+ }
1799
1949
  }
1800
1950
  // ── attach (the data-plane verb: put a named file on a token) ─────────────────
1801
1951
  /**