@cloakely/sdk 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/drops.js ADDED
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Sealed NFT drops: one collection and its sealed sale (CloakDrop), made by CloakDropFactory.
3
+ *
4
+ * GTD listed wallets mint their guaranteed spots at the creator's GTD price, until the GTD window closes.
5
+ * Auction everyone else bids what they'd pay, sealed to a drand round. The creator's price is the floor. Bids rank
6
+ * by score: their price, or for WL bids their price × (1 + boost). Public winners pay the clearing score;
7
+ * WL winners pay it ÷ (1 + boost), never under the floor. Bids tied at the clearing score share what's left
8
+ * by a lottery whose random start comes from every bid's sealed salt.
9
+ *
10
+ * The GTD and WL lists live on chain only as Merkle roots. The lists themselves are published once, in the factory's
11
+ * DropDetails event, so anyone can rebuild a proof; a list that doesn't match the root proves nothing.
12
+ */
13
+ import { encodeAbiParameters, getAddress, isAddress, keccak256, toHex } from "viem";
14
+ import { cloakDropAbi, cloakDropFactoryAbi } from "./abis.js";
15
+ import { commitmentOf } from "./cloak.js";
16
+ import { drandRoundAt, drandRoundTime, sealBid, unsealBid } from "./seal.js";
17
+ export const DROP_BPS = 10000n;
18
+ export const DROP_MAX_BOOST_BPS = 3_000;
19
+ export const DROP_MAX_PER_BID = 20;
20
+ export const DROP_MAX_SUPPLY = 10_000;
21
+ /** Most entries one GTD or WL list can hold (the lists travel in one transaction). */
22
+ export const DROP_LIST_MAX = 2_000;
23
+ /** How long the keeper has to open the bids at most. Settlement doesn't wait for it once every bid is open. */
24
+ export const DROP_DEFAULT_REVEAL_SECS = 1_800;
25
+ // ───────────────────────────── Merkle lists ─────────────────────────────
26
+ const enc = (types, values) => encodeAbiParameters(types, values);
27
+ /** A WL list entry, as the drop hashes it. */
28
+ export const wlLeaf = (wallet) => keccak256(keccak256(enc([{ type: "address" }], [wallet])));
29
+ /** A GTD list entry: a wallet and its number of spots. */
30
+ export const gtdLeaf = (wallet, spots) => keccak256(keccak256(enc([{ type: "address" }, { type: "uint32" }], [wallet, spots])));
31
+ const hashPair = (a, b) => BigInt(a) < BigInt(b) ? keccak256(enc([{ type: "bytes32" }, { type: "bytes32" }], [a, b])) : keccak256(enc([{ type: "bytes32" }, { type: "bytes32" }], [b, a]));
32
+ /** A Merkle tree over `leaves` (sorted-pair hashing, the layout CloakDrop checks). */
33
+ export function merkleTree(leaves) {
34
+ const base = [...new Set(leaves.map((l) => l.toLowerCase()))].sort((a, b) => (BigInt(a) < BigInt(b) ? -1 : 1));
35
+ if (!base.length)
36
+ return { root: `0x${"0".repeat(64)}`, proof: () => [] };
37
+ const layers = [base];
38
+ while (layers.at(-1).length > 1) {
39
+ const prev = layers.at(-1);
40
+ const next = [];
41
+ for (let i = 0; i < prev.length; i += 2)
42
+ next.push(i + 1 < prev.length ? hashPair(prev[i], prev[i + 1]) : prev[i]);
43
+ layers.push(next);
44
+ }
45
+ return {
46
+ root: layers.at(-1)[0],
47
+ proof(leaf) {
48
+ let i = layers[0].indexOf(leaf.toLowerCase());
49
+ if (i < 0)
50
+ throw new Error("not on the list");
51
+ const out = [];
52
+ for (let k = 0; k < layers.length - 1; k++) {
53
+ const sib = i ^ 1;
54
+ if (sib < layers[k].length)
55
+ out.push(layers[k][sib]);
56
+ i = Math.floor(i / 2);
57
+ }
58
+ return out;
59
+ },
60
+ };
61
+ }
62
+ const PHASES = ["gtd", "bidding", "reveal", "settle", "numbering", "settled"];
63
+ // ───────────────────────────── client ─────────────────────────────
64
+ export class CloakDrops {
65
+ publicClient;
66
+ walletClient;
67
+ factory;
68
+ fromBlock;
69
+ constructor(publicClient, walletClient, factory,
70
+ /** First block to read the factory's and drops' events from. */
71
+ fromBlock = 0n) {
72
+ this.publicClient = publicClient;
73
+ this.walletClient = walletClient;
74
+ this.factory = factory;
75
+ this.fromBlock = fromBlock;
76
+ }
77
+ /** Many reads in one multicall where the chain has multicall3, else one by one (a local chain). */
78
+ async reads(contracts) {
79
+ if (!contracts.length)
80
+ return [];
81
+ if (this.publicClient.chain?.contracts?.multicall3)
82
+ return this.publicClient.multicall({ contracts: contracts, allowFailure: false });
83
+ return Promise.all(contracts.map((c) => this.publicClient.readContract(c)));
84
+ }
85
+ wallet() {
86
+ if (!this.walletClient)
87
+ throw new Error("CloakDrops: a walletClient is required for this call");
88
+ return this.walletClient;
89
+ }
90
+ async send(hash) {
91
+ const r = await this.publicClient.waitForTransactionReceipt({ hash });
92
+ if (r.status !== "success")
93
+ throw new Error(`transaction reverted: ${hash}`);
94
+ return r;
95
+ }
96
+ /** Every drop the factory made, newest last. */
97
+ async list() {
98
+ const n = Number(await this.publicClient.readContract({ address: this.factory, abi: cloakDropFactoryAbi, functionName: "dropsCount" }));
99
+ if (!n)
100
+ return [];
101
+ return (await this.reads(Array.from({ length: n }, (_, i) => ({ address: this.factory, abi: cloakDropFactoryAbi, functionName: "drops", args: [BigInt(i)] }))));
102
+ }
103
+ /** Create a drop. The caller is its creator. Returns the drop's address. */
104
+ async create(o) {
105
+ const w = this.wallet();
106
+ if (!o.name.trim() || o.name.length > 64 || !o.symbol.trim() || o.symbol.length > 16)
107
+ throw new Error("Give the collection a name (up to 64 characters) and a symbol (up to 16).");
108
+ if (!Number.isInteger(o.supply) || o.supply < 1 || o.supply > DROP_MAX_SUPPLY)
109
+ throw new Error(`Supply is 1 to ${DROP_MAX_SUPPLY}.`);
110
+ if (o.floor <= 0n)
111
+ throw new Error("Set a price above zero.");
112
+ const boost = o.wl?.boostBps ?? 0;
113
+ if (boost < 0 || boost > DROP_MAX_BOOST_BPS)
114
+ throw new Error("The WL boost is 0% to 30%.");
115
+ if ((o.gtd?.list.length ?? 0) > DROP_LIST_MAX || (o.wl?.list.length ?? 0) > DROP_LIST_MAX)
116
+ throw new Error(`A list holds up to ${DROP_LIST_MAX} wallets.`);
117
+ const gtdList = (o.gtd?.list ?? []).map((e) => ({ wallet: getAddress(e.wallet), spots: e.spots }));
118
+ const wlList = (o.wl?.list ?? []).map((a) => getAddress(a));
119
+ for (const e of gtdList)
120
+ if (!Number.isInteger(e.spots) || e.spots < 1)
121
+ throw new Error(`Give ${e.wallet} at least one GTD spot.`);
122
+ const gtdSpots = gtdList.reduce((a, e) => a + e.spots, 0);
123
+ if (gtdSpots > o.supply)
124
+ throw new Error("There are more GTD spots than NFTs.");
125
+ const now = Number((await this.publicClient.getBlock()).timestamp);
126
+ const gtdEnd = now + 90 + (gtdList.length ? o.gtd.durationSecs : 0);
127
+ const commitEnd = gtdEnd + o.biddingSecs;
128
+ const tlockRound = drandRoundAt(commitEnd);
129
+ const revealEnd = Math.max(commitEnd + (o.revealSecs ?? DROP_DEFAULT_REVEAL_SECS), drandRoundTime(tlockRound) + 60);
130
+ const params = {
131
+ creator: w.account.address,
132
+ supply: o.supply,
133
+ maxPerBid: o.maxPerBid ?? 2,
134
+ numTicks: o.numTicks ?? 200,
135
+ wlBoostBps: wlList.length ? boost : 0,
136
+ royaltyBps: o.royaltyBps ?? 500,
137
+ gtdEnd: BigInt(gtdEnd),
138
+ commitEnd: BigInt(commitEnd),
139
+ revealEnd: BigInt(revealEnd),
140
+ tlockRound: BigInt(tlockRound),
141
+ floor: o.floor,
142
+ tickSize: o.tickSize ?? (o.floor / 20n || 1n),
143
+ gtdPrice: o.gtd?.price ?? 0n,
144
+ gtdRoot: merkleTree(gtdList.map((e) => gtdLeaf(e.wallet, e.spots))).root,
145
+ wlRoot: merkleTree(wlList.map(wlLeaf)).root,
146
+ };
147
+ const details = { v: 1, ...(o.image ? { image: o.image } : {}), ...(o.description ? { description: o.description.slice(0, 1000) } : {}), ...(gtdList.length ? { gtd: gtdList } : {}), ...(wlList.length ? { wl: wlList } : {}), ...(o.spots ? { spots: { partner: o.spots.partner.slice(0, 200) } } : {}) };
148
+ const { result, request } = await this.publicClient.simulateContract({
149
+ address: this.factory,
150
+ abi: cloakDropFactoryAbi,
151
+ functionName: "createDrop",
152
+ args: [params, o.name.trim(), o.symbol.trim(), o.baseURI.trim(), JSON.stringify(details)],
153
+ account: w.account,
154
+ });
155
+ const txHash = await w.writeContract(request);
156
+ await this.send(txHash);
157
+ return { drop: result, txHash };
158
+ }
159
+ /** A drop's published details: its image, description and the lists behind its roots. */
160
+ async details(drop) {
161
+ const logs = await this.chunked((from, to) => this.publicClient.getContractEvents({ address: this.factory, abi: cloakDropFactoryAbi, eventName: "DropDetails", args: { drop }, fromBlock: from, toBlock: to, strict: true }), true);
162
+ try {
163
+ const d = JSON.parse(logs[0]?.args.details ?? "{}");
164
+ return {
165
+ image: typeof d.image === "string" ? d.image : undefined,
166
+ description: typeof d.description === "string" ? d.description : undefined,
167
+ gtd: Array.isArray(d.gtd) ? d.gtd.filter((e) => isAddress(e?.wallet) && Number.isInteger(e?.spots)) : undefined,
168
+ wl: Array.isArray(d.wl) ? d.wl.filter((a) => isAddress(a)) : undefined,
169
+ spots: typeof d.spots?.partner === "string" ? { partner: d.spots.partner } : undefined,
170
+ };
171
+ }
172
+ catch {
173
+ return {};
174
+ }
175
+ }
176
+ async get(drop) {
177
+ const c = { address: drop, abi: cloakDropAbi };
178
+ const fn = ["params", "name", "symbol", "baseURI", "phase", "bidsCount", "revealCount", "gtdMinted", "auctionSupply", "totalSupply", "settled", "failed", "sold", "clearingScore", "tieSupply", "tieDemand"];
179
+ const r = await this.reads(fn.map((f) => ({ ...c, functionName: f })));
180
+ const p = r[0];
181
+ const settled = r[10];
182
+ const clearing = r[13];
183
+ const zero = `0x${"0".repeat(64)}`;
184
+ const publicPrice = settled ? (clearing + DROP_BPS - 1n) / DROP_BPS : 0n;
185
+ let wlPrice = settled ? (clearing + DROP_BPS + BigInt(p.wlBoostBps) - 1n) / (DROP_BPS + BigInt(p.wlBoostBps)) : 0n;
186
+ if (settled && wlPrice < p.floor)
187
+ wlPrice = p.floor;
188
+ return {
189
+ address: drop,
190
+ name: r[1],
191
+ symbol: r[2],
192
+ baseURI: r[3],
193
+ creator: p.creator,
194
+ supply: p.supply,
195
+ maxPerBid: p.maxPerBid,
196
+ numTicks: p.numTicks,
197
+ wlBoostBps: p.wlBoostBps,
198
+ royaltyBps: p.royaltyBps,
199
+ gtdEnd: Number(p.gtdEnd),
200
+ commitEnd: Number(p.commitEnd),
201
+ revealEnd: Number(p.revealEnd),
202
+ tlockRound: Number(p.tlockRound),
203
+ floor: p.floor,
204
+ tickSize: p.tickSize,
205
+ gtdPrice: p.gtdPrice,
206
+ hasGtd: p.gtdRoot !== zero,
207
+ hasWl: p.wlRoot !== zero,
208
+ phase: PHASES[Number(r[4])],
209
+ bids: Number(r[5]),
210
+ reveals: Number(r[6]),
211
+ gtdMinted: Number(r[7]),
212
+ auctionSupply: Number(r[8]),
213
+ minted: Number(r[9]),
214
+ settled,
215
+ failed: r[11],
216
+ sold: Number(r[12]),
217
+ publicPrice,
218
+ wlPrice,
219
+ tieSupply: Number(r[14]),
220
+ tieDemand: Number(r[15]),
221
+ };
222
+ }
223
+ /** The bid level for a price: the highest level at or below it (bids snap down, never up). */
224
+ static tickFor(s, price) {
225
+ if (price < s.floor)
226
+ throw new Error("Bid at least the creator's price.");
227
+ const t = s.tickSize > 0n ? Number((price - s.floor) / s.tickSize) : 0;
228
+ return Math.min(t, s.numTicks - 1);
229
+ }
230
+ static priceAt(s, tick) {
231
+ return s.floor + BigInt(tick) * s.tickSize;
232
+ }
233
+ /** Whether a wallet is on a drop's WL or GTD list (and with how many GTD spots). */
234
+ async listed(drop, wallet) {
235
+ const d = await this.details(drop);
236
+ const me = wallet.toLowerCase();
237
+ const gtd = d.gtd?.find((e) => e.wallet.toLowerCase() === me);
238
+ const gtdMinted = gtd ? Number(await this.publicClient.readContract({ address: drop, abi: cloakDropAbi, functionName: "gtdMintedBy", args: [wallet] })) : 0;
239
+ return { wl: !!d.wl?.some((a) => a.toLowerCase() === me), gtdSpots: gtd?.spots ?? 0, gtdMinted };
240
+ }
241
+ /** Mint GTD spots from the listed wallet (the connected one). */
242
+ async gtdMint(drop, quantity) {
243
+ const w = this.wallet();
244
+ const [s, d] = await Promise.all([this.get(drop), this.details(drop)]);
245
+ const me = w.account.address;
246
+ const entry = d.gtd?.find((e) => e.wallet.toLowerCase() === me.toLowerCase());
247
+ if (!entry)
248
+ throw new Error("This wallet isn't on the GTD list.");
249
+ const tree = merkleTree((d.gtd ?? []).map((e) => gtdLeaf(getAddress(e.wallet), e.spots)));
250
+ const leaf = gtdLeaf(getAddress(entry.wallet), entry.spots);
251
+ const hash = await w.writeContract({ address: drop, abi: cloakDropAbi, functionName: "gtdMint", args: [entry.spots, tree.proof(leaf), quantity], value: s.gtdPrice * BigInt(quantity) });
252
+ await this.send(hash);
253
+ return hash;
254
+ }
255
+ /**
256
+ * Place a sealed bid: `quantity` NFTs at up to `price` wei each. The bid is encrypted to the drop's drand round, so
257
+ * nobody can read it before bidding closes; the keeper opens it then. A WL bid must come from the listed wallet.
258
+ * `deposit` may be more than the bid needs (the rest comes back), which hides the bid's size from the deposit.
259
+ */
260
+ async bid(drop, o) {
261
+ const w = this.wallet();
262
+ const s = await this.get(drop);
263
+ if (s.phase !== "bidding")
264
+ throw new Error(s.phase === "gtd" ? "Bidding opens when the GTD window closes." : "Bidding has closed.");
265
+ if (!Number.isInteger(o.quantity) || o.quantity < 1 || o.quantity > s.maxPerBid)
266
+ throw new Error(`Bid for 1 to ${s.maxPerBid} NFTs.`);
267
+ const tick = CloakDrops.tickFor(s, o.price);
268
+ const cost = CloakDrops.priceAt(s, tick) * BigInt(o.quantity);
269
+ const deposit = o.deposit ?? cost;
270
+ if (deposit < cost)
271
+ throw new Error("The deposit must cover the bid.");
272
+ let proof = [];
273
+ if (o.wl) {
274
+ const d = await this.details(drop);
275
+ const list = (d.wl ?? []).map((a) => getAddress(a));
276
+ if (!list.some((a) => a.toLowerCase() === w.account.address.toLowerCase()))
277
+ throw new Error("This wallet isn't on the WL.");
278
+ proof = merkleTree(list.map(wlLeaf)).proof(wlLeaf(w.account.address));
279
+ }
280
+ const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));
281
+ const quantity = BigInt(o.quantity);
282
+ const commitment = commitmentOf(drop, w.account.address, tick, quantity, salt);
283
+ const sealed = await sealBid({ auction: drop, bidder: w.account.address, tick, quantity, salt }, s.tlockRound);
284
+ const { result, request } = await this.publicClient.simulateContract({ address: drop, abi: cloakDropAbi, functionName: "commit", args: [commitment, sealed, !!o.wl, proof], value: deposit, account: w.account });
285
+ const txHash = await w.writeContract(request);
286
+ await this.send(txHash);
287
+ return { id: result, txHash, tick, salt };
288
+ }
289
+ /** Read logs from `fromBlock` to now in one call, or in 100k-block pieces where the RPC limits ranges. */
290
+ async chunked(get, firstOnly = false) {
291
+ const latest = await this.publicClient.getBlockNumber({ cacheTime: 0 });
292
+ try {
293
+ return await get(this.fromBlock, latest);
294
+ }
295
+ catch {
296
+ const out = [];
297
+ for (let from = this.fromBlock; from <= latest; from += 100000n) {
298
+ out.push(...(await get(from, from + 99999n > latest ? latest : from + 99999n)));
299
+ if (firstOnly && out.length)
300
+ break;
301
+ }
302
+ return out;
303
+ }
304
+ }
305
+ async committedLogs(drop) {
306
+ return this.chunked((from, to) => this.publicClient.getContractEvents({ address: drop, abi: cloakDropAbi, eventName: "Committed", strict: true, fromBlock: from, toBlock: to }));
307
+ }
308
+ /** Deliver one bid now: its NFTs to the bidder and the rest of its deposit back (anyone can, it's always the bidder's). */
309
+ async claim(drop, id) {
310
+ const { request } = await this.publicClient.simulateContract({ address: drop, abi: cloakDropAbi, functionName: "claim", args: [id], account: this.wallet().account });
311
+ const h = await this.wallet().writeContract(request);
312
+ await this.send(h);
313
+ return h;
314
+ }
315
+ /** A refund the drop couldn't send (a wallet that refuses HYPE) waits here for its bidder. */
316
+ async refundOwed(drop, bidder) {
317
+ return this.publicClient.readContract({ address: drop, abi: cloakDropAbi, functionName: "refundOwed", args: [bidder] });
318
+ }
319
+ async withdrawRefund(drop, to) {
320
+ const { request } = await this.publicClient.simulateContract({ address: drop, abi: cloakDropAbi, functionName: "withdrawRefund", args: [to], account: this.wallet().account });
321
+ const h = await this.wallet().writeContract(request);
322
+ await this.send(h);
323
+ return h;
324
+ }
325
+ /** Every bid on a drop (or one bidder's), with what it won once settled. */
326
+ async bids(drop, bidder) {
327
+ const [s, n] = await Promise.all([this.get(drop), this.publicClient.readContract({ address: drop, abi: cloakDropAbi, functionName: "bidsCount" })]);
328
+ if (!n)
329
+ return [];
330
+ const ids = Array.from({ length: Number(n) }, (_, i) => BigInt(i));
331
+ const raw = (await this.reads(ids.map((id) => ({ address: drop, abi: cloakDropAbi, functionName: "getBid", args: [id] }))));
332
+ const ready = s.settled && (s.tieSupply === 0 || s.phase === "settled");
333
+ const alloc = ready ? await this.reads(ids.map((id) => ({ address: drop, abi: cloakDropAbi, functionName: "allocationOf", args: [id] }))) : [];
334
+ return raw
335
+ .map((b, i) => {
336
+ const a = alloc[i];
337
+ return { id: ids[i], bidder: b.bidder, wl: b.wl, revealed: b.revealed, claimed: b.claimed, tick: b.tick, price: CloakDrops.priceAt(s, b.tick), quantity: b.quantity, deposit: b.deposit, nfts: a ? Number(a[0]) : 0, paid: a?.[1] ?? 0n, refund: a?.[2] ?? 0n };
338
+ })
339
+ .filter((b) => !bidder || b.bidder.toLowerCase() === bidder.toLowerCase());
340
+ }
341
+ // ── what the keeper (or anyone) runs ──
342
+ /** Open every sealed bid whose drand round has passed. Garbage ciphertexts are skipped (and penalised at claim). */
343
+ async revealAll(drop, opts = {}) {
344
+ const log = opts.log ?? (() => { });
345
+ const logs = await this.committedLogs(drop);
346
+ const reveals = [];
347
+ for (const l of logs) {
348
+ const b = await this.publicClient.readContract({ address: drop, abi: cloakDropAbi, functionName: "getBid", args: [l.args.id] });
349
+ if (b.revealed)
350
+ continue;
351
+ try {
352
+ const p = await unsealBid(l.args.sealedBid);
353
+ if (p.auction.toLowerCase() !== drop.toLowerCase())
354
+ throw new Error("sealed for another drop");
355
+ if (commitmentOf(drop, b.bidder, p.tick, p.quantity, p.salt) !== b.commitment)
356
+ throw new Error("ciphertext does not match commitment");
357
+ reveals.push({ id: l.args.id, tick: p.tick, quantity: p.quantity, salt: p.salt });
358
+ }
359
+ catch (e) {
360
+ log(`bid #${l.args.id}: cannot unseal (${e.message})`);
361
+ }
362
+ }
363
+ const size = opts.batchSize ?? 40;
364
+ for (let i = 0; i < reveals.length; i += size) {
365
+ await this.send(await this.wallet().writeContract({ address: drop, abi: cloakDropAbi, functionName: "revealBatch", args: [reveals.slice(i, i + size)] }));
366
+ log(`revealed ${Math.min(i + size, reveals.length)}/${reveals.length}`);
367
+ }
368
+ return { revealed: reveals.length };
369
+ }
370
+ /** Settle, then number the tied bids (if any) so claims can start. */
371
+ async settle(drop, opts = {}) {
372
+ const c = { address: drop, abi: cloakDropAbi };
373
+ const txs = [];
374
+ if (!(await this.publicClient.readContract({ ...c, functionName: "settled" }))) {
375
+ const h = await this.wallet().writeContract({ ...c, functionName: "settle" });
376
+ await this.send(h);
377
+ txs.push(h);
378
+ }
379
+ while ((await this.get(drop)).phase === "numbering") {
380
+ const h = await this.wallet().writeContract({ ...c, functionName: "numberTies", args: [BigInt(opts.tieBatch ?? 400)] });
381
+ await this.send(h);
382
+ txs.push(h);
383
+ }
384
+ return txs;
385
+ }
386
+ /** Deliver every unclaimed bid (NFTs and refunds always go to the bidder, never the caller). */
387
+ async claimAll(drop, opts = {}) {
388
+ const ids = (await this.bids(drop)).filter((b) => !b.claimed).map((b) => b.id);
389
+ const size = opts.batchSize ?? 25;
390
+ for (let i = 0; i < ids.length; i += size)
391
+ await this.send(await this.wallet().writeContract({ address: drop, abi: cloakDropAbi, functionName: "claimBatch", args: [ids.slice(i, i + size)] }));
392
+ return { claimed: ids.length };
393
+ }
394
+ /** The bidder takes its NFTs to another address (a fresh wallet, say); the refund still goes to the bidder. */
395
+ async claimTo(drop, id, to) {
396
+ const { request } = await this.publicClient.simulateContract({ address: drop, abi: cloakDropAbi, functionName: "claimTo", args: [id, to], account: this.wallet().account });
397
+ const h = await this.wallet().writeContract(request);
398
+ await this.send(h);
399
+ return h;
400
+ }
401
+ /** Pay out the creator's proceeds and the protocol fee (anyone can call; the destinations are fixed). */
402
+ async payOut(drop) {
403
+ const c = { address: drop, abi: cloakDropAbi };
404
+ const [owed, fees] = await Promise.all([this.publicClient.readContract({ ...c, functionName: "creatorOwed" }), this.publicClient.readContract({ ...c, functionName: "feesOwed" })]);
405
+ const txs = [];
406
+ if (owed > 0n) {
407
+ const h = await this.wallet().writeContract({ ...c, functionName: "withdrawProceeds" });
408
+ await this.send(h);
409
+ txs.push(h);
410
+ }
411
+ if (fees > 0n) {
412
+ try {
413
+ await this.publicClient.simulateContract({ ...c, functionName: "collectFees", account: this.wallet().account });
414
+ const h = await this.wallet().writeContract({ ...c, functionName: "collectFees" });
415
+ await this.send(h);
416
+ txs.push(h);
417
+ }
418
+ catch { }
419
+ }
420
+ return txs;
421
+ }
422
+ /** The creator mints what nobody bought (or all of it, if the drop failed). */
423
+ async mintUnsold(drop, to, quantity) {
424
+ const h = await this.wallet().writeContract({ address: drop, abi: cloakDropAbi, functionName: "mintUnsold", args: [to, quantity] });
425
+ await this.send(h);
426
+ return h;
427
+ }
428
+ }
package/dist/index.d.ts CHANGED
@@ -4,4 +4,6 @@ export * from "./deployments.js";
4
4
  export * from "./registry.js";
5
5
  export * from "./seal.js";
6
6
  export * from "./cloak.js";
7
+ export * from "./drops.js";
7
8
  export * from "./private/index.js";
9
+ export * from "./spots.js";
package/dist/index.js CHANGED
@@ -4,4 +4,6 @@ export * from "./deployments.js";
4
4
  export * from "./registry.js";
5
5
  export * from "./seal.js";
6
6
  export * from "./cloak.js";
7
+ export * from "./drops.js";
7
8
  export * from "./private/index.js";
9
+ export * from "./spots.js";
@@ -103,7 +103,10 @@ export declare class SharedVaultClient {
103
103
  private quote;
104
104
  /** 1–2 notes of `token` covering `target`, combining notes first (a relayed send to yourself) when needed. */
105
105
  private inputsFor;
106
- /** Expected output of a swap through `token`'s Uniswap market (0.3% fee), before slippage. */
106
+ /**
107
+ * Expected output of a swap through `token`'s market, before slippage. The market prices it at its own current fee:
108
+ * 0.3% on a Uniswap V2 pool, and on a v4 pool whatever the hook charges now (higher just after listing, then 0.3%).
109
+ */
107
110
  quoteSwap(token: Address, side: "buy" | "sell", amountIn: bigint): Promise<bigint>;
108
111
  /** Wallet tokens → a private note on a one-time key. This step is public (the wallet deposits). */
109
112
  shield(wallet: Wallet, token: Address, amount: bigint, onStatus?: Status): Promise<Hex>;
@@ -260,7 +260,10 @@ export class SharedVaultClient {
260
260
  }
261
261
  throw new Error("too many notes to combine");
262
262
  }
263
- /** Expected output of a swap through `token`'s Uniswap market (0.3% fee), before slippage. */
263
+ /**
264
+ * Expected output of a swap through `token`'s market, before slippage. The market prices it at its own current fee:
265
+ * 0.3% on a Uniswap V2 pool, and on a v4 pool whatever the hook charges now (higher just after listing, then 0.3%).
266
+ */
264
267
  async quoteSwap(token, side, amountIn) {
265
268
  const m = this.info(token).market;
266
269
  const [rH, rT] = await Promise.all([
@@ -268,8 +271,7 @@ export class SharedVaultClient {
268
271
  this.pub.readContract({ address: m, abi: cloakPairAbi, functionName: "reserveToken" }),
269
272
  ]);
270
273
  const [rIn, rOut] = side === "buy" ? [rH, rT] : [rT, rH];
271
- const a = amountIn * 997n;
272
- return (a * rOut) / (rIn * 1000n + a);
274
+ return this.pub.readContract({ address: m, abi: cloakPairAbi, functionName: "getAmountOut", args: [amountIn, rIn, rOut] });
273
275
  }
274
276
  // ───────────── public → private ─────────────
275
277
  /** Wallet tokens → a private note on a one-time key. This step is public (the wallet deposits). */
@@ -0,0 +1,65 @@
1
+ /**
2
+ * WL spot sales: a partner collection gives out whitelist spots and they're sold as a Cloak drop.
3
+ *
4
+ * The drop's supply is the number of spots and its sealed auction picks the winners (the floor, ties and refunds work
5
+ * as in any drop). CloakSpotBook marks the drop as a spot sale. Once it settles, each winning bid names the wallets to
6
+ * whitelist, one per spot it won, and can change them until the deadline. The book holds the list the partner uses.
7
+ * Each winner also gets the drop's NFT as a receipt.
8
+ */
9
+ import { type Address, type Hex } from "viem";
10
+ import { CloakDrops, type CreateDropOptions, type DropBid } from "./drops.js";
11
+ /** How long winners have to name their wallets, by default, once bids are opened. */
12
+ export declare const SPOT_DEFAULT_WALLET_DAYS = 3;
13
+ export declare const SPOT_MAX_WALLET_DAYS = 90;
14
+ export interface SpotSale {
15
+ drop: Address;
16
+ partner: string;
17
+ /** Unix seconds: winners name or change wallets until then. */
18
+ deadline: number;
19
+ }
20
+ /** One winning bid and the wallets it named (none yet: `wallets` is empty). */
21
+ export interface SpotWinner {
22
+ bidId: bigint;
23
+ bidder: Address;
24
+ spots: number;
25
+ wallets: Address[];
26
+ }
27
+ export type CreateSpotSaleOptions = Omit<CreateDropOptions, "gtd" | "baseURI" | "spots"> & {
28
+ /** The partner collection giving the spots. */
29
+ partner: string;
30
+ /** Days after the reveal window that winners have to name wallets (default 3). */
31
+ walletDays?: number;
32
+ baseURI?: string;
33
+ };
34
+ export declare class CloakSpots {
35
+ readonly drops: CloakDrops;
36
+ readonly book: Address;
37
+ constructor(drops: CloakDrops, book: Address);
38
+ private get pc();
39
+ private send;
40
+ /**
41
+ * Create a drop that sells `supply` WL spots for `partner`, then mark it as a spot sale. Two transactions; if the
42
+ * second fails, `open(drop, …)` finishes the job.
43
+ */
44
+ create(o: CreateSpotSaleOptions): Promise<{
45
+ drop: Address;
46
+ txHash: Hex;
47
+ openTx: Hex;
48
+ deadline: number;
49
+ }>;
50
+ /** Mark an existing drop (the caller's, not settled yet) as a WL spot sale. */
51
+ open(drop: Address, partner: string, deadline: number): Promise<Hex>;
52
+ /** The sale on a drop, or null if the drop isn't a WL spot sale. */
53
+ sale(drop: Address): Promise<SpotSale | null>;
54
+ /** Every drop marked as a spot sale, oldest first. */
55
+ sales(): Promise<Address[]>;
56
+ /** Every winning bid (once the drop is settled) with the wallets it has named so far. */
57
+ winners(drop: Address, bids?: DropBid[]): Promise<SpotWinner[]>;
58
+ /** Name the wallets to whitelist for a winning bid of the connected wallet: one per spot. Replaces earlier ones. */
59
+ setWallets(drop: Address, bidId: bigint, wallets: string[]): Promise<Hex>;
60
+ /**
61
+ * The whitelist as CSV, one row per spot: the wallet to whitelist (or blank if the winner hasn't named one), the
62
+ * bid and the bidder. A partner whitelists the `wallet` column.
63
+ */
64
+ static csv(winners: SpotWinner[]): string;
65
+ }
package/dist/spots.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * WL spot sales: a partner collection gives out whitelist spots and they're sold as a Cloak drop.
3
+ *
4
+ * The drop's supply is the number of spots and its sealed auction picks the winners (the floor, ties and refunds work
5
+ * as in any drop). CloakSpotBook marks the drop as a spot sale. Once it settles, each winning bid names the wallets to
6
+ * whitelist, one per spot it won, and can change them until the deadline. The book holds the list the partner uses.
7
+ * Each winner also gets the drop's NFT as a receipt.
8
+ */
9
+ import { getAddress, isAddress } from "viem";
10
+ import { cloakSpotBookAbi } from "./abis.js";
11
+ /** How long winners have to name their wallets, by default, once bids are opened. */
12
+ export const SPOT_DEFAULT_WALLET_DAYS = 3;
13
+ export const SPOT_MAX_WALLET_DAYS = 90;
14
+ export class CloakSpots {
15
+ drops;
16
+ book;
17
+ constructor(drops, book) {
18
+ this.drops = drops;
19
+ this.book = book;
20
+ }
21
+ get pc() {
22
+ return this.drops.publicClient;
23
+ }
24
+ async send(hash) {
25
+ const r = await this.pc.waitForTransactionReceipt({ hash });
26
+ if (r.status !== "success")
27
+ throw new Error(`transaction reverted: ${hash}`);
28
+ return r;
29
+ }
30
+ /**
31
+ * Create a drop that sells `supply` WL spots for `partner`, then mark it as a spot sale. Two transactions; if the
32
+ * second fails, `open(drop, …)` finishes the job.
33
+ */
34
+ async create(o) {
35
+ const partner = o.partner.trim();
36
+ if (!partner || partner.length > 200)
37
+ throw new Error("Name the partner collection (up to 200 characters).");
38
+ const days = o.walletDays ?? SPOT_DEFAULT_WALLET_DAYS;
39
+ if (!(days > 0 && days <= SPOT_MAX_WALLET_DAYS))
40
+ throw new Error(`Winners get 1 to ${SPOT_MAX_WALLET_DAYS} days to name wallets.`);
41
+ const { drop, txHash } = await this.drops.create({ ...o, baseURI: o.baseURI ?? "", maxPerBid: o.maxPerBid ?? 1, spots: { partner } });
42
+ const s = await this.drops.get(drop);
43
+ const deadline = s.revealEnd + Math.round(days * 86400);
44
+ const openTx = await this.open(drop, partner, deadline);
45
+ return { drop, txHash, openTx, deadline };
46
+ }
47
+ /** Mark an existing drop (the caller's, not settled yet) as a WL spot sale. */
48
+ async open(drop, partner, deadline) {
49
+ const w = this.drops.walletClient;
50
+ if (!w)
51
+ throw new Error("CloakSpots: a walletClient is required for this call");
52
+ const all = await this.drops.list();
53
+ const index = all.findIndex((a) => a.toLowerCase() === drop.toLowerCase());
54
+ if (index < 0)
55
+ throw new Error("That isn't a Cloak drop.");
56
+ const { request } = await this.pc.simulateContract({ address: this.book, abi: cloakSpotBookAbi, functionName: "open", args: [BigInt(index), partner.trim(), BigInt(deadline)], account: w.account });
57
+ const h = await w.writeContract(request);
58
+ await this.send(h);
59
+ return h;
60
+ }
61
+ /** The sale on a drop, or null if the drop isn't a WL spot sale. */
62
+ async sale(drop) {
63
+ const s = await this.pc.readContract({ address: this.book, abi: cloakSpotBookAbi, functionName: "sale", args: [drop] });
64
+ return s.open ? { drop, partner: s.partner, deadline: Number(s.deadline) } : null;
65
+ }
66
+ /** Every drop marked as a spot sale, oldest first. */
67
+ async sales() {
68
+ const n = Number(await this.pc.readContract({ address: this.book, abi: cloakSpotBookAbi, functionName: "salesCount" }));
69
+ const out = [];
70
+ for (let i = 0; i < n; i++)
71
+ out.push(await this.pc.readContract({ address: this.book, abi: cloakSpotBookAbi, functionName: "sales", args: [BigInt(i)] }));
72
+ return out;
73
+ }
74
+ /** Every winning bid (once the drop is settled) with the wallets it has named so far. */
75
+ async winners(drop, bids) {
76
+ const all = bids ?? (await this.drops.bids(drop));
77
+ const won = all.filter((b) => b.nfts > 0);
78
+ const [ids, wallets] = await this.pc.readContract({ address: this.book, abi: cloakSpotBookAbi, functionName: "list", args: [drop] });
79
+ return won.map((b) => ({ bidId: b.id, bidder: b.bidder, spots: b.nfts, wallets: wallets.filter((_, i) => ids[i] === b.id) }));
80
+ }
81
+ /** Name the wallets to whitelist for a winning bid of the connected wallet: one per spot. Replaces earlier ones. */
82
+ async setWallets(drop, bidId, wallets) {
83
+ const w = this.drops.walletClient;
84
+ if (!w)
85
+ throw new Error("CloakSpots: a walletClient is required for this call");
86
+ const list = wallets.map((a) => a.trim());
87
+ const bad = list.find((a) => !isAddress(a));
88
+ if (bad !== undefined)
89
+ throw new Error(`${bad || "An empty line"} isn't a wallet address.`);
90
+ const clean = list.map((a) => getAddress(a));
91
+ if (new Set(clean.map((a) => a.toLowerCase())).size !== clean.length)
92
+ throw new Error("Each spot needs a different wallet.");
93
+ const { request } = await this.pc.simulateContract({ address: this.book, abi: cloakSpotBookAbi, functionName: "setWallets", args: [drop, bidId, clean], account: w.account });
94
+ const h = await w.writeContract(request);
95
+ await this.send(h);
96
+ return h;
97
+ }
98
+ /**
99
+ * The whitelist as CSV, one row per spot: the wallet to whitelist (or blank if the winner hasn't named one), the
100
+ * bid and the bidder. A partner whitelists the `wallet` column.
101
+ */
102
+ static csv(winners) {
103
+ const rows = ["wallet,bid,bidder,status"];
104
+ for (const w of winners)
105
+ for (let i = 0; i < w.spots; i++)
106
+ rows.push(`${w.wallets[i] ?? ""},${w.bidId},${w.bidder},${w.wallets[i] ? "named" : "not named yet"}`);
107
+ return rows.join("\n") + "\n";
108
+ }
109
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloakely/sdk",
3
- "version": "0.8.1",
4
- "description": "Cloak on Elysium: sealed-bid token launches and private trading (shielded balances, private swaps, batch auctions, private sends) with on-device zero-knowledge proofs.",
3
+ "version": "0.8.3",
4
+ "description": "Cloak on Elysium: sealed-bid token launches, sealed NFT drops and WL spot sales, and private trading (shielded balances, private swaps, batch auctions, private sends) with on-device zero-knowledge proofs.",
5
5
  "keywords": [
6
6
  "cloak",
7
7
  "elysium",