@artblocks/abx-sdk 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +79 -0
- package/README.md +130 -0
- package/dist/abi/generated.d.ts +8502 -129
- package/dist/abi/generated.d.ts.map +1 -1
- package/dist/abi/generated.js +40 -22
- package/dist/abi/generated.js.map +1 -1
- package/dist/abi/index.d.ts +2645 -168
- package/dist/abi/index.d.ts.map +1 -1
- package/dist/abi/index.js +34 -7
- package/dist/abi/index.js.map +1 -1
- package/dist/anchors.d.ts +318 -0
- package/dist/anchors.d.ts.map +1 -0
- package/dist/anchors.js +701 -0
- package/dist/anchors.js.map +1 -0
- package/dist/chains.d.ts +19 -9
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +31 -15
- package/dist/chains.js.map +1 -1
- package/dist/chunks.d.ts +123 -27
- package/dist/chunks.d.ts.map +1 -1
- package/dist/chunks.js +124 -9
- package/dist/chunks.js.map +1 -1
- package/dist/clients.d.ts +23 -11
- package/dist/clients.d.ts.map +1 -1
- package/dist/clients.js +17 -18
- package/dist/clients.js.map +1 -1
- package/dist/create2.d.ts +84 -1
- package/dist/create2.d.ts.map +1 -1
- package/dist/create2.js +158 -2
- package/dist/create2.js.map +1 -1
- package/dist/creator-token.d.ts +132 -0
- package/dist/creator-token.d.ts.map +1 -0
- package/dist/creator-token.js +183 -0
- package/dist/creator-token.js.map +1 -0
- package/dist/deploy.d.ts +234 -14
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +289 -111
- package/dist/deploy.js.map +1 -1
- package/dist/deployments.d.ts +85 -5
- package/dist/deployments.d.ts.map +1 -1
- package/dist/deployments.js +159 -30
- package/dist/deployments.js.map +1 -1
- package/dist/deps.d.ts +123 -6
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +249 -7
- package/dist/deps.js.map +1 -1
- package/dist/env.d.ts +5 -4
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +20 -5
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +109 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +148 -0
- package/dist/errors.js.map +1 -0
- package/dist/execute.d.ts +118 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +159 -0
- package/dist/execute.js.map +1 -0
- package/dist/gateways.d.ts +79 -0
- package/dist/gateways.d.ts.map +1 -0
- package/dist/gateways.js +156 -0
- package/dist/gateways.js.map +1 -0
- package/dist/generator-document.d.ts +57 -0
- package/dist/generator-document.d.ts.map +1 -0
- package/dist/generator-document.js +123 -0
- package/dist/generator-document.js.map +1 -0
- package/dist/generator.d.ts +44 -2
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +29 -0
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +18 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +48 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +295 -0
- package/dist/inspect.js.map +1 -0
- package/dist/migrate.d.ts +41 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +142 -0
- package/dist/migrate.js.map +1 -0
- package/dist/mime.d.ts +11 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +40 -0
- package/dist/mime.js.map +1 -0
- package/dist/node.d.ts +69 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +92 -0
- package/dist/node.js.map +1 -0
- package/dist/onchain-uri.d.ts +110 -0
- package/dist/onchain-uri.d.ts.map +1 -0
- package/dist/onchain-uri.js +276 -0
- package/dist/onchain-uri.js.map +1 -0
- package/dist/ops.d.ts +507 -14
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +850 -34
- package/dist/ops.js.map +1 -1
- package/dist/policy.d.ts +57 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +35 -0
- package/dist/policy.js.map +1 -0
- package/dist/probe.d.ts +58 -0
- package/dist/probe.d.ts.map +1 -1
- package/dist/probe.js +190 -9
- package/dist/probe.js.map +1 -1
- package/dist/reconstruct.d.ts +141 -3
- package/dist/reconstruct.d.ts.map +1 -1
- package/dist/reconstruct.js +479 -100
- package/dist/reconstruct.js.map +1 -1
- package/dist/resume.d.ts +140 -0
- package/dist/resume.d.ts.map +1 -0
- package/dist/resume.js +146 -0
- package/dist/resume.js.map +1 -0
- package/dist/script-chunks.d.ts +105 -0
- package/dist/script-chunks.d.ts.map +1 -0
- package/dist/script-chunks.js +158 -0
- package/dist/script-chunks.js.map +1 -0
- package/dist/service.d.ts +151 -9
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +103 -11
- package/dist/service.js.map +1 -1
- package/dist/spine.d.ts +47 -4
- package/dist/spine.d.ts.map +1 -1
- package/dist/spine.js +0 -0
- package/dist/spine.js.map +1 -1
- package/dist/staging.d.ts +139 -0
- package/dist/staging.d.ts.map +1 -0
- package/dist/staging.js +139 -0
- package/dist/staging.js.map +1 -0
- package/dist/token.d.ts +11 -1
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +32 -1
- package/dist/token.js.map +1 -1
- package/dist/tokendata.d.ts +63 -3
- package/dist/tokendata.d.ts.map +1 -1
- package/dist/tokendata.js +81 -19
- package/dist/tokendata.js.map +1 -1
- package/dist/tokens.d.ts +119 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +317 -0
- package/dist/tokens.js.map +1 -0
- package/dist/types.d.ts +107 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/util.d.ts +74 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +106 -0
- package/dist/util.js.map +1 -0
- 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,
|
|
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
|
-
|
|
259
|
-
|
|
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 —
|
|
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:
|
|
382
|
-
data:
|
|
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 —
|
|
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:
|
|
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 —
|
|
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:
|
|
518
|
-
data:
|
|
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
|
|
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`).
|
|
563
|
-
*
|
|
564
|
-
* the
|
|
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
|
|
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({
|
|
574
|
-
|
|
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
|
-
|
|
579
|
-
:
|
|
580
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 `
|
|
722
|
-
*
|
|
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
|
|
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
|