@cloakely/sdk 0.6.0 → 0.7.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.
@@ -0,0 +1,459 @@
1
+ import { encodeAbiParameters, formatEther, keccak256, zeroAddress } from "viem";
2
+ import { cloakAuctionAbi, cloakPairAbi, sharedRouterAbi, sharedVaultAbi } from "../abis.js";
3
+ import { Cloak } from "../cloak.js";
4
+ import { MAIN_KEY, encryptFullMemo, encryptRouterMemo, noteCommitment, nullifierOf, parseCloakAddress, randomField, randomKeyIndex, readMemo } from "./crypto.js";
5
+ import { MerkleTree } from "./tree.js";
6
+ import { buildTransaction, selectNotes } from "./tx.js";
7
+ /**
8
+ * The shared launch-token vault (testnet experiment): one private vault for every Cloak launch token that opens on
9
+ * Uniswap from now on, next to the single-token vaults of HYPE and USDC.
10
+ *
11
+ * shield wallet tokens → private note (one-time key)
12
+ * buy private HYPE → private token, through the launch's Uniswap pool
13
+ * sell private token → private HYPE
14
+ * send private token → any cloak: address, and nobody can tell WHICH token moved: the vault sees a send with no
15
+ * fee, and the relayer is paid from the sender's private HYPE in the same transaction
16
+ * unshield private token → any address
17
+ *
18
+ * Each note holds one token (Poseidon(amount, pubKey, blinding, token)), and every proof spends and creates notes of a
19
+ * single token only. Uses the account's keys, HYPE vault and relayer from `CloakPrivate`.
20
+ */
21
+ export const SHARED_SWAP_DATA_ABI = [
22
+ {
23
+ type: "tuple",
24
+ components: [
25
+ { name: "token", type: "address" },
26
+ { name: "minOut", type: "uint256" },
27
+ { name: "outPubKey", type: "uint256" },
28
+ { name: "outBlinding", type: "uint256" },
29
+ { name: "outMemo", type: "bytes" },
30
+ { name: "deadline", type: "uint64" },
31
+ ],
32
+ },
33
+ ];
34
+ const json = (v) => JSON.stringify(v, (_, x) => (typeof x === "bigint" ? x.toString() : x));
35
+ /** POST shared-vault calls to a Cloak relayer (`services/keeper` serves `/relay-shared` and `/quote-shared`). */
36
+ export function sharedRelayerSubmitter(url) {
37
+ const base = url.replace(/\/$/, "");
38
+ const submit = async (call) => {
39
+ const res = await fetch(`${base}/relay-shared`, { method: "POST", headers: { "content-type": "application/json" }, body: json(call) });
40
+ const out = await res.json().catch(() => ({}));
41
+ if (!res.ok)
42
+ throw new Error(out.error ?? `relay failed (${res.status})`);
43
+ return out.txHash;
44
+ };
45
+ submit.quote = async (kind, token) => {
46
+ const res = await fetch(`${base}/quote-shared?kind=${kind}&token=${token}`);
47
+ const out = await res.json().catch(() => ({}));
48
+ if (!res.ok)
49
+ throw new Error(out.error ?? `relayer quote failed (${res.status})`);
50
+ return { relayer: out.relayer, fee: BigInt(out.fee) };
51
+ };
52
+ return submit;
53
+ }
54
+ /** Submit shared-vault calls from a wallet (that wallet pays gas and appears as the sender). For tests and relayers. */
55
+ export function sharedWalletSubmitter(priv, wallet, vault, router) {
56
+ return async (call) => {
57
+ const req = call.kind === "sharedTransact"
58
+ ? { address: vault, abi: sharedVaultAbi, functionName: "transact", args: [call.proof, call.extData] }
59
+ : call.kind === "sharedBuy"
60
+ ? { address: router, abi: sharedRouterAbi, functionName: "buy", args: [call.proof, call.extData] }
61
+ : call.kind === "sharedSell"
62
+ ? { address: router, abi: sharedRouterAbi, functionName: "sell", args: [call.proof, call.extData] }
63
+ : { address: router, abi: sharedRouterAbi, functionName: "send", args: [call.proof, call.extData, call.feeProof, call.feeExtData] };
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ const est = await priv.publicClient.estimateContractGas({ ...req, account: wallet.account.address });
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ const hash = await wallet.writeContract({ ...req, gas: (est * 13n) / 10n + 100000n });
68
+ const r = await priv.publicClient.waitForTransactionReceipt({ hash });
69
+ if (r.status !== "success")
70
+ throw new Error(`shared-vault call reverted: ${hash}`);
71
+ return hash;
72
+ };
73
+ }
74
+ const ERC20 = [
75
+ { type: "function", name: "allowance", stateMutability: "view", inputs: [{ type: "address" }, { type: "address" }], outputs: [{ type: "uint256" }] },
76
+ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
77
+ { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
78
+ { type: "function", name: "symbol", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
79
+ ];
80
+ const EPOCH = 2 ** 20;
81
+ const asNum = (t) => BigInt(t);
82
+ export class SharedVaultClient {
83
+ priv;
84
+ vault;
85
+ router;
86
+ prover;
87
+ submit;
88
+ fromBlock;
89
+ tokens = [];
90
+ leaves = [];
91
+ scannedUpTo = 0;
92
+ notes = [];
93
+ nextBlock;
94
+ constructor(priv, vault, router, prover, submit, fromBlock = 0n) {
95
+ this.priv = priv;
96
+ this.vault = vault;
97
+ this.router = router;
98
+ this.prover = prover;
99
+ this.submit = submit;
100
+ this.fromBlock = fromBlock;
101
+ this.nextBlock = fromBlock;
102
+ }
103
+ get pub() {
104
+ return this.priv.publicClient;
105
+ }
106
+ /** Every token admitted to the vault, with its Uniswap market. */
107
+ async loadTokens() {
108
+ const c = { address: this.vault, abi: sharedVaultAbi };
109
+ const n = Number(await this.pub.readContract({ ...c, functionName: "tokensCount" }));
110
+ for (let i = this.tokens.length; i < n; i++) {
111
+ const token = await this.pub.readContract({ ...c, functionName: "tokens", args: [BigInt(i)] });
112
+ const [market, symbol] = await Promise.all([
113
+ this.pub.readContract({ ...c, functionName: "marketOf", args: [token] }),
114
+ this.pub.readContract({ address: token, abi: ERC20, functionName: "symbol" }).catch(() => "?"),
115
+ ]);
116
+ this.tokens.push({ token, market, symbol });
117
+ }
118
+ return this.tokens;
119
+ }
120
+ /** Whether `token` is in the shared vault (reads the vault's list if it isn't known yet). */
121
+ async isAdmitted(token) {
122
+ const has = () => this.tokens.some((t) => t.token.toLowerCase() === token.toLowerCase());
123
+ if (!has())
124
+ await this.loadTokens();
125
+ return has();
126
+ }
127
+ info(token) {
128
+ const t = this.tokens.find((x) => x.token.toLowerCase() === token.toLowerCase());
129
+ if (!t)
130
+ throw new Error("this token isn't in the shared vault");
131
+ return t;
132
+ }
133
+ /** Let a settled Uniswap launch's token into the vault (anyone can; costs gas). */
134
+ async admit(wallet, factory, launchIndex) {
135
+ const h = await wallet.writeContract({ address: this.vault, abi: sharedVaultAbi, functionName: "admit", args: [factory, launchIndex], account: wallet.account, chain: wallet.chain });
136
+ const r = await this.pub.waitForTransactionReceipt({ hash: h });
137
+ if (r.status !== "success")
138
+ throw new Error("admit reverted");
139
+ await this.loadTokens();
140
+ return h;
141
+ }
142
+ /** Pull new notes, find which are ours and which token each holds, refresh spent flags. */
143
+ async sync() {
144
+ if (!this.tokens.length)
145
+ await this.loadTokens();
146
+ const latest = await this.pub.getBlockNumber({ cacheTime: 0 });
147
+ if (latest >= this.nextBlock) {
148
+ const logs = await this.pub.getContractEvents({ address: this.vault, abi: sharedVaultAbi, eventName: "NewCommitment", fromBlock: this.nextBlock, toBlock: latest, strict: true });
149
+ const known = new Set(this.leaves.map((l) => l.index));
150
+ for (const l of logs) {
151
+ const index = Number(l.args.index);
152
+ if (!known.has(index))
153
+ this.leaves.push({ index, commitment: l.args.commitment, memo: l.args.encryptedOutput });
154
+ }
155
+ this.leaves.sort((a, b) => a.index - b.index);
156
+ this.nextBlock = latest + 1n;
157
+ }
158
+ let n = 0;
159
+ while (n < this.leaves.length && this.leaves[n].index === n)
160
+ n++;
161
+ let reloaded = false;
162
+ for (const l of this.leaves.slice(this.scannedUpTo, n)) {
163
+ const m = readMemo(this.priv.keys.encPriv, l.memo);
164
+ if (!m || m.amount === 0n)
165
+ continue;
166
+ const k = await this.priv.keys.key(m.keyIndex);
167
+ const base = { amount: m.amount, pubKey: k.pubKey, blinding: m.blinding };
168
+ // the memo doesn't say which token: the commitment does, so try each admitted token
169
+ let asset;
170
+ for (let pass = 0; pass < 2 && asset === undefined; pass++) {
171
+ for (const t of this.tokens)
172
+ if ((await noteCommitment({ ...base, asset: asNum(t.token) })) === l.commitment) {
173
+ asset = asNum(t.token);
174
+ break;
175
+ }
176
+ if (asset === undefined && !reloaded) {
177
+ await this.loadTokens();
178
+ reloaded = true;
179
+ }
180
+ else
181
+ break;
182
+ }
183
+ if (asset === undefined)
184
+ continue; // not ours, or a forged memo
185
+ const note = { ...base, asset };
186
+ const pos = l.index % EPOCH;
187
+ this.notes.push({ ...note, leafIndex: pos, epoch: Math.floor(l.index / EPOCH), keyIndex: m.keyIndex, privKey: k.privKey, commitment: l.commitment, nullifier: await nullifierOf(note, pos, k.privKey), spent: false });
188
+ }
189
+ this.scannedUpTo = n;
190
+ const flags = await Promise.all(this.notes.map((x) => (x.spent ? true : this.pub.readContract({ address: this.vault, abi: sharedVaultAbi, functionName: "isSpent", args: [x.nullifier] }))));
191
+ this.notes.forEach((x, i) => (x.spent = Boolean(flags[i])));
192
+ return this.notes.filter((x) => !x.spent);
193
+ }
194
+ /** Private balance of `token` in the shared vault. */
195
+ async balance(token) {
196
+ const t = asNum(token);
197
+ return (await this.sync()).filter((x) => x.asset === t).reduce((a, x) => a + x.amount, 0n);
198
+ }
199
+ /** Every token this account holds privately in the shared vault. */
200
+ async balances() {
201
+ const live = await this.sync();
202
+ return this.tokens.map((t) => ({ token: t.token, symbol: t.symbol, amount: live.filter((x) => x.asset === asNum(t.token)).reduce((a, x) => a + x.amount, 0n) })).filter((x) => x.amount > 0n);
203
+ }
204
+ /** The most one withdrawal (or sell) of `token` can take: the balance minus the relayer's fee (with a 25% cushion). */
205
+ async maxWithdraw(token, kind = "sharedTransact") {
206
+ const [bal, q] = await Promise.all([this.balance(token), this.quote(kind, token)]);
207
+ const max = bal - (q.fee * 5n) / 4n;
208
+ return max > 0n ? max : 0n;
209
+ }
210
+ async tree(epoch) {
211
+ const t = new MerkleTree();
212
+ for (const l of this.leaves.slice(0, this.scannedUpTo))
213
+ if (Math.floor(l.index / EPOCH) === epoch)
214
+ await t.insert(l.commitment);
215
+ return t;
216
+ }
217
+ async quote(kind, token) {
218
+ return this.submit.quote ? this.submit.quote(kind, token) : { relayer: zeroAddress, fee: 0n };
219
+ }
220
+ /** 1–2 notes of `token` covering `target`, combining notes first (a relayed send to yourself) when needed. */
221
+ async inputsFor(token, target, onStatus) {
222
+ const asset = asNum(token);
223
+ for (let round = 0; round < 6; round++) {
224
+ const live = (await this.sync()).filter((x) => x.asset === asset);
225
+ const epoch = live.length ? Math.max(...live.map((x) => x.epoch ?? 0)) : 0;
226
+ const mine = live.filter((x) => (x.epoch ?? 0) === epoch);
227
+ const sum = mine.reduce((a, x) => a + x.amount, 0n);
228
+ if (sum < target)
229
+ throw new Error("insufficient private balance");
230
+ const pick = selectNotes(mine, target);
231
+ if ("inputs" in pick)
232
+ return pick.inputs;
233
+ onStatus("Combining private notes first…");
234
+ const q = await this.quote("sharedTransact", token);
235
+ const total = pick.merge.reduce((a, x) => a + x.amount, 0n) - q.fee;
236
+ const { proof, extData } = await buildTransaction({
237
+ tree: await this.tree(epoch),
238
+ inputs: pick.merge,
239
+ outputs: [await this.priv.changeOutput(total)],
240
+ fee: q.fee,
241
+ relayer: q.relayer,
242
+ asset,
243
+ publicAsset: asset,
244
+ ...(await this.priv.dummy()),
245
+ prover: await this.prover(),
246
+ });
247
+ await this.pub.waitForTransactionReceipt({ hash: await this.submit({ kind: "sharedTransact", proof, extData }) });
248
+ }
249
+ throw new Error("too many notes to combine");
250
+ }
251
+ /** Expected output of a swap through `token`'s Uniswap market (0.3% fee), before slippage. */
252
+ async quoteSwap(token, side, amountIn) {
253
+ const m = this.info(token).market;
254
+ const [rH, rT] = await Promise.all([
255
+ this.pub.readContract({ address: m, abi: cloakPairAbi, functionName: "reserveHype" }),
256
+ this.pub.readContract({ address: m, abi: cloakPairAbi, functionName: "reserveToken" }),
257
+ ]);
258
+ const [rIn, rOut] = side === "buy" ? [rH, rT] : [rT, rH];
259
+ const a = amountIn * 997n;
260
+ return (a * rOut) / (rIn * 1000n + a);
261
+ }
262
+ // ───────────── public → private ─────────────
263
+ /** Wallet tokens → a private note on a one-time key. This step is public (the wallet deposits). */
264
+ async shield(wallet, token, amount, onStatus = () => { }) {
265
+ if (amount <= 0n)
266
+ throw new Error("amount must be positive");
267
+ if (!this.tokens.length)
268
+ await this.loadTokens();
269
+ this.info(token);
270
+ const owner = wallet.account.address;
271
+ const held = await this.pub.readContract({ address: token, abi: ERC20, functionName: "balanceOf", args: [owner] });
272
+ if (held < amount)
273
+ throw new Error(`insufficient token balance in wallet: has ${held}, needs ${amount}`);
274
+ const allowance = await this.pub.readContract({ address: token, abi: ERC20, functionName: "allowance", args: [owner, this.vault] });
275
+ if (allowance < amount) {
276
+ onStatus("Approve the deposit in your wallet…");
277
+ const a = await wallet.writeContract({ address: token, abi: ERC20, functionName: "approve", args: [this.vault, amount], account: wallet.account, chain: wallet.chain });
278
+ if ((await this.pub.waitForTransactionReceipt({ hash: a })).status !== "success")
279
+ throw new Error("approve reverted");
280
+ }
281
+ const keyIndex = randomKeyIndex();
282
+ const k = await this.priv.keys.key(keyIndex);
283
+ const blinding = randomField();
284
+ onStatus("Confirm the deposit in your wallet…");
285
+ const hash = await wallet.writeContract({
286
+ address: this.vault,
287
+ abi: sharedVaultAbi,
288
+ functionName: "depositTo",
289
+ args: [token, k.pubKey, amount, blinding, encryptFullMemo(this.priv.keys.encPub, amount, blinding, keyIndex)],
290
+ account: wallet.account,
291
+ chain: wallet.chain,
292
+ });
293
+ if ((await this.pub.waitForTransactionReceipt({ hash })).status !== "success")
294
+ throw new Error("shield reverted");
295
+ return hash;
296
+ }
297
+ /** Claim won bids of a shared-vault launch and move the tokens in. Two public steps from the bidder's wallet. */
298
+ async claimInto(wallet, auction, bidIds, onStatus = () => { }) {
299
+ const c = new Cloak(this.pub, wallet);
300
+ const s = await c.getAuction(auction);
301
+ let tokens = 0n;
302
+ for (const id of bidIds)
303
+ tokens += (await this.pub.readContract({ address: auction, abi: cloakAuctionAbi, functionName: "allocationOf", args: [id] }))[0];
304
+ onStatus("Claiming…");
305
+ await c.claim(auction, bidIds);
306
+ if (tokens > 0n)
307
+ await this.shield(wallet, s.token, tokens, onStatus);
308
+ return { tokens };
309
+ }
310
+ // ───────────── private trades ─────────────
311
+ /** Private HYPE → private `token`. The HYPE side, the swap and the output note are proven here, relayed elsewhere. */
312
+ async buy(token, hypeIn, opts = {}) {
313
+ const onStatus = opts.onStatus ?? (() => { });
314
+ if (!this.tokens.length)
315
+ await this.loadTokens();
316
+ this.info(token);
317
+ const hypePool = this.priv.hypeMarket.pool;
318
+ const q = await this.quote("sharedBuy", token);
319
+ const inputs = await this.priv.inputsFor(hypePool, hypeIn + q.fee, onStatus);
320
+ const arrives = await this.priv.afterFee(hypePool, hypeIn);
321
+ const minOut = ((await this.quoteSwap(token, "buy", arrives)) * BigInt(10_000 - (opts.slippageBps ?? 200))) / 10000n;
322
+ const keyIndex = randomKeyIndex();
323
+ const k = await this.priv.keys.key(keyIndex);
324
+ const outBlinding = randomField();
325
+ const data = encodeAbiParameters(SHARED_SWAP_DATA_ABI, [
326
+ { token, minOut, outPubKey: k.pubKey, outBlinding, outMemo: encryptRouterMemo(this.priv.keys.encPub, outBlinding, keyIndex), deadline: BigInt(Math.floor(Date.now() / 1000) + 600) },
327
+ ]);
328
+ const change = inputs.reduce((a, x) => a + x.amount, 0n) - hypeIn - q.fee;
329
+ onStatus("Proving on this device…");
330
+ const { proof, extData } = await buildTransaction({
331
+ tree: await this.priv.treeFor(hypePool, inputs),
332
+ inputs,
333
+ outputs: change > 0n ? [await this.priv.changeOutput(change)] : [],
334
+ extAmount: -hypeIn,
335
+ fee: q.fee,
336
+ relayer: q.relayer,
337
+ recipient: this.router,
338
+ data,
339
+ ...(await this.priv.dummy()),
340
+ prover: await this.priv.prover(),
341
+ });
342
+ onStatus("Relaying…");
343
+ return this.submit({ kind: "sharedBuy", proof, extData });
344
+ }
345
+ /** Private `token` → private HYPE. */
346
+ async sell(token, amount, opts = {}) {
347
+ const onStatus = opts.onStatus ?? (() => { });
348
+ if (!this.tokens.length)
349
+ await this.loadTokens();
350
+ this.info(token);
351
+ const asset = asNum(token);
352
+ const q = await this.quote("sharedSell", token);
353
+ const inputs = await this.inputsFor(token, amount + q.fee, onStatus);
354
+ const arrives = await this.pub.readContract({ address: this.vault, abi: sharedVaultAbi, functionName: "afterFee", args: [amount] });
355
+ const minOut = ((await this.quoteSwap(token, "sell", arrives)) * BigInt(10_000 - (opts.slippageBps ?? 200))) / 10000n;
356
+ const keyIndex = randomKeyIndex();
357
+ const k = await this.priv.keys.key(keyIndex);
358
+ const outBlinding = randomField();
359
+ const data = encodeAbiParameters(SHARED_SWAP_DATA_ABI, [
360
+ { token, minOut, outPubKey: k.pubKey, outBlinding, outMemo: encryptRouterMemo(this.priv.keys.encPub, outBlinding, keyIndex), deadline: BigInt(Math.floor(Date.now() / 1000) + 600) },
361
+ ]);
362
+ const change = inputs.reduce((a, x) => a + x.amount, 0n) - amount - q.fee;
363
+ onStatus("Proving on this device…");
364
+ const { proof, extData } = await buildTransaction({
365
+ tree: await this.tree(inputs[0].epoch ?? 0),
366
+ inputs,
367
+ outputs: change > 0n ? [await this.priv.changeOutput(change)] : [],
368
+ extAmount: -amount,
369
+ fee: q.fee,
370
+ relayer: q.relayer,
371
+ recipient: this.router,
372
+ data,
373
+ asset,
374
+ publicAsset: asset,
375
+ ...(await this.priv.dummy()),
376
+ prover: await this.prover(),
377
+ });
378
+ onStatus("Relaying…");
379
+ return this.submit({ kind: "sharedSell", proof, extData });
380
+ }
381
+ /**
382
+ * Send `amount` of `token` to a cloak: address without revealing which token: the shared-vault proof names no token
383
+ * and pays no fee, and a second proof pays the relayer from this account's private HYPE. Needs some private HYPE.
384
+ */
385
+ async send(token, amount, to, onStatus = () => { }) {
386
+ if (amount <= 0n)
387
+ throw new Error("amount must be positive");
388
+ if (!this.tokens.length)
389
+ await this.loadTokens();
390
+ this.info(token);
391
+ const asset = asNum(token);
392
+ const dest = parseCloakAddress(to);
393
+ const hypePool = this.priv.hypeMarket.pool;
394
+ const q = await this.quote("sharedSend", zeroAddress); // the relayer is never told which token a hidden send moves
395
+ const hypeHeld = await this.priv.balance(hypePool);
396
+ if (hypeHeld < q.fee) {
397
+ throw new Error(`a hidden send pays its relayer from your private HYPE, so you need about ${formatEther(q.fee)} HYPE in your private balance (you have ${formatEther(hypeHeld)})`);
398
+ }
399
+ const inputs = await this.inputsFor(token, amount, onStatus);
400
+ const feeInputs = await this.priv.inputsFor(hypePool, q.fee, onStatus);
401
+ const change = inputs.reduce((a, x) => a + x.amount, 0n) - amount;
402
+ onStatus("Proving the send on this device…");
403
+ const send = await buildTransaction({
404
+ tree: await this.tree(inputs[0].epoch ?? 0),
405
+ inputs,
406
+ outputs: [
407
+ { amount, pubKey: dest.pubKey, memo: (b) => encryptFullMemo(dest.encPub, amount, b, MAIN_KEY) },
408
+ ...(change > 0n ? [await this.priv.changeOutput(change)] : []),
409
+ ],
410
+ asset,
411
+ publicAsset: 0n,
412
+ ...(await this.priv.dummy()),
413
+ prover: await this.prover(),
414
+ });
415
+ const fingerprint = keccak256(encodeAbiParameters([{ type: "uint256[2]" }, { type: "uint256[2]" }], [send.proof.inputNullifiers, send.proof.outputCommitments]));
416
+ const feeChange = feeInputs.reduce((a, x) => a + x.amount, 0n) - q.fee;
417
+ onStatus("Proving the relayer's fee…");
418
+ const fee = await buildTransaction({
419
+ tree: await this.priv.treeFor(hypePool, feeInputs),
420
+ inputs: feeInputs,
421
+ outputs: feeChange > 0n ? [await this.priv.changeOutput(feeChange)] : [],
422
+ extAmount: -q.fee,
423
+ recipient: this.router,
424
+ data: encodeAbiParameters([{ type: "bytes32" }], [fingerprint]),
425
+ ...(await this.priv.dummy()),
426
+ prover: await this.priv.prover(),
427
+ });
428
+ onStatus("Relaying…");
429
+ return this.submit({ kind: "sharedSend", proof: send.proof, extData: send.extData, feeProof: fee.proof, feeExtData: fee.extData });
430
+ }
431
+ /** Private `token` → `to` (any address). Names the token, as any withdrawal must. */
432
+ async unshield(token, amount, to, onStatus = () => { }) {
433
+ if (amount <= 0n)
434
+ throw new Error("amount must be positive");
435
+ if (!this.tokens.length)
436
+ await this.loadTokens();
437
+ this.info(token);
438
+ const asset = asNum(token);
439
+ const q = await this.quote("sharedTransact", token);
440
+ const inputs = await this.inputsFor(token, amount + q.fee, onStatus);
441
+ const change = inputs.reduce((a, x) => a + x.amount, 0n) - amount - q.fee;
442
+ onStatus("Proving on this device…");
443
+ const { proof, extData } = await buildTransaction({
444
+ tree: await this.tree(inputs[0].epoch ?? 0),
445
+ inputs,
446
+ outputs: change > 0n ? [await this.priv.changeOutput(change)] : [],
447
+ extAmount: -amount,
448
+ fee: q.fee,
449
+ relayer: q.relayer,
450
+ recipient: to,
451
+ asset,
452
+ publicAsset: asset,
453
+ ...(await this.priv.dummy()),
454
+ prover: await this.prover(),
455
+ });
456
+ onStatus("Relaying…");
457
+ return this.submit({ kind: "sharedTransact", proof, extData });
458
+ }
459
+ }
@@ -22,6 +22,8 @@ export interface OwnedNote {
22
22
  commitment: bigint;
23
23
  nullifier: bigint;
24
24
  spent: boolean;
25
+ /** Shared vault only: which token this note holds (its address as a number). */
26
+ asset?: bigint;
25
27
  }
26
28
  export interface OutputNote {
27
29
  amount: bigint;
@@ -109,6 +111,9 @@ export declare function buildTransaction(p: {
109
111
  dummyPubKey: bigint;
110
112
  dummyMemo: (blinding: bigint) => Hex;
111
113
  prover: Prover;
114
+ /** Shared vault only: the token every note of this transaction holds, and whether the proof names it (0 hides it). */
115
+ asset?: bigint;
116
+ publicAsset?: bigint;
112
117
  }): Promise<{
113
118
  proof: ProofArgs;
114
119
  extData: ExtData;
@@ -84,6 +84,11 @@ export async function buildTransaction(p) {
84
84
  const privKey = randomField();
85
85
  ins.push({ amount: 0n, privKey, pubKey: await poseidon([privKey]), blinding: randomField(), leafIndex: 0 });
86
86
  }
87
+ const shared = p.asset !== undefined;
88
+ if (shared && p.inputs.some((n) => n.asset !== p.asset))
89
+ throw new Error("every input must hold the same token");
90
+ if (shared && (extAmount !== 0n || fee !== 0n) && p.publicAsset !== p.asset)
91
+ throw new Error("moving tokens in or out must name the token");
87
92
  const outs = p.outputs.map((o) => ({ ...o, blinding: o.blinding ?? randomField() }));
88
93
  while (outs.length < 2)
89
94
  outs.push({ amount: 0n, pubKey: p.dummyPubKey, blinding: randomField(), memo: p.dummyMemo });
@@ -119,8 +124,9 @@ export async function buildTransaction(p) {
119
124
  root,
120
125
  publicAmount: fieldMod(extAmount - fee),
121
126
  extDataHash: hashExtData(extData),
122
- inputNullifier: await Promise.all(ins.map((n) => nullifierOf(n, n.leafIndex, n.privKey))),
123
- outputCommitment: await Promise.all(outs.map((o) => noteCommitment(o))),
127
+ ...(shared ? { publicAsset: p.publicAsset ?? 0n, asset: p.asset } : {}),
128
+ inputNullifier: await Promise.all(ins.map((n) => nullifierOf({ ...n, asset: p.asset }, n.leafIndex, n.privKey))),
129
+ outputCommitment: await Promise.all(outs.map((o) => noteCommitment({ ...o, asset: p.asset }))),
124
130
  inAmount: ins.map((n) => n.amount),
125
131
  inPrivateKey: ins.map((n) => n.privKey),
126
132
  inBlinding: ins.map((n) => n.blinding),
@@ -37,6 +37,11 @@ export interface CloakRegistry {
37
37
  batcher?: Address;
38
38
  batcherBlock?: bigint;
39
39
  migrator?: Address;
40
+ /** SharedVault: one private vault for every new Uniswap launch token (testnet experiment). */
41
+ shared?: Address;
42
+ sharedBlock?: bigint;
43
+ /** SharedRouter: private buys, sells and hidden sends for the shared vault. */
44
+ sharedRouter?: Address;
40
45
  factory: Address;
41
46
  factoryBlock: bigint;
42
47
  /** Earlier factories of the current version (a factory added next to the first), newest first. */
package/dist/registry.js CHANGED
@@ -32,6 +32,9 @@ export async function readRegistry(publicClient, config) {
32
32
  batcher: batcher?.addr,
33
33
  batcherBlock: batcher?.startBlock,
34
34
  migrator: newest("migrator", router.version)?.addr,
35
+ shared: newest("shared")?.addr,
36
+ sharedBlock: newest("shared")?.startBlock,
37
+ sharedRouter: newest("sharedRouter")?.addr,
35
38
  factory: factory.addr,
36
39
  factoryBlock: factory.startBlock,
37
40
  earlierFactories: entries.filter((e) => e.kind === "factory" && e.version === factory.version && e.addr !== factory.addr).reverse().map((e) => e.addr),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloakely/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
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.",
5
5
  "keywords": [
6
6
  "cloak",