@artblocks/abx-sdk 0.1.0-alpha.3 → 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.
Files changed (147) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +130 -0
  3. package/dist/abi/generated.d.ts +8502 -129
  4. package/dist/abi/generated.d.ts.map +1 -1
  5. package/dist/abi/generated.js +40 -22
  6. package/dist/abi/generated.js.map +1 -1
  7. package/dist/abi/index.d.ts +2645 -168
  8. package/dist/abi/index.d.ts.map +1 -1
  9. package/dist/abi/index.js +34 -7
  10. package/dist/abi/index.js.map +1 -1
  11. package/dist/anchors.d.ts +318 -0
  12. package/dist/anchors.d.ts.map +1 -0
  13. package/dist/anchors.js +701 -0
  14. package/dist/anchors.js.map +1 -0
  15. package/dist/chains.d.ts +19 -9
  16. package/dist/chains.d.ts.map +1 -1
  17. package/dist/chains.js +31 -15
  18. package/dist/chains.js.map +1 -1
  19. package/dist/chunks.d.ts +67 -26
  20. package/dist/chunks.d.ts.map +1 -1
  21. package/dist/chunks.js +82 -9
  22. package/dist/chunks.js.map +1 -1
  23. package/dist/clients.d.ts +23 -11
  24. package/dist/clients.d.ts.map +1 -1
  25. package/dist/clients.js +17 -18
  26. package/dist/clients.js.map +1 -1
  27. package/dist/create2.d.ts +84 -1
  28. package/dist/create2.d.ts.map +1 -1
  29. package/dist/create2.js +158 -2
  30. package/dist/create2.js.map +1 -1
  31. package/dist/creator-token.d.ts +132 -0
  32. package/dist/creator-token.d.ts.map +1 -0
  33. package/dist/creator-token.js +183 -0
  34. package/dist/creator-token.js.map +1 -0
  35. package/dist/deploy.d.ts +234 -14
  36. package/dist/deploy.d.ts.map +1 -1
  37. package/dist/deploy.js +289 -111
  38. package/dist/deploy.js.map +1 -1
  39. package/dist/deployments.d.ts +70 -5
  40. package/dist/deployments.d.ts.map +1 -1
  41. package/dist/deployments.js +139 -30
  42. package/dist/deployments.js.map +1 -1
  43. package/dist/deps.d.ts +123 -6
  44. package/dist/deps.d.ts.map +1 -1
  45. package/dist/deps.js +249 -7
  46. package/dist/deps.js.map +1 -1
  47. package/dist/env.d.ts +5 -4
  48. package/dist/env.d.ts.map +1 -1
  49. package/dist/env.js +20 -5
  50. package/dist/env.js.map +1 -1
  51. package/dist/errors.d.ts +94 -0
  52. package/dist/errors.d.ts.map +1 -0
  53. package/dist/errors.js +126 -0
  54. package/dist/errors.js.map +1 -0
  55. package/dist/execute.d.ts +118 -0
  56. package/dist/execute.d.ts.map +1 -0
  57. package/dist/execute.js +159 -0
  58. package/dist/execute.js.map +1 -0
  59. package/dist/gateways.d.ts +79 -0
  60. package/dist/gateways.d.ts.map +1 -0
  61. package/dist/gateways.js +156 -0
  62. package/dist/gateways.js.map +1 -0
  63. package/dist/generator-document.d.ts +57 -0
  64. package/dist/generator-document.d.ts.map +1 -0
  65. package/dist/generator-document.js +123 -0
  66. package/dist/generator-document.js.map +1 -0
  67. package/dist/generator.d.ts +44 -2
  68. package/dist/generator.d.ts.map +1 -1
  69. package/dist/generator.js +29 -0
  70. package/dist/generator.js.map +1 -1
  71. package/dist/index.d.ts +18 -3
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +18 -3
  74. package/dist/index.js.map +1 -1
  75. package/dist/inspect.d.ts +48 -0
  76. package/dist/inspect.d.ts.map +1 -0
  77. package/dist/inspect.js +295 -0
  78. package/dist/inspect.js.map +1 -0
  79. package/dist/migrate.d.ts +41 -0
  80. package/dist/migrate.d.ts.map +1 -0
  81. package/dist/migrate.js +142 -0
  82. package/dist/migrate.js.map +1 -0
  83. package/dist/mime.d.ts +11 -0
  84. package/dist/mime.d.ts.map +1 -0
  85. package/dist/mime.js +40 -0
  86. package/dist/mime.js.map +1 -0
  87. package/dist/node.d.ts +10 -0
  88. package/dist/node.d.ts.map +1 -0
  89. package/dist/node.js +24 -0
  90. package/dist/node.js.map +1 -0
  91. package/dist/onchain-uri.d.ts +110 -0
  92. package/dist/onchain-uri.d.ts.map +1 -0
  93. package/dist/onchain-uri.js +276 -0
  94. package/dist/onchain-uri.js.map +1 -0
  95. package/dist/ops.d.ts +507 -14
  96. package/dist/ops.d.ts.map +1 -1
  97. package/dist/ops.js +850 -34
  98. package/dist/ops.js.map +1 -1
  99. package/dist/policy.d.ts +57 -0
  100. package/dist/policy.d.ts.map +1 -0
  101. package/dist/policy.js +35 -0
  102. package/dist/policy.js.map +1 -0
  103. package/dist/probe.d.ts +58 -0
  104. package/dist/probe.d.ts.map +1 -1
  105. package/dist/probe.js +190 -9
  106. package/dist/probe.js.map +1 -1
  107. package/dist/reconstruct.d.ts +114 -2
  108. package/dist/reconstruct.d.ts.map +1 -1
  109. package/dist/reconstruct.js +443 -98
  110. package/dist/reconstruct.js.map +1 -1
  111. package/dist/resume.d.ts +96 -0
  112. package/dist/resume.d.ts.map +1 -0
  113. package/dist/resume.js +95 -0
  114. package/dist/resume.js.map +1 -0
  115. package/dist/script-chunks.d.ts +24 -0
  116. package/dist/script-chunks.d.ts.map +1 -0
  117. package/dist/script-chunks.js +63 -0
  118. package/dist/script-chunks.js.map +1 -0
  119. package/dist/service.d.ts +151 -9
  120. package/dist/service.d.ts.map +1 -1
  121. package/dist/service.js +103 -11
  122. package/dist/service.js.map +1 -1
  123. package/dist/spine.d.ts +47 -4
  124. package/dist/spine.d.ts.map +1 -1
  125. package/dist/spine.js +0 -0
  126. package/dist/spine.js.map +1 -1
  127. package/dist/staging.d.ts +139 -0
  128. package/dist/staging.d.ts.map +1 -0
  129. package/dist/staging.js +139 -0
  130. package/dist/staging.js.map +1 -0
  131. package/dist/token.d.ts +1 -1
  132. package/dist/token.js +1 -1
  133. package/dist/tokendata.d.ts +63 -3
  134. package/dist/tokendata.d.ts.map +1 -1
  135. package/dist/tokendata.js +81 -19
  136. package/dist/tokendata.js.map +1 -1
  137. package/dist/tokens.d.ts +119 -0
  138. package/dist/tokens.d.ts.map +1 -0
  139. package/dist/tokens.js +317 -0
  140. package/dist/tokens.js.map +1 -0
  141. package/dist/types.d.ts +107 -5
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/util.d.ts +74 -0
  144. package/dist/util.d.ts.map +1 -0
  145. package/dist/util.js +106 -0
  146. package/dist/util.js.map +1 -0
  147. package/package.json +15 -6
package/dist/ops.js CHANGED
@@ -1,6 +1,10 @@
1
- import { encodeFunctionData, toHex, zeroAddress } from 'viem';
2
- import { oneOfOneImageAbi, oneOfOneImageFactoryAbi, oneOfOneImageFactoryBytecode, seriesImageAbi, seriesImageFactoryAbi, seriesImageFactoryBytecode, abxMetadataRendererBytecode, abxFixedPriceMinterAbi, abxFixedPriceMinterBytecode, seriesCodeAbi, seriesCodeFactoryAbi, } from './abi/index.js';
1
+ import { encodeFunctionData, parseEventLogs, toHex, zeroAddress } from 'viem';
2
+ import { oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererBytecode, abxFixedPriceMinterAbi, abxFixedPriceMinterBytecode, abxFixedPriceMinter1155Abi, abxFixedPriceMinter1155Bytecode, abxSeedSourceAbi, abxSeedSourceBytecode, seriesCodeAbi, seriesCodeFactoryAbi, oneOfOneEditionAbi, oneOfOneEditionFactoryAbi, editionImageFactoryAbi, editionCodeFactoryAbi, } from './abi/index.js';
3
3
  import { encodeTag } from './spine.js';
4
+ import { SeedSourceUnusableError } from './errors.js';
5
+ // The two edition-image factories are library-linked, so their initcode comes from the `link*`
6
+ // helpers, never from the raw `*Bytecode` exports (which still carry solc's `__$…$__` placeholder).
7
+ import { ABX_SALT, CREATE2_PROXY, create2Calldata, linkEditionFactory, linkFactory, linkOneOfOneEditionFactory, linkSeriesFactory, } from './create2.js';
4
8
  const ZERO_VALUE = '0x0';
5
9
  function short(addr) {
6
10
  return addr.length > 12 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr;
@@ -69,6 +73,11 @@ export function prepareMulticall(args) {
69
73
  * preserved — important when a later op depends on an earlier one's effect — so a run is
70
74
  * only merged while the target stays the same. This is what turns "6 owner edits" into
71
75
  * "1 transaction" for the UX, while staying honest about what genuinely needs its own tx.
76
+ *
77
+ * A CREATE2-proxy deploy (`to === CREATE2_PROXY` — the renderer/minter/seed-source singletons)
78
+ * is creation too, even though it carries a `to`: the proxy has no `multicall`, so folding two of
79
+ * them together would silently produce a transaction that reverts on the target it's actually
80
+ * sent to. Excluded from merging for the same reason `to === null` is.
72
81
  */
73
82
  export function batchOps(ops) {
74
83
  const out = [];
@@ -81,7 +90,7 @@ export function batchOps(ops) {
81
90
  run = [];
82
91
  };
83
92
  for (const op of ops) {
84
- const mergeable = op.to !== null && op.value === ZERO_VALUE;
93
+ const mergeable = op.to !== null && op.to !== CREATE2_PROXY && op.value === ZERO_VALUE;
85
94
  if (mergeable && run.length > 0 && run[0].to === op.to) {
86
95
  run.push(op);
87
96
  }
@@ -131,6 +140,96 @@ export function prepareMint(args) {
131
140
  fields: { contract: args.contract, to: args.to, token: 'next in order' },
132
141
  };
133
142
  }
143
+ /**
144
+ * The token ids a mint actually created, decoded from its receipt's mint logs — `Transfer` from
145
+ * `0x0` on a 721 token, or `TransferSingle`/`TransferBatch` from `0x0` on an edition (each id
146
+ * reported once, regardless of how many copies it minted — an edition amount has nowhere to ride
147
+ * in a `string[]`, so it's a job for the sibling {@link mintedEditionAmounts}). Both event
148
+ * families are probed unconditionally (harmless — a contract only ever emits the one that
149
+ * matches its own standard, so exactly one probe ever finds anything) rather than branching on
150
+ * contract type, so this stays a single call for either kind.
151
+ *
152
+ * Authoritative by construction: the ids come from the transaction that minted them, so a
153
+ * `--count N` batch reports all N and a concurrent mint elsewhere cannot skew the answer. Returns
154
+ * `[]` rather than throwing if the receipt can't be read — a mint that landed on-chain must not be
155
+ * reported as failed because a follow-up read hiccuped.
156
+ */
157
+ export async function mintedTokenIds(client, contract, txHash) {
158
+ try {
159
+ const receipt = await client.getTransactionReceipt({ hash: txHash });
160
+ const logs = receipt.logs.filter((log) => log.address.toLowerCase() === contract.toLowerCase());
161
+ const ids = new Set();
162
+ const transfers = parseEventLogs({ abi: oneOfOneImageAbi, eventName: 'Transfer', logs });
163
+ for (const ev of transfers) {
164
+ const args = ev.args;
165
+ if (args.from && args.from !== zeroAddress)
166
+ continue; // a transfer, not a mint
167
+ const id = args.id ?? args.tokenId;
168
+ if (id !== undefined)
169
+ ids.add(id.toString());
170
+ }
171
+ const singles = parseEventLogs({ abi: oneOfOneEditionAbi, eventName: 'TransferSingle', logs });
172
+ for (const ev of singles) {
173
+ const args = ev.args;
174
+ if (args.from && args.from !== zeroAddress)
175
+ continue;
176
+ if (args.id !== undefined)
177
+ ids.add(args.id.toString());
178
+ }
179
+ const batches = parseEventLogs({ abi: oneOfOneEditionAbi, eventName: 'TransferBatch', logs });
180
+ for (const ev of batches) {
181
+ const args = ev.args;
182
+ if (args.from && args.from !== zeroAddress)
183
+ continue;
184
+ for (const id of args.ids ?? [])
185
+ ids.add(id.toString());
186
+ }
187
+ return [...ids].sort((a, b) => Number(BigInt(a) - BigInt(b)));
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ }
193
+ /**
194
+ * The `(id, amount)` pairs an edition mint actually created, decoded from its receipt's
195
+ * `TransferSingle`/`TransferBatch` (`from == 0x0`) logs — the amount-carrying sibling of {@link
196
+ * mintedTokenIds}, for callers that need "how many copies of #7 did this tx mint" rather than
197
+ * just "which ids." Amounts are summed per id (a batch could in principle mint the same id
198
+ * twice in one call), then sorted by id. Returns `[]` on a 721 receipt (no matching events) or
199
+ * any read failure, same fail-open contract as {@link mintedTokenIds}.
200
+ */
201
+ export async function mintedEditionAmounts(client, contract, txHash) {
202
+ try {
203
+ const receipt = await client.getTransactionReceipt({ hash: txHash });
204
+ const logs = receipt.logs.filter((log) => log.address.toLowerCase() === contract.toLowerCase());
205
+ const amounts = new Map();
206
+ const add = (id, amount) => amounts.set(id, (amounts.get(id) ?? 0n) + amount);
207
+ const singles = parseEventLogs({ abi: oneOfOneEditionAbi, eventName: 'TransferSingle', logs });
208
+ for (const ev of singles) {
209
+ const args = ev.args;
210
+ if (args.from && args.from !== zeroAddress)
211
+ continue;
212
+ if (args.id !== undefined)
213
+ add(args.id.toString(), args.amount ?? 0n);
214
+ }
215
+ const batches = parseEventLogs({ abi: oneOfOneEditionAbi, eventName: 'TransferBatch', logs });
216
+ for (const ev of batches) {
217
+ const args = ev.args;
218
+ if (args.from && args.from !== zeroAddress)
219
+ continue;
220
+ const batchIds = args.ids ?? [];
221
+ const batchAmounts = args.amounts ?? [];
222
+ for (let i = 0; i < batchIds.length; i++)
223
+ add(batchIds[i].toString(), batchAmounts[i] ?? 0n);
224
+ }
225
+ return [...amounts.entries()]
226
+ .map(([tokenId, amount]) => ({ tokenId, amount: amount.toString() }))
227
+ .sort((a, b) => Number(BigInt(a.tokenId) - BigInt(b.tokenId)));
228
+ }
229
+ catch {
230
+ return [];
231
+ }
232
+ }
134
233
  /** Re-point a token's resolver BASE (move the resolver). The per-token pointer is derived
135
234
  * on-chain as `{base}/{chainId}/{address}/{tokenId}`. Affects every token without an
136
235
  * override. Signer must be the owner. */
@@ -254,9 +353,28 @@ export function prepareLockContractURI(args) {
254
353
  };
255
354
  }
256
355
  /** Set the contract-wide default royalty. Signer must be the owner. */
356
+ /**
357
+ * The on-chain royalty ceiling, mirrored from `RoyaltyExtension._maxRoyaltyBps()` (1000 bps = 10%).
358
+ *
359
+ * Client-side validation used to allow the full 0–10000 ERC-2981 range, and the CLI's own error text
360
+ * offered `10000 = 100%` as an example — so a creator asking for an ordinary 15% got a fully built,
361
+ * fully signed transaction that reverted `RoyaltyTooHigh()` on chain. On the wallet lane they approved
362
+ * it in their own wallet first and paid the gas to find out. Validate against the real cap so the
363
+ * refusal is instant, local and free.
364
+ *
365
+ * The royalty ceiling is now **per-collection**: owner-set at deploy (0–10000 bps) and reduce-only
366
+ * after. So this constant is the ABSOLUTE protocol maximum (100%); a collection's own, possibly
367
+ * lower, `maxRoyaltyBps()` is the binding cap and is enforced on chain. To refuse an over-cap
368
+ * `set-royalty` instantly rather than after a revert, pass `maxBps` (the collection's live cap, read
369
+ * from `maxRoyaltyBps()`); it defaults to the absolute max when omitted.
370
+ */
371
+ export const MAX_ROYALTY_BPS = 10_000;
257
372
  export function prepareSetRoyalty(args) {
258
- if (args.bps < 0 || args.bps > 10000)
259
- throw new Error(`royalty bps out of range (0–10000): ${args.bps}`);
373
+ const cap = args.maxBps ?? MAX_ROYALTY_BPS;
374
+ if (args.bps < 0 || args.bps > cap)
375
+ throw new Error(`royalty bps out of range (0–${cap}): ${args.bps}. This collection's royalty cap is ` +
376
+ `${cap / 100}% (RoyaltyExtension.maxRoyaltyBps, owner-set at deploy, reduce-only) — a higher ` +
377
+ `value REVERTS with RoyaltyTooHigh().`);
260
378
  return {
261
379
  op: 'set-royalty',
262
380
  to: args.contract,
@@ -271,6 +389,30 @@ export function prepareSetRoyalty(args) {
271
389
  fields: { contract: args.contract, receiver: args.receiver, bps: String(args.bps) },
272
390
  };
273
391
  }
392
+ /**
393
+ * Lower a collection's royalty ceiling (`RoyaltyExtension.reduceMaxRoyaltyBps`). Owner-only and
394
+ * **reduce-only**: the chain reverts `RoyaltyCapNotReduced()` if `newMaxBps` is not strictly below
395
+ * the current cap, and `RoyaltyCapBelowRoyalty()` if it would sit under the live royalty rate. This
396
+ * builder is domain-only (0–100%); the CLI reads the live cap + rate to refuse both cases before
397
+ * signing so the failure is local and free rather than a paid revert. Emits `MaxRoyaltyBpsUpdated`.
398
+ */
399
+ export function prepareReduceMaxRoyaltyBps(args) {
400
+ if (args.newMaxBps < 0 || args.newMaxBps > MAX_ROYALTY_BPS)
401
+ throw new Error(`royalty cap out of range (0\u2013${MAX_ROYALTY_BPS}): ${args.newMaxBps}`);
402
+ return {
403
+ op: 'set-royalty-cap',
404
+ to: args.contract,
405
+ data: encodeFunctionData({
406
+ abi: oneOfOneImageAbi,
407
+ functionName: 'reduceMaxRoyaltyBps',
408
+ args: [args.newMaxBps],
409
+ }),
410
+ value: ZERO_VALUE,
411
+ chainId: args.chainId,
412
+ summary: `Reduce royalty cap \u2192 ${(args.newMaxBps / 100).toFixed(2)}% (reduce-only)`,
413
+ fields: { contract: args.contract, maxBps: String(args.newMaxBps) },
414
+ };
415
+ }
274
416
  /**
275
417
  * Set/replace an on-chain metadata field for a token. `field` is what (e.g. "image",
276
418
  * "description"), `representation` is how it's carried (e.g. "inline", "keccak256",
@@ -374,27 +516,35 @@ export function prepareDeployOneOfOne(args) {
374
516
  },
375
517
  };
376
518
  }
377
- /** The unsigned form of the factory (trust-anchor) deploy — a contract creation. */
519
+ /** The unsigned form of the factory (trust-anchor) deploy — CREATE2 via the keyless proxy at the
520
+ * canonical salt, matching the forge `Deploy` script and `predictFactory()` (create2.ts). A trust
521
+ * anchor is the one address platforms allowlist, so it has to be the same on every chain and
522
+ * computable before the first tx; a plain creation (`to: null`) would make it depend on the
523
+ * deployer's nonce and could never match the recorded manifest. */
378
524
  export function prepareDeployFactory(args) {
379
525
  return {
380
526
  op: 'deploy-factory',
381
- to: null,
382
- data: oneOfOneImageFactoryBytecode,
527
+ to: CREATE2_PROXY,
528
+ data: create2Calldata(ABX_SALT.oneOfOneFactory, linkFactory()),
383
529
  value: ZERO_VALUE,
384
530
  chainId: args.chainId,
385
531
  summary: 'Deploy the canonical clone factory (trust anchor)',
386
532
  fields: {},
387
533
  };
388
534
  }
389
- /** The unsigned form of the canonical metadata-renderer deploy — a contract creation. */
535
+ /** The unsigned form of the canonical metadata-renderer deploy — CREATE2 via the keyless proxy +
536
+ * canonical salt, so it lands at the SAME (predictable) address on every chain, matching the forge
537
+ * `DeployRenderer` script and `predictRenderer()` (create2.ts). NOT a plain contract creation
538
+ * (`to: null`) — that would land at a nonce-dependent address, defeating the whole point of a
539
+ * canonical, cross-chain-identical singleton. */
390
540
  export function prepareDeployRenderer(args) {
391
541
  return {
392
542
  op: 'deploy-renderer',
393
- to: null,
394
- data: abxMetadataRendererBytecode,
543
+ to: CREATE2_PROXY,
544
+ data: create2Calldata(ABX_SALT.renderer, abxMetadataRendererBytecode),
395
545
  value: ZERO_VALUE,
396
546
  chainId: args.chainId,
397
- summary: 'Deploy the canonical on-chain metadata renderer',
547
+ summary: 'Deploy the canonical on-chain metadata renderer (CREATE2, deterministic address)',
398
548
  fields: {},
399
549
  };
400
550
  }
@@ -510,21 +660,218 @@ export function prepareDeploySeries(args) {
510
660
  },
511
661
  };
512
662
  }
513
- /** The unsigned form of the Series factory (trust-anchor) deploy — a contract creation. */
663
+ /** The unsigned form of the Series factory (trust-anchor) deploy — CREATE2 via the keyless proxy at
664
+ * the canonical salt, matching the forge `DeploySeries` script and `predictSeriesFactory()`. Same
665
+ * reasoning as `prepareDeployFactory` above. */
514
666
  export function prepareDeploySeriesFactory(args) {
515
667
  return {
516
668
  op: 'deploy-series-factory',
517
- to: null,
518
- data: seriesImageFactoryBytecode,
669
+ to: CREATE2_PROXY,
670
+ data: create2Calldata(ABX_SALT.seriesFactory, linkSeriesFactory()),
519
671
  value: ZERO_VALUE,
520
672
  chainId: args.chainId,
521
673
  summary: 'Deploy the canonical Series clone factory (trust anchor)',
522
674
  fields: {},
523
675
  };
524
676
  }
677
+ // ── ERC-1155 editions (IAbxEditionMint + Edition Supply) ──────────────────────
678
+ // Ids are caller-named, not a sequential cursor — a minter/owner targets any id, in any order,
679
+ // with a per-mint amount. `mint`'s signature is uniform across all three edition kinds
680
+ // (`OneOfOneEdition` reverts unless `id === 0`, its id space fixed to the single work), so
681
+ // every op below encodes against `oneOfOneEditionAbi` — the narrowest-common-superset ABI for
682
+ // the family, exactly like `oneOfOneImageAbi` plays that role on the 721 side.
683
+ /**
684
+ * Mint `amount` copies of `id` to `to` (`IAbxEditionMint.mint`). Owner always; the authorized
685
+ * minter only while unpaused — same auth shape as {@link prepareMint}/{@link prepareSeriesMint}.
686
+ * The edition twin of both: there is no separate "next in order" primitive here, since an
687
+ * edition mint always names its id.
688
+ */
689
+ export function prepareEditionMint(args) {
690
+ const id = BigInt(args.tokenId);
691
+ const amount = BigInt(args.amount);
692
+ return {
693
+ op: 'mint',
694
+ to: args.contract,
695
+ data: encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'mint', args: [args.to, id, amount] }),
696
+ value: ZERO_VALUE,
697
+ chainId: args.chainId,
698
+ summary: `Mint ${amount} cop${amount === 1n ? 'y' : 'ies'} of #${id} → ${short(args.to)}`,
699
+ fields: { contract: args.contract, to: args.to, tokenId: id.toString(), amount: amount.toString() },
700
+ };
701
+ }
702
+ /** Transfer `amount` copies of `id` to a new holder (`safeTransferFrom(from,to,id,amount,"")` on
703
+ * the ERC-1155 ABI) — the edition twin of {@link prepareTransfer}. Signer must be `from` (or an
704
+ * operator it approved via the standard `setApprovalForAll`). */
705
+ export function prepareEditionTransfer(args) {
706
+ const id = BigInt(args.tokenId);
707
+ const amount = BigInt(args.amount);
708
+ return {
709
+ op: 'transfer',
710
+ to: args.contract,
711
+ data: encodeFunctionData({
712
+ abi: oneOfOneEditionAbi,
713
+ functionName: 'safeTransferFrom',
714
+ args: [args.from, args.to, id, amount, '0x'],
715
+ }),
716
+ value: ZERO_VALUE,
717
+ chainId: args.chainId,
718
+ summary: `Transfer ${amount} cop${amount === 1n ? 'y' : 'ies'} of #${id} → ${short(args.to)}`,
719
+ fields: { contract: args.contract, from: args.from, to: args.to, tokenId: id.toString(), amount: amount.toString() },
720
+ };
721
+ }
722
+ /**
723
+ * Owner overrides `id`'s supply cap (Edition Supply extension) — the edition twin of {@link
724
+ * prepareSetMaxInvocations}, one id finer. Monotonically non-increasing once used for this id,
725
+ * never below its live `totalSupply(id)` (see `IAbxEditionSupply`'s "0 = open until overridden"
726
+ * semantics — an explicit `0` permanently closes the id, it does not mean "reopen to uncapped").
727
+ * Signer must be the owner.
728
+ */
729
+ export function prepareSetMaxSupply(args) {
730
+ const id = BigInt(args.tokenId);
731
+ const cap = BigInt(args.cap);
732
+ return {
733
+ op: 'set-max-supply',
734
+ to: args.contract,
735
+ data: encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'setMaxSupply', args: [id, cap] }),
736
+ value: ZERO_VALUE,
737
+ chainId: args.chainId,
738
+ summary: `Set #${id}'s supply cap → ${cap}`,
739
+ fields: { contract: args.contract, tokenId: id.toString(), cap: cap.toString() },
740
+ };
741
+ }
742
+ /**
743
+ * Permissionless, caller-chunked re-emission of the native `URI` event for the given ids —
744
+ * called after a contract-wide re-point (`setTokenURIBase`/`setTokenURIRenderer`, which emit
745
+ * only their own Register-2 config event; see `Uri1155`'s class-level dev note on why there's no
746
+ * automatic per-id loop for that case) to make marketplaces/indexers that only honor the native
747
+ * event re-index. Anyone may call it — it only re-emits already-public, current truth, so there
748
+ * is no state to protect; the caller picks the batch size.
749
+ */
750
+ export function preparePingURI(args) {
751
+ const ids = args.tokenIds.map((id) => BigInt(id));
752
+ return {
753
+ op: 'ping-uri',
754
+ to: args.contract,
755
+ data: encodeFunctionData({ abi: oneOfOneEditionAbi, functionName: 'pingURI', args: [ids] }),
756
+ value: ZERO_VALUE,
757
+ chainId: args.chainId,
758
+ summary: `Re-emit URI for ${ids.length} id${ids.length === 1 ? '' : 's'}`,
759
+ fields: { contract: args.contract, tokenIds: ids.join(',') },
760
+ };
761
+ }
762
+ /** The unsigned form of a 1/1-edition genesis deploy — `deployDeterministic(params, salt)` on
763
+ * the {@link oneOfOneEditionFactoryAbi} factory. The edition twin of {@link
764
+ * prepareDeployOneOfOne}. */
765
+ export function prepareDeployOneOfOneEdition(args) {
766
+ return {
767
+ op: 'deploy-one-of-one-edition',
768
+ to: args.factory,
769
+ data: encodeFunctionData({
770
+ abi: oneOfOneEditionFactoryAbi,
771
+ functionName: 'deployDeterministic',
772
+ args: [args.params, args.salt],
773
+ }),
774
+ value: ZERO_VALUE,
775
+ chainId: args.chainId,
776
+ summary: `Deploy edition "${args.params.name}" → ${args.clone}`,
777
+ fields: {
778
+ factory: args.factory,
779
+ clone: args.clone,
780
+ name: args.params.name,
781
+ symbol: args.params.symbol,
782
+ owner: args.params.owner,
783
+ },
784
+ };
785
+ }
786
+ /** The unsigned form of the 1/1-edition factory (trust-anchor) deploy — CREATE2 via the keyless
787
+ * proxy + canonical salt, so it lands at the SAME (predictable) address on every chain, matching
788
+ * `predictOneOfOneEditionFactory()` (create2.ts). See {@link prepareDeployRenderer} for why this
789
+ * is `to: CREATE2_PROXY`, not a plain creation.
790
+ *
791
+ * The initcode goes out LINKED against `AbxEditionLib` (`linkOneOfOneEditionFactory()`): the
792
+ * embedded `OneOfOneEdition` implementation delegates its uri / creator-token / edition-supply
793
+ * bodies into that library, so the shipped bytecode carries a solc `__$…$__` placeholder. That
794
+ * library must already be on-chain at `predictEditionLib()` for the deployed factory to work —
795
+ * {@link deployOneOfOneEditionFactory} (deploy.ts) is the path that guarantees it. */
796
+ export function prepareDeployOneOfOneEditionFactory(args) {
797
+ return {
798
+ op: 'deploy-one-of-one-edition-factory',
799
+ to: CREATE2_PROXY,
800
+ data: create2Calldata(ABX_SALT.oneOfOneEditionFactory, linkOneOfOneEditionFactory()),
801
+ value: ZERO_VALUE,
802
+ chainId: args.chainId,
803
+ summary: 'Deploy the canonical 1/1-edition clone factory (trust anchor), linked against AbxEditionLib — CREATE2, deterministic address',
804
+ fields: {},
805
+ };
806
+ }
807
+ /** The unsigned form of an edition-image genesis deploy — `deployDeterministic(params, salt)` on
808
+ * the {@link editionImageFactoryAbi} factory. The edition twin of {@link prepareDeploySeries}. */
809
+ export function prepareDeployEditionImage(args) {
810
+ return {
811
+ op: 'deploy-edition-image',
812
+ to: args.factory,
813
+ data: encodeFunctionData({
814
+ abi: editionImageFactoryAbi,
815
+ functionName: 'deployDeterministic',
816
+ args: [args.params, args.salt],
817
+ }),
818
+ value: ZERO_VALUE,
819
+ chainId: args.chainId,
820
+ summary: `Deploy edition "${args.params.name}" (${String(args.params.maxInvocations)} max) → ${args.clone}`,
821
+ fields: {
822
+ factory: args.factory,
823
+ clone: args.clone,
824
+ name: args.params.name,
825
+ symbol: args.params.symbol,
826
+ owner: args.params.owner,
827
+ maxInvocations: String(args.params.maxInvocations),
828
+ },
829
+ };
830
+ }
831
+ /** The unsigned form of the edition-image factory (trust-anchor) deploy — CREATE2 via the
832
+ * keyless proxy + canonical salt, matching `predictEditionFactory()` (create2.ts). See {@link
833
+ * prepareDeployRenderer} for why this is `to: CREATE2_PROXY`, not a plain creation, and {@link
834
+ * prepareDeployOneOfOneEditionFactory} for why the initcode is linked against `AbxEditionLib`
835
+ * (`EditionImage` delegates the same three mixins' bodies into it). */
836
+ export function prepareDeployEditionFactory(args) {
837
+ return {
838
+ op: 'deploy-edition-factory',
839
+ to: CREATE2_PROXY,
840
+ data: create2Calldata(ABX_SALT.editionFactory, linkEditionFactory()),
841
+ value: ZERO_VALUE,
842
+ chainId: args.chainId,
843
+ summary: 'Deploy the canonical multi-work edition clone factory (trust anchor), linked against AbxEditionLib — CREATE2, deterministic address',
844
+ fields: {},
845
+ };
846
+ }
847
+ /** Deterministic deploy of an {EditionCode} clone — the code-project edition factory op. The
848
+ * edition twin of {@link prepareDeploySeriesCode}. */
849
+ export function prepareDeployEditionCode(args) {
850
+ return {
851
+ op: 'deploy-edition-code',
852
+ to: args.factory,
853
+ data: encodeFunctionData({
854
+ abi: editionCodeFactoryAbi,
855
+ functionName: 'deployDeterministic',
856
+ args: [args.params, args.salt],
857
+ }),
858
+ value: ZERO_VALUE,
859
+ chainId: args.chainId,
860
+ summary: `Deploy code-project edition "${args.params.name}" (${String(args.params.maxInvocations)} max) → ${args.clone}`,
861
+ fields: {
862
+ factory: args.factory,
863
+ clone: args.clone,
864
+ name: args.params.name,
865
+ symbol: args.params.symbol,
866
+ owner: args.params.owner,
867
+ seedSource: args.params.seedSource,
868
+ maxInvocations: String(args.params.maxInvocations),
869
+ },
870
+ };
871
+ }
525
872
  // ── Fixed-price minter (the Minter spine) ─────────────────────────────────────
526
873
  // The minter is a shared, ownerless, multi-tenant singleton. `configure` defers to the
527
- // token owner; `purchase`/`purchaseTo` are public. See specs/protocol/minter-spine.md.
874
+ // token owner; `purchase`/`purchaseTo` are public. See site/content/docs/protocol/minting.mdx.
528
875
  /**
529
876
  * Set/update a project's fixed-price sale on the shared minter. Signer must be the ABX
530
877
  * token's owner. `paymentToken == 0x0` prices the sale in ETH; otherwise it's that ERC-20.
@@ -559,39 +906,333 @@ export function prepareConfigureSale(args) {
559
906
  }
560
907
  /**
561
908
  * Buy one token from the shared minter. `to` omitted → minted to the signer (`purchase`);
562
- * `to` set → minted to that address (`purchaseTo`). `value` is the ETH to attach (equal to
563
- * the sale price for an ETH sale; `0` for an ERC-20 sale, where the buyer must have approved
564
- * the minter to pull `price` beforehand). Public — no owner rights needed.
909
+ * `to` set → minted to that address (`purchaseTo`). Public — no owner rights needed.
910
+ *
911
+ * `sale` is the live terms ({@link readSaleConfig}), and they are **mandatory**: the minter takes
912
+ * `(expectedPaymentToken, maxPrice)` and reverts `SaleTermsChanged` if the sale has moved, which is
913
+ * what stops a project owner from front-running a pending purchase and spending the buyer's whole
914
+ * ERC-20 allowance (`configure` has no timelock). The ETH to attach is derived from those terms —
915
+ * `price` for an ETH sale, `0` for an ERC-20 one (where the buyer must have approved the minter to
916
+ * pull `price` beforehand) — so a caller can't desync the payment from the guard.
917
+ *
918
+ * `maxPrice` defaults to `sale.price` (accept exactly what was read). Pass it to state a wider
919
+ * ceiling. It bounds the guard only; the ETH attached still comes from `sale.price`, since the minter
920
+ * wants exact payment. There is deliberately no "no maximum" sentinel.
921
+ *
922
+ * **A wide `maxPrice` is how a caller opts out of the protection.** The minter refuses a sentinel
923
+ * precisely so that a bound has to be stated, but nothing stops a caller from stating an enormous
924
+ * one — and on the ERC-20 lane that hands back exactly the vector the guard closes: the minter pulls
925
+ * the live price from the buyer's standing allowance, so a ceiling of `2^256-1` lets a re-priced sale
926
+ * take the whole allowance. Omitting the argument is the safe path and is what every caller in this
927
+ * repo does. If you widen it, widen it by an amount you would be content to lose, and never derive it
928
+ * from a price you read in the same breath as sending — that is the same thing as no bound at all.
929
+ *
930
+ * **On the ETH lane, `maxPrice` is a terms ASSERTION, not slippage tolerance — do not sell it as
931
+ * one.** The minter requires `msg.value == price` (an equality) and V1 has no refund path, so an
932
+ * in-flight ETH purchase reverts `WrongPayment` if the price moves in *either* direction, however
933
+ * wide the ceiling. Raising `maxPrice` on an ETH sale buys the buyer nothing at all. Real tolerance
934
+ * exists only on the **ERC-20** lane, where the minter pulls the live `price` from the buyer's
935
+ * allowance and any price at or below the ceiling settles. Re-pricing a live ETH sale therefore fails
936
+ * every buy already in flight, by design — pause first for a clean cutover.
565
937
  */
566
938
  export function preparePurchase(args) {
567
- const value = BigInt(args.value ?? 0n);
939
+ const paymentToken = args.sale.paymentToken;
940
+ const price = BigInt(args.sale.price);
941
+ const isEth = paymentToken === zeroAddress;
942
+ const maxPrice = args.maxPrice === undefined ? price : BigInt(args.maxPrice);
943
+ const value = isEth ? price : 0n; // exact payment; an ERC-20 sale attaches none
568
944
  const toRecipient = args.to && args.to !== zeroAddress;
945
+ const cost = isEth ? `${price} wei` : `${price} units of ${short(paymentToken)}`;
569
946
  return {
570
947
  op: 'purchase',
571
948
  to: args.minter,
572
949
  data: toRecipient
573
- ? encodeFunctionData({ abi: abxFixedPriceMinterAbi, functionName: 'purchaseTo', args: [args.token, args.to] })
574
- : encodeFunctionData({ abi: abxFixedPriceMinterAbi, functionName: 'purchase', args: [args.token] }),
950
+ ? encodeFunctionData({
951
+ abi: abxFixedPriceMinterAbi,
952
+ functionName: 'purchaseTo',
953
+ args: [args.token, args.to, paymentToken, maxPrice],
954
+ })
955
+ : encodeFunctionData({
956
+ abi: abxFixedPriceMinterAbi,
957
+ functionName: 'purchase',
958
+ args: [args.token, paymentToken, maxPrice],
959
+ }),
575
960
  value: value > 0n ? toHex(value) : ZERO_VALUE,
576
961
  chainId: args.chainId,
577
- summary: toRecipient
578
- ? `Buy 1 token → ${short(args.to)}${value > 0n ? ` for ${value} wei` : ''}`
579
- : `Buy 1 token${value > 0n ? ` for ${value} wei` : ''}`,
580
- fields: { minter: args.minter, token: args.token, to: args.to ?? '(sender)', value: value.toString() },
962
+ summary: toRecipient ? `Buy 1 token → ${short(args.to)} for ${cost}` : `Buy 1 token for ${cost}`,
963
+ fields: {
964
+ minter: args.minter,
965
+ token: args.token,
966
+ to: args.to ?? '(sender)',
967
+ value: value.toString(),
968
+ expectedPaymentToken: paymentToken,
969
+ maxPrice: maxPrice.toString(),
970
+ },
581
971
  };
582
972
  }
583
- /** The unsigned form of the shared fixed-price minter deploy — a contract creation. */
973
+ export async function readSaleConfig(client, minter, token) {
974
+ const [configured, paymentToken, price, allocation, sold] = (await client.readContract({
975
+ address: minter,
976
+ abi: abxFixedPriceMinterAbi,
977
+ functionName: 'sales',
978
+ args: [token],
979
+ }));
980
+ return { configured, paymentToken, price, allocation, sold };
981
+ }
982
+ /** The unsigned form of the shared fixed-price minter deploy — CREATE2 via the keyless proxy +
983
+ * canonical salt (same address on every chain), matching `predictFixedPriceMinter()`. See
984
+ * {@link prepareDeployRenderer} for why this is `to: CREATE2_PROXY`, not a plain creation. */
584
985
  export function prepareDeployFixedPriceMinter(args) {
585
986
  return {
586
987
  op: 'deploy-fixed-price-minter',
587
- to: null,
588
- data: abxFixedPriceMinterBytecode,
988
+ to: CREATE2_PROXY,
989
+ data: create2Calldata(ABX_SALT.fixedPriceMinter, abxFixedPriceMinterBytecode),
589
990
  value: ZERO_VALUE,
590
991
  chainId: args.chainId,
591
- summary: 'Deploy the canonical fixed-price minter (shared, ownerless)',
992
+ summary: 'Deploy the canonical fixed-price minter (shared, ownerless) — CREATE2, deterministic address',
592
993
  fields: {},
593
994
  };
594
995
  }
996
+ // ── Fixed-price EDITION minter (the Minter spine's 1155 lane) ────────────────
997
+ // The structural sibling of the block above, keyed one step finer: `(token, id)` instead of just
998
+ // `token`, since an edition project prices each work on its own terms. See
999
+ // `AbxFixedPriceMinter1155`'s own doc comment for the two-grant model (mint rights via
1000
+ // `setMinter`, sale terms via `configure` — unchanged from the 721 lane, one id finer).
1001
+ /**
1002
+ * Set/update a project's fixed-price sale for one id on the shared edition minter. The id-keyed
1003
+ * twin of {@link prepareConfigureSale}. Signer must be the ABX token's owner.
1004
+ */
1005
+ export function prepareConfigureSale1155(args) {
1006
+ const id = BigInt(args.tokenId);
1007
+ const paymentToken = args.paymentToken ?? zeroAddress;
1008
+ const price = BigInt(args.price);
1009
+ const allocation = BigInt(args.allocation);
1010
+ const isEth = paymentToken === zeroAddress;
1011
+ return {
1012
+ op: 'configure-sale',
1013
+ to: args.minter,
1014
+ data: encodeFunctionData({
1015
+ abi: abxFixedPriceMinter1155Abi,
1016
+ functionName: 'configure',
1017
+ args: [args.token, id, paymentToken, price, allocation],
1018
+ }),
1019
+ value: ZERO_VALUE,
1020
+ chainId: args.chainId,
1021
+ summary: `Configure sale for #${id}: ${price} ${isEth ? 'wei' : `of ${short(paymentToken)}`}/copy, allocation ${allocation}`,
1022
+ fields: {
1023
+ minter: args.minter,
1024
+ token: args.token,
1025
+ tokenId: id.toString(),
1026
+ paymentToken,
1027
+ price: price.toString(),
1028
+ allocation: allocation.toString(),
1029
+ },
1030
+ };
1031
+ }
1032
+ /**
1033
+ * Buy `quantity` copies of `id` from the shared edition minter. The id/quantity-keyed twin of
1034
+ * {@link preparePurchase}. `to` omitted → minted to the signer (`purchase`); `to` set → minted to
1035
+ * that address (`purchaseTo`). Public — no owner rights needed.
1036
+ *
1037
+ * `sale` is the live terms ({@link readSaleConfig1155}) and is mandatory, same terms guard as the 721
1038
+ * lane — except the minter bounds the **total** (`price × quantity`), which is what actually leaves
1039
+ * the buyer's balance. `price × quantity` is computed here (once), for both the attached ETH and the
1040
+ * bound; `maxTotalPrice` defaults to it, and passing it states a wider ceiling.
1041
+ *
1042
+ * The ETH-lane caveat on {@link preparePurchase} applies here identically: `msg.value` must equal the
1043
+ * total exactly, so `maxTotalPrice` is an assertion about the terms, **not** slippage tolerance. Only
1044
+ * an ERC-20 edition sale gets real tolerance.
1045
+ */
1046
+ export function preparePurchase1155(args) {
1047
+ const id = BigInt(args.tokenId);
1048
+ const qty = BigInt(args.quantity);
1049
+ const paymentToken = args.sale.paymentToken;
1050
+ const total = BigInt(args.sale.price) * qty;
1051
+ const isEth = paymentToken === zeroAddress;
1052
+ const maxTotalPrice = args.maxTotalPrice === undefined ? total : BigInt(args.maxTotalPrice);
1053
+ const value = isEth ? total : 0n; // exact payment; an ERC-20 sale attaches none
1054
+ const toRecipient = args.to && args.to !== zeroAddress;
1055
+ const cost = isEth ? `${total} wei` : `${total} units of ${short(paymentToken)}`;
1056
+ const copies = `${qty} cop${qty === 1n ? 'y' : 'ies'} of #${id}`;
1057
+ return {
1058
+ op: 'purchase',
1059
+ to: args.minter,
1060
+ data: toRecipient
1061
+ ? encodeFunctionData({
1062
+ abi: abxFixedPriceMinter1155Abi,
1063
+ functionName: 'purchaseTo',
1064
+ args: [args.token, id, qty, args.to, paymentToken, maxTotalPrice],
1065
+ })
1066
+ : encodeFunctionData({
1067
+ abi: abxFixedPriceMinter1155Abi,
1068
+ functionName: 'purchase',
1069
+ args: [args.token, id, qty, paymentToken, maxTotalPrice],
1070
+ }),
1071
+ value: value > 0n ? toHex(value) : ZERO_VALUE,
1072
+ chainId: args.chainId,
1073
+ summary: toRecipient ? `Buy ${copies} → ${short(args.to)} for ${cost}` : `Buy ${copies} for ${cost}`,
1074
+ fields: {
1075
+ minter: args.minter,
1076
+ token: args.token,
1077
+ tokenId: id.toString(),
1078
+ quantity: qty.toString(),
1079
+ to: args.to ?? '(sender)',
1080
+ value: value.toString(),
1081
+ expectedPaymentToken: paymentToken,
1082
+ maxTotalPrice: maxTotalPrice.toString(),
1083
+ },
1084
+ };
1085
+ }
1086
+ /** The id-keyed twin of {@link readSaleConfig}. */
1087
+ export async function readSaleConfig1155(client, minter, token, tokenId) {
1088
+ const [configured, paymentToken, price, allocation, sold] = (await client.readContract({
1089
+ address: minter,
1090
+ abi: abxFixedPriceMinter1155Abi,
1091
+ functionName: 'sales',
1092
+ args: [token, BigInt(tokenId)],
1093
+ }));
1094
+ return { configured, paymentToken, price, allocation, sold };
1095
+ }
1096
+ /** The unsigned form of the shared edition minter deploy — CREATE2 via the keyless proxy +
1097
+ * canonical salt (same address on every chain), matching `predictFixedPriceMinter1155()`. See
1098
+ * {@link prepareDeployRenderer} for why this is `to: CREATE2_PROXY`, not a plain creation. */
1099
+ export function prepareDeployFixedPriceMinter1155(args) {
1100
+ return {
1101
+ op: 'deploy-fixed-price-minter-1155',
1102
+ to: CREATE2_PROXY,
1103
+ data: create2Calldata(ABX_SALT.fixedPriceMinter1155, abxFixedPriceMinter1155Bytecode),
1104
+ value: ZERO_VALUE,
1105
+ chainId: args.chainId,
1106
+ summary: 'Deploy the canonical fixed-price edition minter (shared, ownerless) — CREATE2, deterministic address',
1107
+ fields: {},
1108
+ };
1109
+ }
1110
+ /** The unsigned form of the canonical seed source deploy — CREATE2 via the keyless proxy +
1111
+ * canonical salt (same address on every chain), matching `predictSeedSource()`. See
1112
+ * {@link prepareDeployRenderer} for why this is `to: CREATE2_PROXY`, not a plain creation. */
1113
+ export function prepareDeploySeedSource(args) {
1114
+ return {
1115
+ op: 'deploy-seed-source',
1116
+ to: CREATE2_PROXY,
1117
+ data: create2Calldata(ABX_SALT.seedSource, abxSeedSourceBytecode),
1118
+ value: ZERO_VALUE,
1119
+ chainId: args.chainId,
1120
+ summary: 'Deploy the canonical pseudorandom seed source (CREATE2, deterministic address)',
1121
+ fields: {},
1122
+ };
1123
+ }
1124
+ // ── the seed source (Seed Source extension, Register 2) ───────────────────────
1125
+ // Mint-time randomness is CONFIGURATION, not hard-coded token logic: the token holds one address
1126
+ // and calls `IAbxSeedSource.seed(tokenId, to)` on it at mint. The canonical `AbxSeedSource` is
1127
+ // deliberately pseudorandom (replayable after the fact, computable *during* the mint tx), so the
1128
+ // documented escape for anything lottery-like is to point this at a commit-reveal / VRF-backed
1129
+ // source of the project's own. That promise is only real if a creator can actually SET the address
1130
+ // — which is what the two ops below, plus the probe, exist for.
1131
+ /**
1132
+ * Set (or clear, with `0x0`) the seed source a code project draws mint seeds from. Signer must be
1133
+ * the owner. **Future mints only** — a seed is settled once assigned, so re-pointing never rewrites
1134
+ * a token that already has one.
1135
+ *
1136
+ * Pass an address that {@link probeSeedSource} says is usable. The chain does NOT validate the
1137
+ * source (`SeedSourceExtension.setSeedSource` stores whatever it's handed), and the failure mode of
1138
+ * a bad one is the worst kind: `seedSource()` reads back exactly what you set, the `SeedSourceSet`
1139
+ * event fires, everything looks configured — and then every mint reverts in the ABI decode of the
1140
+ * source's (empty or short) return. Callers should probe first and refuse before gas.
1141
+ */
1142
+ export function prepareSetSeedSource(args) {
1143
+ const clearing = args.seedSource === zeroAddress;
1144
+ return {
1145
+ op: 'set-seed-source',
1146
+ to: args.contract,
1147
+ data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setSeedSource', args: [args.seedSource] }),
1148
+ value: ZERO_VALUE,
1149
+ chainId: args.chainId,
1150
+ summary: clearing
1151
+ ? 'Clear the seed source (future mints draw no seed; assigned seeds stay settled)'
1152
+ : `Set seed source → ${short(args.seedSource)} (future mints only)`,
1153
+ fields: { contract: args.contract, seedSource: args.seedSource },
1154
+ };
1155
+ }
1156
+ /**
1157
+ * The seed source a token currently draws from — `zeroAddress` for "no mint-time seed", and
1158
+ * `undefined` when the getter isn't there at all (a 1/1 or an image Series never composed the
1159
+ * extension). Those two are NOT the same fact and must not be collapsed: one is a code project
1160
+ * that opted out, the other is a token that has no such knob to set.
1161
+ */
1162
+ export async function readSeedSource(client, contract) {
1163
+ return (await client
1164
+ .readContract({ address: contract, abi: seriesCodeAbi, functionName: 'seedSource' })
1165
+ .catch(() => undefined));
1166
+ }
1167
+ /**
1168
+ * Ask a candidate seed source the exact question the token will ask it, and report whether it
1169
+ * answers with 32 bytes.
1170
+ *
1171
+ * **`code.length > 0` is not enough**, which is the whole reason this exists. Three separate shapes
1172
+ * pass a has-code check, read back correctly from `seedSource()`, emit `SeedSourceSet`, and then
1173
+ * revert **every mint of the collection**: no code, a permissive fallback that returns empty (a
1174
+ * Safe, an uninitialised proxy, a 7702-delegated EOA — the same footgun the transfer-validator
1175
+ * probe in `CreatorToken.sol` was written for), and anything answering with fewer than 32 bytes.
1176
+ * A misconfigured seed source is silent until the first buyer, so the check belongs at configure
1177
+ * time, loudly.
1178
+ *
1179
+ * `IAbxSeedSource.seed` is **non-`view`** (sources may keep state — commit-reveal, oracle-fed), but
1180
+ * an `eth_call` simulates a state-changing function perfectly well and the canonical implementation
1181
+ * is itself `view`, so one `eth_call` probes both kinds without sending anything.
1182
+ *
1183
+ * The call is made **as the token** (`from` = `opts.as`, when given): `msg.sender` namespaces the
1184
+ * canonical seed and is exactly what a caller-gating custom source checks, so probing from a
1185
+ * random address would false-negative a legitimate source. Same reason the transfer-validator
1186
+ * research used `cast call --from <collection>`.
1187
+ *
1188
+ * Never throws for an on-chain reason — every failure is a verdict.
1189
+ */
1190
+ export async function probeSeedSource(client, source, opts = {}) {
1191
+ let code;
1192
+ try {
1193
+ code = await client.getCode({ address: source });
1194
+ }
1195
+ catch (err) {
1196
+ return { verdict: 'unreachable', address: source, error: firstLine(err) };
1197
+ }
1198
+ if (!code || code === '0x')
1199
+ return { verdict: 'no-code', address: source };
1200
+ const tokenId = opts.tokenId ?? 0n;
1201
+ // `to` mirrors the mint: the token asks for a seed for a recipient. The canonical source ignores
1202
+ // it; a custom one may not, so pass a plausible non-zero address rather than 0x0 (which a source
1203
+ // validating its recipient would rightly refuse, turning a good source into a false negative).
1204
+ const to = opts.as ?? source;
1205
+ try {
1206
+ const res = await client.call({
1207
+ to: source,
1208
+ data: encodeFunctionData({ abi: abxSeedSourceAbi, functionName: 'seed', args: [tokenId, to] }),
1209
+ ...(opts.as ? { account: opts.as } : {}),
1210
+ });
1211
+ const bytes = res.data ? (res.data.length - 2) / 2 : 0;
1212
+ if (bytes === 0)
1213
+ return { verdict: 'empty-return', address: source, returnedBytes: 0 };
1214
+ if (bytes < 32)
1215
+ return { verdict: 'short-return', address: source, returnedBytes: bytes };
1216
+ return { verdict: 'ok', address: source, returnedBytes: bytes };
1217
+ }
1218
+ catch (err) {
1219
+ return { verdict: 'reverted', address: source, error: firstLine(err) };
1220
+ }
1221
+ }
1222
+ /** First line of an error message — a revert reason is readable; viem's full dump is not. */
1223
+ function firstLine(err) {
1224
+ return String(err?.message ?? err).split('\n')[0].trim();
1225
+ }
1226
+ /**
1227
+ * {@link probeSeedSource}, as a guard: throws {@link SeedSourceUnusableError} unless the verdict is
1228
+ * `ok`. For an SDK caller that just wants "refuse a bad source" without composing prose; the CLI
1229
+ * uses the probe directly so it can name its own flags in the refusal.
1230
+ */
1231
+ export async function assertSeedSourceUsable(client, source, opts = {}) {
1232
+ const probe = await probeSeedSource(client, source, opts);
1233
+ if (probe.verdict !== 'ok')
1234
+ throw new SeedSourceUnusableError(probe);
1235
+ }
595
1236
  /** Deterministic deploy of a {SeriesCode} clone — the code-project factory op. */
596
1237
  export function prepareDeploySeriesCode(args) {
597
1238
  return {
@@ -621,12 +1262,22 @@ export function prepareDeploySeriesCode(args) {
621
1262
  * one atomic multicall. */
622
1263
  export function prepareCodeSetup(args) {
623
1264
  const deps = args.deps ?? [];
1265
+ // EVM code deposit is exactly 200 gas per stored byte, so a multicall that stores these chunks
1266
+ // CANNOT cost less than this — before the CREATEs themselves, the schema/dependency/URI legs, or
1267
+ // any mints riding along. Deliberately the bare deposit and nothing else: this number's whole job
1268
+ // is to be un-arguable, so that an estimate below it is proof of a broken estimate rather than a
1269
+ // hint. (Do NOT pad it into a "probably enough" figure — a padded bound tempts a caller to send it
1270
+ // as the gas limit, and it would under-fund any setup carrying more than a couple of legs.)
1271
+ const chunkBytes = args.chunkBytes ?? [];
1272
+ const deposit = chunkBytes.reduce((g, n) => g + n * 200, 0);
1273
+ const gasFloor = deposit > 0 ? BigInt(deposit) : undefined;
624
1274
  return {
625
1275
  op: 'code-setup',
626
1276
  to: args.contract,
627
1277
  data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'multicall', args: [args.calls] }),
628
1278
  value: ZERO_VALUE,
629
1279
  chainId: args.chainId,
1280
+ ...(gasFloor === undefined ? {} : { gasFloor: `0x${gasFloor.toString(16)}` }),
630
1281
  // Human, and ACCURATE to what THIS multicall actually contains — a creator signs it in their
631
1282
  // wallet. (The old label was protocol jargon AND wrong for the renderer-only lane: it always
632
1283
  // claimed a "generator animation field" even when no generator/animation was in the tx.)
@@ -654,7 +1305,7 @@ export function prepareCodeSetup(args) {
654
1305
  // ── dependencies (Dependencies extension — code projects) ─────────────────────
655
1306
  // The ordered library declarations of a template-mode code project (index 0 = the runtime,
656
1307
  // by convention). Owner-only; the list stays dense (set at index ≤ count, remove only the
657
- // last). See specs/protocol/dependency-registry.md + src/deps.ts for the ref encoding.
1308
+ // last). See site/content/docs/protocol/code-projects.mdx + src/deps.ts for the ref encoding.
658
1309
  /** Set/replace the dependency at `index` — `resolution` 0 = Registry (`ref` = readable
659
1310
  * `name@version` bytes32), 1 = OnChain (`ref` = a data-contract address, left-aligned).
660
1311
  * Encode the pair with `parseDependencyRef`. Signer must be the owner. */
@@ -716,10 +1367,28 @@ export function prepareLockDependencies(args) {
716
1367
  fields: { contract: args.contract },
717
1368
  };
718
1369
  }
1370
+ /** Freeze the on-chain program (script chunks) forever — `setScriptChunk`/`removeLastScriptChunk`
1371
+ * revert after this. Irreversible, owner-only. This is the lock that actually freezes the WORK
1372
+ * of a code project; `lock-field`/`lock-uri` only freeze the metadata, and `lock-dependencies` only
1373
+ * the library set. All of them together freeze everything the CONTRACT stores — which is not the
1374
+ * same as a frozen output: params have no lock and the renderer serves them, and a `Registry`
1375
+ * dependency resolves from the registry at read time. */
1376
+ export function prepareLockScript(args) {
1377
+ return {
1378
+ op: 'lock-script',
1379
+ to: args.contract,
1380
+ data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'lockScript', args: [] }),
1381
+ value: ZERO_VALUE,
1382
+ chainId: args.chainId,
1383
+ summary: 'Freeze the on-chain program (script chunks) — permanent',
1384
+ fields: { contract: args.contract },
1385
+ };
1386
+ }
719
1387
  /** Set a CONTRACT-scope param to a literal `bytes32` (the raw owner setter — Params base).
720
1388
  * Only for schema-less keys (a schema'd key closes the raw path: `SchemaGoverned`). This is
721
- * the write behind well-known contract params like `params.keys` / `display.gateway`.
722
- * Signer must be the owner. */
1389
+ * the write behind well-known contract params like `display.animation`. The key joins the
1390
+ * contract's on-chain key list automatically (`contractParamKeys`), so it is enumerated by
1391
+ * the generator and the renderer with no separate declaration. Signer must be the owner. */
723
1392
  export function prepareSetContractParam(args) {
724
1393
  return {
725
1394
  op: 'set-contract-param',
@@ -755,7 +1424,15 @@ export function prepareSetContractParamData(args) {
755
1424
  /** Wire the three param-lifecycle hook addresses (configure/augment/transfer) in ONE owner call.
756
1425
  * The contract has no per-hook setter — `setParamHooks` writes all three at once — so a caller
757
1426
  * changing one hook must pass the CURRENT values for the other two (read them via `paramHooks()`),
758
- * else it silently clears them. `zeroAddress` for any role means "no hook". SeriesCode only. */
1427
+ * else it silently clears them. `zeroAddress` for any role means "no hook". SeriesCode/EditionCode
1428
+ * only.
1429
+ *
1430
+ * Note what the `transferHook` slot is: a **veto**. Its revert bubbles and fails the transfer, and
1431
+ * a mint is a transfer from `0x0`, so a reverting hook stops minting for the project too. Setting
1432
+ * one is therefore a standing power over a collector's ability to sell — disclose it, and offer
1433
+ * {@link prepareLockParamHooks} to a project that wants to prove it will never arm one.
1434
+ *
1435
+ * Reverts `ParamHooksLocked()` once {@link prepareLockParamHooks} has been sent. */
759
1436
  export function prepareSetParamHooks(args) {
760
1437
  return {
761
1438
  op: 'set-param-hooks',
@@ -776,6 +1453,60 @@ export function prepareSetParamHooks(args) {
776
1453
  },
777
1454
  };
778
1455
  }
1456
+ /**
1457
+ * Is the param hook set frozen (`lockParamHooks()` already sent)?
1458
+ *
1459
+ * One `eth_call` to `paramHooksLocked()`. This used to simulate `setParamHooks` as the owner and
1460
+ * watch for a `ParamHooksLocked()` revert, on the stated grounds that no getter existed — it does
1461
+ * now (`IAbxConfigurableParams`), and the simulation cost two extra reads (`paramHooks` + `owner`)
1462
+ * to infer what one read states.
1463
+ *
1464
+ * The other honest read is the `ParamHooksFrozen` event, which is what `reconstructProject` folds
1465
+ * (`ProjectState.paramHooks.locked`). Prefer the fold when you already have the logs; use this when
1466
+ * you have an address and one RPC round trip, which is `abx state`'s situation.
1467
+ *
1468
+ * Returns `undefined` — never throws for an on-chain reason — when the answer can't be established:
1469
+ * not a ConfigurableParams project, or the node refused the call. A caller must render that as
1470
+ * "unknown", never as "unlocked".
1471
+ */
1472
+ export async function readParamHooksLocked(client, contract) {
1473
+ try {
1474
+ return (await client.readContract({
1475
+ address: contract,
1476
+ abi: seriesCodeAbi,
1477
+ functionName: 'paramHooksLocked',
1478
+ }));
1479
+ }
1480
+ catch {
1481
+ return undefined;
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Freeze the param-lifecycle hook set forever — `setParamHooks` reverts `ParamHooksLocked()` after
1486
+ * this. Irreversible, owner-only. The sibling of `lock-script`/`lock-dependencies`/`lock-uri`, and
1487
+ * the one aimed at a **buyer** rather than at the metadata: the `transferHook` is a veto (its revert
1488
+ * fails a transfer, and a mint is a transfer from `0x0`), so while the hooks are unwritten-in-stone a
1489
+ * project retains a standing power over whether a collector can ever sell. Sending this gives up
1490
+ * three abilities permanently — arming a transfer veto, arming a write-time configure veto, and
1491
+ * re-pointing or clearing the read-time augment hook — and it cannot be undone, re-opened, or
1492
+ * time-limited. A project that never wants the power sends it before the sale; a buyer reads
1493
+ * `paramHooks()` plus the `ParamHooksFrozen` event (`abx state`) to check.
1494
+ *
1495
+ * It does NOT freeze anything else: schemas, values, script, dependencies, and URIs all keep their
1496
+ * own locks, and a hook already set keeps running exactly as before — freezing the SET is not
1497
+ * disarming the hooks in it.
1498
+ */
1499
+ export function prepareLockParamHooks(args) {
1500
+ return {
1501
+ op: 'lock-param-hooks',
1502
+ to: args.contract,
1503
+ data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'lockParamHooks', args: [] }),
1504
+ value: ZERO_VALUE,
1505
+ chainId: args.chainId,
1506
+ summary: 'Freeze the param hooks (configure/augment/transfer) — permanent',
1507
+ fields: { contract: args.contract },
1508
+ };
1509
+ }
779
1510
  /** Set a schema-governed PostParam (literal scalar) — signer must satisfy the schema's auth. */
780
1511
  export function prepareConfigureTokenParam(args) {
781
1512
  return {
@@ -808,4 +1539,89 @@ export function prepareConfigureTokenParamData(args) {
808
1539
  fields: { contract: args.contract, tokenId: String(args.tokenId), key: args.key },
809
1540
  };
810
1541
  }
1542
+ // ── PostParam schemas, post-deploy (ConfigurableParams) ──────────────────────
1543
+ // `setParamSchema` is owner-gated and has NO deploy-time restriction and no `exists` check, so it is
1544
+ // an upsert usable for the whole life of a project: attach a schema to a new key, or replace one.
1545
+ // There is no delete anywhere in the contract — `exists` is only ever set true — so a governed key
1546
+ // stays governed. What IS available is retirement: a `lockAfter` in the past makes every subsequent
1547
+ // write revert `ParamLockExpired`, permanently. See `prepareRetireParam`.
1548
+ /** Attach or replace one key's schema. Owner-only.
1549
+ *
1550
+ * A replacement does NOT re-validate values already stored under the key — narrow a bound, drop a
1551
+ * `Select` option or change the type and existing tokens keep values that now violate their own
1552
+ * schema. Callers that are editing (rather than creating) should read the current schema first and
1553
+ * carry forward every field they don't mean to change; this is a full-row write, not a patch. */
1554
+ export function prepareSetParamSchema(args) {
1555
+ const lockAfter = args.lockAfter ?? 0;
1556
+ const authAddress = args.authAddress ?? '0x0000000000000000000000000000000000000000';
1557
+ return {
1558
+ op: 'set-param-schema',
1559
+ to: args.contract,
1560
+ data: encodeFunctionData({
1561
+ abi: seriesCodeAbi,
1562
+ functionName: 'setParamSchema',
1563
+ args: [encodeTag(args.key), args.paramType, args.auth, authAddress, lockAfter, args.min, args.max, args.selectOptions ?? []],
1564
+ }),
1565
+ value: ZERO_VALUE,
1566
+ chainId: args.chainId,
1567
+ summary: `Set the on-chain schema for "${args.key}"${lockAfter ? ` (value locks after ${new Date(lockAfter * 1000).toISOString().slice(0, 19)}Z)` : ''}`,
1568
+ fields: {
1569
+ contract: args.contract,
1570
+ key: args.key,
1571
+ schema: args.display ?? args.key,
1572
+ ...(lockAfter ? { lockAfter: String(lockAfter) } : {}),
1573
+ },
1574
+ };
1575
+ }
1576
+ /** Read one key's schema from chain. The read half of the upsert — anything editing a live schema
1577
+ * MUST start here, or it silently rewrites the fields it didn't mention. */
1578
+ export async function readParamSchema(client, contract, key) {
1579
+ const r = (await client.readContract({
1580
+ address: contract,
1581
+ abi: seriesCodeAbi,
1582
+ functionName: 'paramSchema',
1583
+ args: [encodeTag(key)],
1584
+ }));
1585
+ return {
1586
+ exists: r[0],
1587
+ paramType: Number(r[1]),
1588
+ auth: Number(r[2]),
1589
+ authAddress: r[3],
1590
+ lockAfter: Number(r[4]),
1591
+ min: r[5],
1592
+ max: r[6],
1593
+ selectOptions: [...r[7]],
1594
+ };
1595
+ }
1596
+ /**
1597
+ * Retire a parameter: keep every field of its existing schema and set only `lockAfter` to a past
1598
+ * timestamp, so all further writes revert `ParamLockExpired`. This is the closest thing the protocol
1599
+ * has to deleting a parameter, and it is the read-modify-write case the upsert makes dangerous —
1600
+ * hence the caller passes the schema it just READ, rather than a schema it composed.
1601
+ *
1602
+ * What retiring does NOT do: erase a value already stored. That value keeps being served in token
1603
+ * data. It cannot be removed, deliberately — a value written under a `TokenOwner` or `Address` leg
1604
+ * came from a collector, and the creator should not be able to delete someone else's contribution.
1605
+ */
1606
+ export function prepareRetireParam(args) {
1607
+ const lockAfter = args.at ?? Math.floor(Date.now() / 1000) - 1;
1608
+ const tx = prepareSetParamSchema({
1609
+ contract: args.contract,
1610
+ key: args.key,
1611
+ paramType: args.current.paramType,
1612
+ auth: args.current.auth,
1613
+ authAddress: args.current.authAddress,
1614
+ lockAfter,
1615
+ min: args.current.min,
1616
+ max: args.current.max,
1617
+ selectOptions: args.current.selectOptions,
1618
+ chainId: args.chainId,
1619
+ });
1620
+ return {
1621
+ ...tx,
1622
+ op: 'retire-param',
1623
+ summary: `Retire "${args.key}" — no further writes, permanently (the schema and any stored value remain)`,
1624
+ fields: { contract: args.contract, key: args.key, lockAfter: String(lockAfter), effect: 'all future writes revert' },
1625
+ };
1626
+ }
811
1627
  //# sourceMappingURL=ops.js.map