@cloakely/sdk 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/abis.d.ts +4584 -0
- package/dist/abis.js +5916 -0
- package/dist/bytecode.d.ts +17 -0
- package/dist/bytecode.js +2 -0
- package/dist/chains.d.ts +74 -0
- package/dist/chains.js +20 -0
- package/dist/cloak.d.ts +1113 -0
- package/dist/cloak.js +415 -0
- package/dist/deployments.d.ts +27 -0
- package/dist/deployments.js +27 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/private/client.d.ts +162 -0
- package/dist/private/client.js +539 -0
- package/dist/private/crypto.d.ts +75 -0
- package/dist/private/crypto.js +135 -0
- package/dist/private/index.d.ts +5 -0
- package/dist/private/index.js +5 -0
- package/dist/private/prover.d.ts +31 -0
- package/dist/private/prover.js +55 -0
- package/dist/private/tree.d.ts +13 -0
- package/dist/private/tree.js +52 -0
- package/dist/private/tx.d.ts +121 -0
- package/dist/private/tx.js +157 -0
- package/dist/seal.d.ts +17 -0
- package/dist/seal.js +41 -0
- package/package.json +72 -4
package/dist/cloak.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { zeroAddress, encodeAbiParameters, keccak256, parseEventLogs, toHex, } from "viem";
|
|
2
|
+
import { cloakAuctionAbi, cloakFactoryAbi, cloakPairAbi, cloakTokenAbi } from "./abis.js";
|
|
3
|
+
import { drandRoundAt, sealBid, unsealBid } from "./seal.js";
|
|
4
|
+
const WAD = 10n ** 18n;
|
|
5
|
+
const PHASES = ["commit", "reveal", "settle", "settled"];
|
|
6
|
+
/**
|
|
7
|
+
* Budget buckets: deposits are rounded up to 1, 2 or 5 × 10ⁿ wei, so the deposit only reveals the bid's order of
|
|
8
|
+
* magnitude (every bid within ~2.5× of another looks the same) at any price scale.
|
|
9
|
+
*/
|
|
10
|
+
export function budgetBuckets(atLeast, count = 8) {
|
|
11
|
+
const out = [];
|
|
12
|
+
let b = bucketDeposit(atLeast > 0n ? atLeast : 1n);
|
|
13
|
+
while (out.length < count) {
|
|
14
|
+
out.push(b);
|
|
15
|
+
b = nextBucket(b);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
function nextBucket(b) {
|
|
20
|
+
const s = b.toString();
|
|
21
|
+
const lead = s[0];
|
|
22
|
+
const pow = 10n ** BigInt(s.length - 1);
|
|
23
|
+
return lead === "1" ? 2n * pow : lead === "2" ? 5n * pow : 10n * pow;
|
|
24
|
+
}
|
|
25
|
+
export function commitmentOf(auction, bidder, tick, quantity, salt) {
|
|
26
|
+
return keccak256(encodeAbiParameters([{ type: "address" }, { type: "address" }, { type: "uint16" }, { type: "uint128" }, { type: "bytes32" }], [auction, bidder, tick, quantity, salt]));
|
|
27
|
+
}
|
|
28
|
+
export function randomSalt() {
|
|
29
|
+
const b = new Uint8Array(32);
|
|
30
|
+
globalThis.crypto.getRandomValues(b);
|
|
31
|
+
return toHex(b);
|
|
32
|
+
}
|
|
33
|
+
/** Round a required deposit up to the next 1/2/5 × 10ⁿ bucket. */
|
|
34
|
+
export function bucketDeposit(cost) {
|
|
35
|
+
if (cost <= 1n)
|
|
36
|
+
return 1n;
|
|
37
|
+
const pow = 10n ** BigInt(cost.toString().length - 1);
|
|
38
|
+
for (const m of [1n, 2n, 5n, 10n])
|
|
39
|
+
if (m * pow >= cost)
|
|
40
|
+
return m * pow;
|
|
41
|
+
return 10n * pow;
|
|
42
|
+
}
|
|
43
|
+
/** `launches(i)` of the first CloakFactory (five fields, no vesting). */
|
|
44
|
+
const LEGACY_LAUNCHES_ABI = [
|
|
45
|
+
{
|
|
46
|
+
type: "function",
|
|
47
|
+
name: "launches",
|
|
48
|
+
stateMutability: "view",
|
|
49
|
+
inputs: [{ type: "uint256" }],
|
|
50
|
+
outputs: [
|
|
51
|
+
{ name: "auction", type: "address" },
|
|
52
|
+
{ name: "token", type: "address" },
|
|
53
|
+
{ name: "creator", type: "address" },
|
|
54
|
+
{ name: "launchpad", type: "address" },
|
|
55
|
+
{ name: "creatorAlloc", type: "uint256" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
export class Cloak {
|
|
60
|
+
publicClient;
|
|
61
|
+
walletClient;
|
|
62
|
+
factory;
|
|
63
|
+
legacyFactory;
|
|
64
|
+
constructor(publicClient, walletClient, factory,
|
|
65
|
+
/** The first deployment's factory, so its launches still list and settle. */
|
|
66
|
+
legacyFactory) {
|
|
67
|
+
this.publicClient = publicClient;
|
|
68
|
+
this.walletClient = walletClient;
|
|
69
|
+
this.factory = factory;
|
|
70
|
+
this.legacyFactory = legacyFactory;
|
|
71
|
+
}
|
|
72
|
+
wallet() {
|
|
73
|
+
if (!this.walletClient)
|
|
74
|
+
throw new Error("Cloak: a walletClient is required for this call");
|
|
75
|
+
return this.walletClient;
|
|
76
|
+
}
|
|
77
|
+
async send(hash) {
|
|
78
|
+
const r = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
79
|
+
if (r.status !== "success")
|
|
80
|
+
throw new Error(`transaction reverted: ${hash}`);
|
|
81
|
+
return r;
|
|
82
|
+
}
|
|
83
|
+
// ───────────── launchpad side ─────────────
|
|
84
|
+
async createLaunch(p) {
|
|
85
|
+
if (!this.factory)
|
|
86
|
+
throw new Error("Cloak: factory address required");
|
|
87
|
+
const w = this.wallet();
|
|
88
|
+
const lock = p.lpLockUntil === undefined || p.lpLockUntil === "burn" ? 0 : p.lpLockUntil;
|
|
89
|
+
const params = {
|
|
90
|
+
creator: p.creator ?? w.account.address,
|
|
91
|
+
saleSupply: p.saleSupply,
|
|
92
|
+
lpReserve: p.lpReserve ?? p.saleSupply,
|
|
93
|
+
minPrice: p.minPrice,
|
|
94
|
+
tickSize: p.tickSize,
|
|
95
|
+
numTicks: p.numTicks ?? 100,
|
|
96
|
+
commitEnd: BigInt(p.commitEnd),
|
|
97
|
+
revealEnd: BigInt(p.revealEnd),
|
|
98
|
+
lpBps: p.lpBps ?? 5000,
|
|
99
|
+
noRevealPenaltyBps: p.noRevealPenaltyBps ?? 1000,
|
|
100
|
+
lpLockUntil: BigInt(lock),
|
|
101
|
+
tlockRound: BigInt(drandRoundAt(p.commitEnd)),
|
|
102
|
+
};
|
|
103
|
+
const txHash = await w.writeContract({
|
|
104
|
+
address: this.factory,
|
|
105
|
+
abi: cloakFactoryAbi,
|
|
106
|
+
functionName: "createLaunch",
|
|
107
|
+
args: [p.name, p.symbol, p.creatorAlloc ?? 0n, BigInt(p.creatorAlloc ? (p.creatorVestSecs ?? 90 * 86400) : 0), params, p.metadataURI ?? ""],
|
|
108
|
+
});
|
|
109
|
+
const r = await this.send(txHash);
|
|
110
|
+
const [ev] = parseEventLogs({ abi: cloakFactoryAbi, logs: r.logs, eventName: "LaunchCreated" });
|
|
111
|
+
return { auction: ev.args.auction, token: ev.args.token, txHash, block: r.blockNumber };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every launch, newest factory first. Launches from `legacyFactory` (the first deployment) are included with
|
|
115
|
+
* `legacy: true`; they have no vesting contract.
|
|
116
|
+
*/
|
|
117
|
+
async listLaunches() {
|
|
118
|
+
if (!this.factory)
|
|
119
|
+
throw new Error("Cloak: factory address required");
|
|
120
|
+
const legacy = this.legacyFactory ? await this.listLegacy(this.legacyFactory) : [];
|
|
121
|
+
return [...(await this.listCurrent()), ...legacy];
|
|
122
|
+
}
|
|
123
|
+
async listLegacy(factory) {
|
|
124
|
+
const n = await this.publicClient.readContract({ address: factory, abi: cloakFactoryAbi, functionName: "launchesCount" });
|
|
125
|
+
const out = [];
|
|
126
|
+
for (let i = 0n; i < n; i++) {
|
|
127
|
+
const [auction, token, creator, launchpad, creatorAlloc] = await this.publicClient.readContract({
|
|
128
|
+
address: factory,
|
|
129
|
+
abi: LEGACY_LAUNCHES_ABI,
|
|
130
|
+
functionName: "launches",
|
|
131
|
+
args: [i],
|
|
132
|
+
});
|
|
133
|
+
out.push({ auction, token, creator, launchpad, creatorAlloc, vesting: zeroAddress, legacy: true });
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
async listCurrent() {
|
|
138
|
+
if (!this.factory)
|
|
139
|
+
throw new Error("Cloak: factory address required");
|
|
140
|
+
const n = await this.publicClient.readContract({
|
|
141
|
+
address: this.factory,
|
|
142
|
+
abi: cloakFactoryAbi,
|
|
143
|
+
functionName: "launchesCount",
|
|
144
|
+
});
|
|
145
|
+
const out = [];
|
|
146
|
+
for (let i = 0n; i < n; i++) {
|
|
147
|
+
const [auction, token, creator, launchpad, creatorAlloc, vesting] = await this.publicClient.readContract({
|
|
148
|
+
address: this.factory,
|
|
149
|
+
abi: cloakFactoryAbi,
|
|
150
|
+
functionName: "launches",
|
|
151
|
+
args: [i],
|
|
152
|
+
});
|
|
153
|
+
out.push({ auction, token, creator, launchpad, creatorAlloc, vesting });
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
// ───────────── reads ─────────────
|
|
158
|
+
async getAuction(auction) {
|
|
159
|
+
const c = { address: auction, abi: cloakAuctionAbi };
|
|
160
|
+
const r = this.publicClient;
|
|
161
|
+
const [params, token, phase, commits, reveals, totalDeposits, totalDemand, settled, failed, clearingTick, clearingPrice, sold, raised, pair, launchFeeBps, bidCap, depositCap, creatorOwed, feesOwed] = await Promise.all([
|
|
162
|
+
r.readContract({ ...c, functionName: "params" }),
|
|
163
|
+
r.readContract({ ...c, functionName: "token" }),
|
|
164
|
+
r.readContract({ ...c, functionName: "phase" }),
|
|
165
|
+
r.readContract({ ...c, functionName: "bidsCount" }),
|
|
166
|
+
r.readContract({ ...c, functionName: "revealCount" }),
|
|
167
|
+
r.readContract({ ...c, functionName: "totalDeposits" }),
|
|
168
|
+
r.readContract({ ...c, functionName: "totalDemand" }),
|
|
169
|
+
r.readContract({ ...c, functionName: "settled" }),
|
|
170
|
+
r.readContract({ ...c, functionName: "failed" }),
|
|
171
|
+
r.readContract({ ...c, functionName: "clearingTick" }),
|
|
172
|
+
r.readContract({ ...c, functionName: "clearingPrice" }),
|
|
173
|
+
r.readContract({ ...c, functionName: "sold" }),
|
|
174
|
+
r.readContract({ ...c, functionName: "raised" }),
|
|
175
|
+
r.readContract({ ...c, functionName: "pair" }),
|
|
176
|
+
// not on first-deployment auctions
|
|
177
|
+
r.readContract({ ...c, functionName: "launchFeeBps" }).catch(() => 0),
|
|
178
|
+
r.readContract({ ...c, functionName: "bidCap" }).catch(() => 0n),
|
|
179
|
+
r.readContract({ ...c, functionName: "depositCap" }).catch(() => 0n),
|
|
180
|
+
r.readContract({ ...c, functionName: "creatorOwed" }).catch(() => 0n),
|
|
181
|
+
r.readContract({ ...c, functionName: "feesOwed" }).catch(() => 0n),
|
|
182
|
+
]);
|
|
183
|
+
const [creator, saleSupply, lpReserve, minPrice, tickSize, numTicks, commitEnd, revealEnd, lpBps, noRevealPenaltyBps, lpLockUntil, tlockRound] = params;
|
|
184
|
+
const [tokenName, tokenSymbol] = await Promise.all([
|
|
185
|
+
r.readContract({ address: token, abi: cloakTokenAbi, functionName: "name" }),
|
|
186
|
+
r.readContract({ address: token, abi: cloakTokenAbi, functionName: "symbol" }),
|
|
187
|
+
]);
|
|
188
|
+
return {
|
|
189
|
+
address: auction,
|
|
190
|
+
token,
|
|
191
|
+
creator,
|
|
192
|
+
saleSupply,
|
|
193
|
+
lpReserve,
|
|
194
|
+
minPrice,
|
|
195
|
+
tickSize,
|
|
196
|
+
numTicks,
|
|
197
|
+
commitEnd: Number(commitEnd),
|
|
198
|
+
revealEnd: Number(revealEnd),
|
|
199
|
+
lpBps,
|
|
200
|
+
noRevealPenaltyBps,
|
|
201
|
+
lpLockUntil: Number(lpLockUntil),
|
|
202
|
+
tlockRound: Number(tlockRound),
|
|
203
|
+
phase: PHASES[phase],
|
|
204
|
+
commits: Number(commits),
|
|
205
|
+
reveals: Number(reveals),
|
|
206
|
+
totalDeposits,
|
|
207
|
+
totalDemand,
|
|
208
|
+
settled,
|
|
209
|
+
failed,
|
|
210
|
+
clearingTick,
|
|
211
|
+
clearingPrice,
|
|
212
|
+
sold,
|
|
213
|
+
raised,
|
|
214
|
+
pair,
|
|
215
|
+
tokenName,
|
|
216
|
+
tokenSymbol,
|
|
217
|
+
launchFeeBps,
|
|
218
|
+
bidCap,
|
|
219
|
+
depositCap,
|
|
220
|
+
creatorOwed,
|
|
221
|
+
feesOwed,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Revealed demand per tick (empty until the reveal phase — that is the point). */
|
|
225
|
+
async demandCurve(auction) {
|
|
226
|
+
const s = await this.getAuction(auction);
|
|
227
|
+
const curve = await this.publicClient.readContract({
|
|
228
|
+
address: auction,
|
|
229
|
+
abi: cloakAuctionAbi,
|
|
230
|
+
functionName: "demandCurve",
|
|
231
|
+
});
|
|
232
|
+
return curve.map((d, t) => ({ tick: t, price: s.minPrice + BigInt(t) * s.tickSize, demand: d }));
|
|
233
|
+
}
|
|
234
|
+
async pairPrice(pair) {
|
|
235
|
+
return this.publicClient.readContract({ address: pair, abi: cloakPairAbi, functionName: "price" });
|
|
236
|
+
}
|
|
237
|
+
// ───────────── bidder side ─────────────
|
|
238
|
+
/** Pick the tick for a max price (rounded down to a tick) and the bucketed deposit that hides the bid size. */
|
|
239
|
+
quoteBid(s, maxPrice, quantity) {
|
|
240
|
+
if (maxPrice < s.minPrice)
|
|
241
|
+
throw new Error("max price is below the auction's minimum price");
|
|
242
|
+
let tick = s.tickSize === 0n ? 0 : Number((maxPrice - s.minPrice) / s.tickSize);
|
|
243
|
+
tick = Math.min(tick, s.numTicks - 1);
|
|
244
|
+
const price = s.minPrice + BigInt(tick) * s.tickSize;
|
|
245
|
+
const cost = (price * quantity + WAD - 1n) / WAD;
|
|
246
|
+
return { tick, price, cost, deposit: bucketDeposit(cost) };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Seal and commit a bid. Only a hash and a drand-timelocked ciphertext go on-chain, so neither the sequencer nor other
|
|
250
|
+
* bidders learn the price or size. After commit closes, any keeper can decrypt and reveal it — no second visit needed.
|
|
251
|
+
*/
|
|
252
|
+
async bid(auction, opts) {
|
|
253
|
+
const w = this.wallet();
|
|
254
|
+
const s = await this.getAuction(auction);
|
|
255
|
+
if (s.phase !== "commit")
|
|
256
|
+
throw new Error("commit phase is over");
|
|
257
|
+
const q = this.quoteBid(s, opts.maxPrice, opts.quantity);
|
|
258
|
+
const deposit = opts.deposit ?? q.deposit;
|
|
259
|
+
if (deposit < q.cost)
|
|
260
|
+
throw new Error("deposit is below the bid's cost");
|
|
261
|
+
const salt = randomSalt();
|
|
262
|
+
const bidder = w.account.address;
|
|
263
|
+
const commitment = commitmentOf(auction, bidder, q.tick, opts.quantity, salt);
|
|
264
|
+
const sealed = await sealBid({ auction, bidder, tick: q.tick, quantity: opts.quantity, salt }, s.tlockRound);
|
|
265
|
+
const txHash = await w.writeContract({
|
|
266
|
+
address: auction,
|
|
267
|
+
abi: cloakAuctionAbi,
|
|
268
|
+
functionName: "commit",
|
|
269
|
+
args: [commitment, sealed],
|
|
270
|
+
value: deposit,
|
|
271
|
+
});
|
|
272
|
+
const r = await this.send(txHash);
|
|
273
|
+
const [ev] = parseEventLogs({ abi: cloakAuctionAbi, logs: r.logs, eventName: "Committed" });
|
|
274
|
+
return { id: ev.args.id, auction, bidder, tick: q.tick, quantity: opts.quantity, salt, deposit, commitment, txHash };
|
|
275
|
+
}
|
|
276
|
+
async reveal(secret) {
|
|
277
|
+
const txHash = await this.wallet().writeContract({
|
|
278
|
+
address: secret.auction,
|
|
279
|
+
abi: cloakAuctionAbi,
|
|
280
|
+
functionName: "reveal",
|
|
281
|
+
args: [secret.id, secret.tick, secret.quantity, secret.salt],
|
|
282
|
+
});
|
|
283
|
+
await this.send(txHash);
|
|
284
|
+
return txHash;
|
|
285
|
+
}
|
|
286
|
+
async settle(auction) {
|
|
287
|
+
const txHash = await this.wallet().writeContract({ address: auction, abi: cloakAuctionAbi, functionName: "settle" });
|
|
288
|
+
await this.send(txHash);
|
|
289
|
+
return txHash;
|
|
290
|
+
}
|
|
291
|
+
/** Pay out a settled launch's creator proceeds, and send its launch fee to the treasury (anyone can call either). */
|
|
292
|
+
async payOut(auction) {
|
|
293
|
+
const s = await this.getAuction(auction);
|
|
294
|
+
const out = [];
|
|
295
|
+
for (const fn of [s.creatorOwed > 0n && "withdrawProceeds", s.feesOwed > 0n && "collectFees"]) {
|
|
296
|
+
if (!fn)
|
|
297
|
+
continue;
|
|
298
|
+
const h = await this.wallet().writeContract({ address: auction, abi: cloakAuctionAbi, functionName: fn });
|
|
299
|
+
await this.send(h);
|
|
300
|
+
out.push(h);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
async claim(auction, ids) {
|
|
305
|
+
const txHash = await this.wallet().writeContract({
|
|
306
|
+
address: auction,
|
|
307
|
+
abi: cloakAuctionAbi,
|
|
308
|
+
functionName: "claimBatch",
|
|
309
|
+
args: [ids],
|
|
310
|
+
});
|
|
311
|
+
await this.send(txHash);
|
|
312
|
+
return txHash;
|
|
313
|
+
}
|
|
314
|
+
async bidsOf(auction, bidder, fromBlock = 0n) {
|
|
315
|
+
const logs = await this.committedLogs(auction, fromBlock);
|
|
316
|
+
const mine = logs.filter((l) => l.args.bidder.toLowerCase() === bidder.toLowerCase());
|
|
317
|
+
return Promise.all(mine.map(async (l) => {
|
|
318
|
+
const [bid, alloc] = await Promise.all([
|
|
319
|
+
this.publicClient.readContract({ address: auction, abi: cloakAuctionAbi, functionName: "getBid", args: [l.args.id] }),
|
|
320
|
+
this.publicClient.readContract({ address: auction, abi: cloakAuctionAbi, functionName: "allocationOf", args: [l.args.id] }),
|
|
321
|
+
]);
|
|
322
|
+
return { id: l.args.id, ...bid, tokens: alloc[0], refund: alloc[1], penalty: alloc[2] };
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
// ───────────── keeper side ─────────────
|
|
326
|
+
async committedLogs(auction, fromBlock = 0n) {
|
|
327
|
+
const latest = await this.publicClient.getBlockNumber({ cacheTime: 0 });
|
|
328
|
+
const get = (from, to) => this.publicClient.getContractEvents({
|
|
329
|
+
address: auction,
|
|
330
|
+
abi: cloakAuctionAbi,
|
|
331
|
+
eventName: "Committed",
|
|
332
|
+
strict: true,
|
|
333
|
+
fromBlock: from,
|
|
334
|
+
toBlock: to,
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
return await get(fromBlock, latest);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// RPCs that cap eth_getLogs ranges: walk in chunks
|
|
341
|
+
const out = [];
|
|
342
|
+
const step = 10000n;
|
|
343
|
+
for (let from = fromBlock; from <= latest; from += step) {
|
|
344
|
+
const to = from + step - 1n > latest ? latest : from + step - 1n;
|
|
345
|
+
out.push(...(await get(from, to)));
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Decrypt every sealed bid whose drand round has passed and reveal them in batches. Anyone can run this — it needs no
|
|
352
|
+
* key material, only the public beacon. Bids whose ciphertext is garbage are skipped (and get penalised at claim).
|
|
353
|
+
*/
|
|
354
|
+
async revealAll(auction, opts = {}) {
|
|
355
|
+
const log = opts.log ?? (() => { });
|
|
356
|
+
const logs = await this.committedLogs(auction, opts.fromBlock ?? 0n);
|
|
357
|
+
const reveals = [];
|
|
358
|
+
for (const l of logs) {
|
|
359
|
+
const bid = await this.publicClient.readContract({
|
|
360
|
+
address: auction,
|
|
361
|
+
abi: cloakAuctionAbi,
|
|
362
|
+
functionName: "getBid",
|
|
363
|
+
args: [l.args.id],
|
|
364
|
+
});
|
|
365
|
+
if (bid.revealed)
|
|
366
|
+
continue;
|
|
367
|
+
try {
|
|
368
|
+
const p = await unsealBid(l.args.sealedBid);
|
|
369
|
+
if (p.auction.toLowerCase() !== auction.toLowerCase())
|
|
370
|
+
throw new Error("sealed for another auction");
|
|
371
|
+
const expected = commitmentOf(auction, bid.bidder, p.tick, p.quantity, p.salt);
|
|
372
|
+
if (expected !== bid.commitment)
|
|
373
|
+
throw new Error("ciphertext does not match commitment");
|
|
374
|
+
reveals.push({ id: l.args.id, tick: p.tick, quantity: p.quantity, salt: p.salt });
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
log(`bid #${l.args.id}: cannot unseal (${e.message})`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const size = opts.batchSize ?? 40;
|
|
381
|
+
const txs = [];
|
|
382
|
+
for (let i = 0; i < reveals.length; i += size) {
|
|
383
|
+
const txHash = await this.wallet().writeContract({
|
|
384
|
+
address: auction,
|
|
385
|
+
abi: cloakAuctionAbi,
|
|
386
|
+
functionName: "revealBatch",
|
|
387
|
+
args: [reveals.slice(i, i + size)],
|
|
388
|
+
});
|
|
389
|
+
await this.send(txHash);
|
|
390
|
+
txs.push(txHash);
|
|
391
|
+
log(`revealed ${Math.min(i + size, reveals.length)}/${reveals.length}`);
|
|
392
|
+
}
|
|
393
|
+
return { revealed: reveals.length, txs };
|
|
394
|
+
}
|
|
395
|
+
/** Claim on behalf of every bidder (tokens and refunds always go to the bidder, never the caller). */
|
|
396
|
+
async claimAll(auction, opts = {}) {
|
|
397
|
+
const logs = await this.committedLogs(auction, opts.fromBlock ?? 0n);
|
|
398
|
+
const ids = [];
|
|
399
|
+
for (const l of logs) {
|
|
400
|
+
const bid = await this.publicClient.readContract({
|
|
401
|
+
address: auction,
|
|
402
|
+
abi: cloakAuctionAbi,
|
|
403
|
+
functionName: "getBid",
|
|
404
|
+
args: [l.args.id],
|
|
405
|
+
});
|
|
406
|
+
if (!bid.claimed)
|
|
407
|
+
ids.push(l.args.id);
|
|
408
|
+
}
|
|
409
|
+
const size = opts.batchSize ?? 40;
|
|
410
|
+
const txs = [];
|
|
411
|
+
for (let i = 0; i < ids.length; i += size)
|
|
412
|
+
txs.push(await this.claim(auction, ids.slice(i, i + size)));
|
|
413
|
+
return { claimed: ids.length, txs };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Address } from "viem";
|
|
2
|
+
/** Where Cloak lives on each chain, so integrators never copy addresses by hand. */
|
|
3
|
+
export interface CloakDeployment {
|
|
4
|
+
chainId: number;
|
|
5
|
+
config: Address;
|
|
6
|
+
factory: Address;
|
|
7
|
+
router: Address;
|
|
8
|
+
/** Block the router was deployed in: scan private-pool events from here. */
|
|
9
|
+
routerBlock: bigint;
|
|
10
|
+
batcher: Address;
|
|
11
|
+
migrator?: Address;
|
|
12
|
+
hypePool: Address;
|
|
13
|
+
whype: Address;
|
|
14
|
+
/** Hosted relayer (POST /relay, GET /quote). */
|
|
15
|
+
relayerUrl: string;
|
|
16
|
+
/** Hosted circuit artifacts (transaction2x2.wasm and transaction2x2_final.zkey). */
|
|
17
|
+
circuitsUrl: string;
|
|
18
|
+
/** The first deployment: its launches still list, and its private balances move here through `migrator`. */
|
|
19
|
+
legacy?: {
|
|
20
|
+
factory: Address;
|
|
21
|
+
router: Address;
|
|
22
|
+
routerBlock: bigint;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare const deployments: Record<number, CloakDeployment>;
|
|
26
|
+
/** The Cloak deployment on `chainId`, or an error naming the chains Cloak is on. */
|
|
27
|
+
export declare function deploymentFor(chainId: number): CloakDeployment;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const deployments = {
|
|
2
|
+
99801: {
|
|
3
|
+
chainId: 99801,
|
|
4
|
+
config: "0x9d30c7b23fa2d5c40779d3db3bb4292e18c94373",
|
|
5
|
+
factory: "0xc1fc14e84cbbd10f75dad9d91bf50f47c48757fc",
|
|
6
|
+
router: "0xd51e731467c49991496329c2b7979cb1aa220ee2",
|
|
7
|
+
routerBlock: 15856031n,
|
|
8
|
+
batcher: "0xa13ffd763a8cccfea1b868c616291723579ddb77",
|
|
9
|
+
migrator: "0xe54c02fa685f2c95e5ae98e1155547194d04d9ad",
|
|
10
|
+
hypePool: "0x4deFEC0814d11c581e5947020ac4ec41e88B55Df",
|
|
11
|
+
whype: "0xe44b27113D4b947c0B1bDf4a93878D0e115f4bdD",
|
|
12
|
+
relayerUrl: "https://cloak-keeper.onrender.com",
|
|
13
|
+
circuitsUrl: "https://cloak.boblabs.tech/circuits",
|
|
14
|
+
legacy: {
|
|
15
|
+
factory: "0x1dc1ea9837ecbd31ec74f0d558e1db1c303a7e1c",
|
|
16
|
+
router: "0xfa2574cfc3afb8f7d3a0651fa2111ff4e3d2fc41",
|
|
17
|
+
routerBlock: 10973723n,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
/** The Cloak deployment on `chainId`, or an error naming the chains Cloak is on. */
|
|
22
|
+
export function deploymentFor(chainId) {
|
|
23
|
+
const d = deployments[chainId];
|
|
24
|
+
if (!d)
|
|
25
|
+
throw new Error(`Cloak isn't deployed on chain ${chainId} (available: ${Object.keys(deployments).join(", ")})`);
|
|
26
|
+
return d;
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type Address, type Hex, type PublicClient } from "viem";
|
|
2
|
+
import type { Wallet } from "../cloak.js";
|
|
3
|
+
import { CloakKeys } from "./crypto.js";
|
|
4
|
+
import { MerkleTree } from "./tree.js";
|
|
5
|
+
import type { ProofArgs, Prover } from "./prover.js";
|
|
6
|
+
import { type ExtData, type OwnedNote } from "./tx.js";
|
|
7
|
+
/** A shielded call the relayer submits so the trader's wallet never appears on-chain. */
|
|
8
|
+
export type RelayKind = "privateSwap" | "unshieldHype" | "transact" | "placeOrder" | "migrate";
|
|
9
|
+
export interface RelayCall {
|
|
10
|
+
kind: RelayKind;
|
|
11
|
+
pool: Address;
|
|
12
|
+
proof: ProofArgs;
|
|
13
|
+
extData: ExtData;
|
|
14
|
+
}
|
|
15
|
+
/** What a relayer charges for one call, paid out of the pool's own token inside the proof. */
|
|
16
|
+
export interface RelayQuote {
|
|
17
|
+
relayer: Address;
|
|
18
|
+
fee: bigint;
|
|
19
|
+
}
|
|
20
|
+
export type Submitter = ((call: RelayCall) => Promise<Hex>) & {
|
|
21
|
+
/** The relayer's fee for a call of this kind on this pool. Without it, calls carry no fee. */
|
|
22
|
+
quote?: (kind: RelayKind, pool: Address) => Promise<RelayQuote>;
|
|
23
|
+
};
|
|
24
|
+
export interface PrivateMarket {
|
|
25
|
+
token: Address;
|
|
26
|
+
pool: Address;
|
|
27
|
+
pair: Address | null;
|
|
28
|
+
symbol?: string;
|
|
29
|
+
}
|
|
30
|
+
/** POST shielded calls to a Cloak relayer (`services/keeper` serves `/relay` and `/quote`). */
|
|
31
|
+
export declare function relayerSubmitter(url: string): Submitter;
|
|
32
|
+
/** Submit from a wallet directly — works, but that wallet pays gas and appears as the sender. Use for tests/relayers. */
|
|
33
|
+
export declare function walletSubmitter(publicClient: PublicClient, wallet: Wallet, router: Address, batcher?: Address, migrator?: Address): Submitter;
|
|
34
|
+
export type Status = (msg: string) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Private trading for Cloak launch tokens on Elysium.
|
|
37
|
+
*
|
|
38
|
+
* shieldHype public HYPE → private balance (one-time key, so deposits are unlinkable)
|
|
39
|
+
* buy / sell private balance → launch token / back, via the PrivateRouter and the launch's CloakPair,
|
|
40
|
+
* proven on this device and submitted by a relayer
|
|
41
|
+
* unshield private HYPE → any address
|
|
42
|
+
*/
|
|
43
|
+
export declare class CloakPrivate {
|
|
44
|
+
readonly publicClient: PublicClient;
|
|
45
|
+
readonly router: Address;
|
|
46
|
+
readonly keys: CloakKeys;
|
|
47
|
+
readonly prover: () => Promise<Prover>;
|
|
48
|
+
readonly submit: Submitter;
|
|
49
|
+
readonly fromBlock: bigint;
|
|
50
|
+
readonly batcher?: Address | undefined;
|
|
51
|
+
readonly migrator?: Address | undefined;
|
|
52
|
+
private pools;
|
|
53
|
+
markets: PrivateMarket[];
|
|
54
|
+
constructor(publicClient: PublicClient, router: Address, keys: CloakKeys, prover: () => Promise<Prover>, submit: Submitter, fromBlock?: bigint, batcher?: Address | undefined, migrator?: Address | undefined);
|
|
55
|
+
/**
|
|
56
|
+
* Derive the private keys from one EIP-712 signature bound to this chain and router, so a signature asked for by any
|
|
57
|
+
* other site or contract can't reproduce them. With `verify`, the wallet signs twice and the keys are refused if the
|
|
58
|
+
* two signatures differ (some smart and MPC wallets sign differently each time, which would hide earlier notes).
|
|
59
|
+
*/
|
|
60
|
+
static keysFromWallet(wallet: Wallet, opts: {
|
|
61
|
+
chainId: number;
|
|
62
|
+
router: Address;
|
|
63
|
+
verify?: boolean;
|
|
64
|
+
}): Promise<CloakKeys>;
|
|
65
|
+
/** Keys of the first Cloak deployment (plain signed message). Only needed to move an old balance across. */
|
|
66
|
+
static legacyKeysFromWallet(wallet: Wallet): Promise<CloakKeys>;
|
|
67
|
+
loadMarkets(): Promise<PrivateMarket[]>;
|
|
68
|
+
get hypeMarket(): PrivateMarket;
|
|
69
|
+
market(token: Address): PrivateMarket;
|
|
70
|
+
private state;
|
|
71
|
+
/** Pull new leaves from the pool's events, trial-decrypt them, and refresh spent flags. */
|
|
72
|
+
sync(pool: Address): Promise<OwnedNote[]>;
|
|
73
|
+
balance(pool: Address): Promise<bigint>;
|
|
74
|
+
/** The most a single relayed call of this kind can move from `pool`: the balance minus the relayer's fee. */
|
|
75
|
+
maxSpendable(pool: Address, kind?: RelayKind): Promise<bigint>;
|
|
76
|
+
/** The Merkle tree of one epoch (default: the epoch the given notes live in, else the newest). */
|
|
77
|
+
private tree;
|
|
78
|
+
treeFor(pool: Address, inputs: OwnedNote[]): Promise<MerkleTree>;
|
|
79
|
+
/** The relayer's fee for this call (0 when submitting from a wallet). */
|
|
80
|
+
relayFee(kind: RelayKind, pool: Address): Promise<RelayQuote>;
|
|
81
|
+
/** What a withdrawal of `amount` from `pool` delivers after the protocol's withdrawal fee. */
|
|
82
|
+
afterFee(pool: Address, amount: bigint): Promise<bigint>;
|
|
83
|
+
private changeOutput;
|
|
84
|
+
dummy(): Promise<{
|
|
85
|
+
dummyPubKey: bigint;
|
|
86
|
+
dummyMemo: (b: bigint) => `0x${string}`;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Get 1–2 notes (from one tree epoch) covering `target`, merging notes privately first when the balance is spread
|
|
90
|
+
* across many. Each merge is a relayed transaction and pays the relayer's fee from the merged notes.
|
|
91
|
+
*/
|
|
92
|
+
private inputsFor;
|
|
93
|
+
/** Public HYPE → private balance. This one step is public (your wallet deposits); everything after is not. */
|
|
94
|
+
shieldHype(wallet: Wallet, amount: bigint): Promise<Hex>;
|
|
95
|
+
roundInfo(): Promise<{
|
|
96
|
+
round: bigint;
|
|
97
|
+
endsAt: number;
|
|
98
|
+
roundSecs: number;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* Batched private buy/sell. The amount is split into standard-size orders (1, 2, 5 × 10ⁿ), each placed from the
|
|
102
|
+
* private balance by the relayer into the current round. When the round ends, buys and sells cross at the pool's
|
|
103
|
+
* mid-price and only the net trades through the pool; outputs arrive as private notes. Observers can't tell how
|
|
104
|
+
* much any one trader moved. Any remainder that isn't a standard size stays in your private balance.
|
|
105
|
+
*/
|
|
106
|
+
batch(token: Address, side: "buy" | "sell", amount: bigint, opts?: {
|
|
107
|
+
maxOrders?: number;
|
|
108
|
+
onStatus?: Status;
|
|
109
|
+
}): Promise<{
|
|
110
|
+
orders: {
|
|
111
|
+
id: bigint;
|
|
112
|
+
size: bigint;
|
|
113
|
+
round: bigint;
|
|
114
|
+
txHash: Hex;
|
|
115
|
+
}[];
|
|
116
|
+
rest: bigint;
|
|
117
|
+
}>;
|
|
118
|
+
quote(token: Address, side: "buy" | "sell", amountIn: bigint): Promise<bigint>;
|
|
119
|
+
/** Private buy: spend private HYPE, receive private tokens. `slippageBps` defaults to 1%. */
|
|
120
|
+
buy(token: Address, hypeIn: bigint, opts?: {
|
|
121
|
+
slippageBps?: number;
|
|
122
|
+
onStatus?: Status;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
txHash: `0x${string}`;
|
|
125
|
+
expectedOut: bigint;
|
|
126
|
+
minOut: bigint;
|
|
127
|
+
relayerFee: bigint;
|
|
128
|
+
}>;
|
|
129
|
+
/** Private sell: spend private tokens, receive private HYPE. */
|
|
130
|
+
sell(token: Address, tokensIn: bigint, opts?: {
|
|
131
|
+
slippageBps?: number;
|
|
132
|
+
onStatus?: Status;
|
|
133
|
+
}): Promise<{
|
|
134
|
+
txHash: `0x${string}`;
|
|
135
|
+
expectedOut: bigint;
|
|
136
|
+
minOut: bigint;
|
|
137
|
+
relayerFee: bigint;
|
|
138
|
+
}>;
|
|
139
|
+
private swap;
|
|
140
|
+
/** Private HYPE → native HYPE at `to` (use a fresh address to stay unlinkable). */
|
|
141
|
+
unshieldHype(to: Address, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
142
|
+
/** Your receiving address for private transfers. */
|
|
143
|
+
address(): Promise<string>;
|
|
144
|
+
/**
|
|
145
|
+
* Private transfer inside a pool: sender, recipient and amount all stay hidden.
|
|
146
|
+
* `token` = a launch token, or the WHYPE address / hypeMarket.token for HYPE.
|
|
147
|
+
*/
|
|
148
|
+
send(token: Address, to: string, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
149
|
+
/** Private launch tokens → any address. */
|
|
150
|
+
unshieldToken(token: Address, to: Address, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
151
|
+
/**
|
|
152
|
+
* Move every private balance `legacy` (a client on the earlier router, with the earlier keys) can see into this
|
|
153
|
+
* deployment, note by note, through `CloakMigrator`. Each move is one relayed transaction: the old proof withdraws to
|
|
154
|
+
* the migrator, which deposits the same amount here as a new note for one of this account's one-time keys.
|
|
155
|
+
*/
|
|
156
|
+
migrateFrom(legacy: CloakPrivate, onStatus?: Status): Promise<{
|
|
157
|
+
token: Address;
|
|
158
|
+
amount: bigint;
|
|
159
|
+
txHash: Hex;
|
|
160
|
+
}[]>;
|
|
161
|
+
private migratorAddress;
|
|
162
|
+
}
|