@cloakely/sdk 0.0.0-stage → 0.2.1
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 +75 -4
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { encodeAbiParameters, parseEventLogs, zeroAddress } from "viem";
|
|
2
|
+
import { cloakMigratorAbi, cloakPairAbi, privateBatcherAbi, privateRouterAbi, shieldedPoolAbi } from "../abis.js";
|
|
3
|
+
import { ACCOUNT_MESSAGE, ACCOUNT_TYPED_DATA, CloakKeys, MAIN_KEY, noteCommitment, encryptFullMemo, encryptRouterMemo, formatCloakAddress, nullifierOf, parseCloakAddress, randomField, randomKeyIndex, readMemo, } from "./crypto.js";
|
|
4
|
+
import { MerkleTree } from "./tree.js";
|
|
5
|
+
import { MOVE_DATA_ABI, ORDER_DATA_ABI, SWAP_DATA_ABI, buildTransaction, selectNotes, splitStandard } from "./tx.js";
|
|
6
|
+
/** Leaf positions per tree epoch (a pool starts a new tree after 2^20 leaves). */
|
|
7
|
+
const EPOCH_BITS = 20;
|
|
8
|
+
const epochOf = (globalIndex) => Math.floor(globalIndex / 2 ** EPOCH_BITS);
|
|
9
|
+
const positionOf = (globalIndex) => globalIndex % 2 ** EPOCH_BITS;
|
|
10
|
+
const json = (v) => JSON.stringify(v, (_, x) => (typeof x === "bigint" ? x.toString() : x));
|
|
11
|
+
/** POST shielded calls to a Cloak relayer (`services/keeper` serves `/relay` and `/quote`). */
|
|
12
|
+
export function relayerSubmitter(url) {
|
|
13
|
+
const base = url.replace(/\/$/, "");
|
|
14
|
+
const submit = async (call) => {
|
|
15
|
+
const res = await fetch(`${base}/relay`, { method: "POST", headers: { "content-type": "application/json" }, body: json(call) });
|
|
16
|
+
const out = await res.json().catch(() => ({}));
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
throw new Error(out.error ?? `relay failed (${res.status})`);
|
|
19
|
+
return out.txHash;
|
|
20
|
+
};
|
|
21
|
+
submit.quote = async (kind, pool) => {
|
|
22
|
+
const res = await fetch(`${base}/quote?kind=${kind}&pool=${pool}`);
|
|
23
|
+
const out = await res.json().catch(() => ({}));
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(out.error ?? `relayer quote failed (${res.status})`);
|
|
26
|
+
return { relayer: out.relayer, fee: BigInt(out.fee) };
|
|
27
|
+
};
|
|
28
|
+
return submit;
|
|
29
|
+
}
|
|
30
|
+
/** Submit from a wallet directly — works, but that wallet pays gas and appears as the sender. Use for tests/relayers. */
|
|
31
|
+
export function walletSubmitter(publicClient, wallet, router, batcher, migrator) {
|
|
32
|
+
return async (call) => {
|
|
33
|
+
// viem cannot infer the nested tuple types through the union; the shapes match the ABI exactly.
|
|
34
|
+
if (call.kind === "migrate" && !migrator)
|
|
35
|
+
throw new Error("no migrator configured");
|
|
36
|
+
const req = call.kind === "migrate"
|
|
37
|
+
? { address: migrator, abi: cloakMigratorAbi, functionName: "migrate", args: [call.pool, call.proof, call.extData] }
|
|
38
|
+
: call.kind === "placeOrder"
|
|
39
|
+
? { address: batcher, abi: privateBatcherAbi, functionName: "placeOrder", args: [call.pool, call.proof, call.extData] }
|
|
40
|
+
: call.kind === "privateSwap"
|
|
41
|
+
? { address: router, abi: privateRouterAbi, functionName: "privateSwap", args: [call.pool, call.proof, call.extData] }
|
|
42
|
+
: call.kind === "unshieldHype"
|
|
43
|
+
? { address: router, abi: privateRouterAbi, functionName: "unshieldHype", args: [call.proof, call.extData] }
|
|
44
|
+
: { address: call.pool, abi: shieldedPoolAbi, functionName: "transact", args: [call.proof, call.extData] };
|
|
45
|
+
if (call.kind === "placeOrder" && !batcher)
|
|
46
|
+
throw new Error("no batcher configured");
|
|
47
|
+
// Pad the estimate: state can change between estimation and inclusion (e.g. an order landing in a new batch round
|
|
48
|
+
// writes fresh storage, ~60k more gas). Unused gas is refunded.
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
const est = await publicClient.estimateContractGas({ ...req, account: wallet.account });
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
const hash = await wallet.writeContract({ ...req, gas: (est * 13n) / 10n + 100000n });
|
|
53
|
+
const r = await publicClient.waitForTransactionReceipt({ hash });
|
|
54
|
+
if (r.status !== "success")
|
|
55
|
+
throw new Error(`shielded call reverted: ${hash}`);
|
|
56
|
+
return hash;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Private trading for Cloak launch tokens on Elysium.
|
|
61
|
+
*
|
|
62
|
+
* shieldHype public HYPE → private balance (one-time key, so deposits are unlinkable)
|
|
63
|
+
* buy / sell private balance → launch token / back, via the PrivateRouter and the launch's CloakPair,
|
|
64
|
+
* proven on this device and submitted by a relayer
|
|
65
|
+
* unshield private HYPE → any address
|
|
66
|
+
*/
|
|
67
|
+
export class CloakPrivate {
|
|
68
|
+
publicClient;
|
|
69
|
+
router;
|
|
70
|
+
keys;
|
|
71
|
+
prover;
|
|
72
|
+
submit;
|
|
73
|
+
fromBlock;
|
|
74
|
+
batcher;
|
|
75
|
+
migrator;
|
|
76
|
+
pools = new Map();
|
|
77
|
+
markets = [];
|
|
78
|
+
constructor(publicClient, router, keys, prover, submit, fromBlock = 0n, batcher, migrator) {
|
|
79
|
+
this.publicClient = publicClient;
|
|
80
|
+
this.router = router;
|
|
81
|
+
this.keys = keys;
|
|
82
|
+
this.prover = prover;
|
|
83
|
+
this.submit = submit;
|
|
84
|
+
this.fromBlock = fromBlock;
|
|
85
|
+
this.batcher = batcher;
|
|
86
|
+
this.migrator = migrator;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Derive the private keys from one EIP-712 signature bound to this chain and router, so a signature asked for by any
|
|
90
|
+
* other site or contract can't reproduce them. With `verify`, the wallet signs twice and the keys are refused if the
|
|
91
|
+
* two signatures differ (some smart and MPC wallets sign differently each time, which would hide earlier notes).
|
|
92
|
+
*/
|
|
93
|
+
static async keysFromWallet(wallet, opts) {
|
|
94
|
+
const req = { account: wallet.account, ...ACCOUNT_TYPED_DATA(opts.chainId, opts.router) };
|
|
95
|
+
const sig = await wallet.signTypedData(req);
|
|
96
|
+
if (opts.verify) {
|
|
97
|
+
const again = await wallet.signTypedData(req);
|
|
98
|
+
if (again.toLowerCase() !== sig.toLowerCase()) {
|
|
99
|
+
throw new Error("This wallet gives a different signature each time, so it can't hold a Cloak private account. Use a standard wallet such as MetaMask or Rabby.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return CloakKeys.fromSignature(sig);
|
|
103
|
+
}
|
|
104
|
+
/** Keys of the first Cloak deployment (plain signed message). Only needed to move an old balance across. */
|
|
105
|
+
static async legacyKeysFromWallet(wallet) {
|
|
106
|
+
const sig = await wallet.signMessage({ account: wallet.account, message: ACCOUNT_MESSAGE });
|
|
107
|
+
return CloakKeys.fromSignature(sig);
|
|
108
|
+
}
|
|
109
|
+
async loadMarkets() {
|
|
110
|
+
const c = { address: this.router, abi: privateRouterAbi };
|
|
111
|
+
const n = await this.publicClient.readContract({ ...c, functionName: "tokensCount" });
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let i = 0n; i < n; i++) {
|
|
114
|
+
const token = await this.publicClient.readContract({ ...c, functionName: "tokens", args: [i] });
|
|
115
|
+
const [pool, pair] = await Promise.all([
|
|
116
|
+
this.publicClient.readContract({ ...c, functionName: "poolOf", args: [token] }),
|
|
117
|
+
this.publicClient.readContract({ ...c, functionName: "pairOf", args: [token] }),
|
|
118
|
+
]);
|
|
119
|
+
out.push({ token, pool, pair: i === 0n ? null : pair });
|
|
120
|
+
}
|
|
121
|
+
this.markets = out;
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
get hypeMarket() {
|
|
125
|
+
const m = this.markets[0];
|
|
126
|
+
if (!m)
|
|
127
|
+
throw new Error("call loadMarkets() first");
|
|
128
|
+
return m;
|
|
129
|
+
}
|
|
130
|
+
market(token) {
|
|
131
|
+
const m = this.markets.find((x) => x.token.toLowerCase() === token.toLowerCase());
|
|
132
|
+
if (!m)
|
|
133
|
+
throw new Error("no private market for this token (createPool after settlement)");
|
|
134
|
+
return m;
|
|
135
|
+
}
|
|
136
|
+
state(pool) {
|
|
137
|
+
let s = this.pools.get(pool.toLowerCase());
|
|
138
|
+
if (!s) {
|
|
139
|
+
s = { leaves: [], scannedUpTo: 0, fromBlock: this.fromBlock, notes: [] };
|
|
140
|
+
this.pools.set(pool.toLowerCase(), s);
|
|
141
|
+
}
|
|
142
|
+
return s;
|
|
143
|
+
}
|
|
144
|
+
/** Pull new leaves from the pool's events, trial-decrypt them, and refresh spent flags. */
|
|
145
|
+
async sync(pool) {
|
|
146
|
+
const s = this.state(pool);
|
|
147
|
+
const latest = await this.publicClient.getBlockNumber({ cacheTime: 0 });
|
|
148
|
+
if (latest >= s.fromBlock) {
|
|
149
|
+
const logs = await this.publicClient.getContractEvents({
|
|
150
|
+
address: pool,
|
|
151
|
+
abi: shieldedPoolAbi,
|
|
152
|
+
eventName: "NewCommitment",
|
|
153
|
+
fromBlock: s.fromBlock,
|
|
154
|
+
toBlock: latest,
|
|
155
|
+
strict: true,
|
|
156
|
+
});
|
|
157
|
+
const known = new Set(s.leaves.map((l) => l.index));
|
|
158
|
+
for (const l of logs) {
|
|
159
|
+
const index = Number(l.args.index);
|
|
160
|
+
if (!known.has(index))
|
|
161
|
+
s.leaves.push({ index, commitment: l.args.commitment, memo: l.args.encryptedOutput });
|
|
162
|
+
}
|
|
163
|
+
s.leaves.sort((a, b) => a.index - b.index);
|
|
164
|
+
s.fromBlock = latest + 1n;
|
|
165
|
+
}
|
|
166
|
+
// only the contiguous prefix forms a valid tree
|
|
167
|
+
let n = 0;
|
|
168
|
+
while (n < s.leaves.length && s.leaves[n].index === n)
|
|
169
|
+
n++;
|
|
170
|
+
for (const l of s.leaves.slice(s.scannedUpTo, n)) {
|
|
171
|
+
const m = readMemo(this.keys.encPriv, l.memo);
|
|
172
|
+
if (!m || m.amount === 0n)
|
|
173
|
+
continue;
|
|
174
|
+
const k = await this.keys.key(m.keyIndex);
|
|
175
|
+
const note = { amount: m.amount, pubKey: k.pubKey, blinding: m.blinding };
|
|
176
|
+
if ((await noteCommitment(note)) !== l.commitment)
|
|
177
|
+
continue; // forged memo → ignored
|
|
178
|
+
const pos = positionOf(l.index);
|
|
179
|
+
s.notes.push({ ...note, leafIndex: pos, epoch: epochOf(l.index), keyIndex: m.keyIndex, privKey: k.privKey, commitment: l.commitment, nullifier: await nullifierOf(note, pos, k.privKey), spent: false });
|
|
180
|
+
}
|
|
181
|
+
s.scannedUpTo = n;
|
|
182
|
+
const flags = await Promise.all(s.notes.map((x) => (x.spent ? true : this.publicClient.readContract({ address: pool, abi: shieldedPoolAbi, functionName: "isSpent", args: [x.nullifier] }))));
|
|
183
|
+
s.notes.forEach((x, i) => (x.spent = Boolean(flags[i])));
|
|
184
|
+
return s.notes.filter((x) => !x.spent);
|
|
185
|
+
}
|
|
186
|
+
async balance(pool) {
|
|
187
|
+
return (await this.sync(pool)).reduce((a, x) => a + x.amount, 0n);
|
|
188
|
+
}
|
|
189
|
+
/** The most a single relayed call of this kind can move from `pool`: the balance minus the relayer's fee. */
|
|
190
|
+
async maxSpendable(pool, kind = "transact") {
|
|
191
|
+
const [bal, q] = await Promise.all([this.balance(pool), this.relayFee(kind, pool)]);
|
|
192
|
+
return bal > q.fee ? bal - q.fee : 0n;
|
|
193
|
+
}
|
|
194
|
+
/** The Merkle tree of one epoch (default: the epoch the given notes live in, else the newest). */
|
|
195
|
+
async tree(pool, epoch) {
|
|
196
|
+
const s = this.state(pool);
|
|
197
|
+
const leaves = s.leaves.slice(0, s.scannedUpTo);
|
|
198
|
+
const e = epoch ?? (leaves.length ? epochOf(leaves[leaves.length - 1].index) : 0);
|
|
199
|
+
const t = new MerkleTree();
|
|
200
|
+
for (const l of leaves)
|
|
201
|
+
if (epochOf(l.index) === e)
|
|
202
|
+
await t.insert(l.commitment);
|
|
203
|
+
return t;
|
|
204
|
+
}
|
|
205
|
+
treeFor(pool, inputs) {
|
|
206
|
+
return this.tree(pool, inputs.length ? inputs[0].epoch : undefined);
|
|
207
|
+
}
|
|
208
|
+
/** The relayer's fee for this call (0 when submitting from a wallet). */
|
|
209
|
+
async relayFee(kind, pool) {
|
|
210
|
+
return this.submit.quote ? this.submit.quote(kind, pool) : { relayer: zeroAddress, fee: 0n };
|
|
211
|
+
}
|
|
212
|
+
/** What a withdrawal of `amount` from `pool` delivers after the protocol's withdrawal fee. */
|
|
213
|
+
async afterFee(pool, amount) {
|
|
214
|
+
// pools of the first deployment have no withdrawal fee (and no afterFee)
|
|
215
|
+
return this.publicClient.readContract({ address: pool, abi: shieldedPoolAbi, functionName: "afterFee", args: [amount] }).catch(() => amount);
|
|
216
|
+
}
|
|
217
|
+
async changeOutput(amount) {
|
|
218
|
+
const main = await this.keys.main();
|
|
219
|
+
return { amount, pubKey: main.pubKey, memo: (b) => encryptFullMemo(this.keys.encPub, amount, b, MAIN_KEY) };
|
|
220
|
+
}
|
|
221
|
+
async dummy() {
|
|
222
|
+
const main = await this.keys.main();
|
|
223
|
+
return { dummyPubKey: main.pubKey, dummyMemo: (b) => encryptFullMemo(this.keys.encPub, 0n, b, MAIN_KEY) };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get 1–2 notes (from one tree epoch) covering `target`, merging notes privately first when the balance is spread
|
|
227
|
+
* across many. Each merge is a relayed transaction and pays the relayer's fee from the merged notes.
|
|
228
|
+
*/
|
|
229
|
+
async inputsFor(pool, target, onStatus) {
|
|
230
|
+
for (let round = 0; round < 6; round++) {
|
|
231
|
+
const live = await this.sync(pool);
|
|
232
|
+
const byEpoch = new Map();
|
|
233
|
+
for (const n of live)
|
|
234
|
+
byEpoch.set(n.epoch ?? 0, [...(byEpoch.get(n.epoch ?? 0) ?? []), n]);
|
|
235
|
+
const sum = (ns) => ns.reduce((a, x) => a + x.amount, 0n);
|
|
236
|
+
const groups = [...byEpoch.values()].sort((a, b) => (sum(b) > sum(a) ? 1 : -1));
|
|
237
|
+
if (!groups.length || sum(groups[0]) < target) {
|
|
238
|
+
throw new Error(sum(live) >= target ? "balance is split across tree epochs: send it in two parts" : "insufficient private balance");
|
|
239
|
+
}
|
|
240
|
+
const pick = selectNotes(groups[0], target);
|
|
241
|
+
if ("inputs" in pick)
|
|
242
|
+
return pick.inputs;
|
|
243
|
+
onStatus("Combining private notes first…");
|
|
244
|
+
const q = await this.relayFee("transact", pool);
|
|
245
|
+
const total = sum(pick.merge) - q.fee;
|
|
246
|
+
const { proof, extData } = await buildTransaction({
|
|
247
|
+
tree: await this.treeFor(pool, pick.merge),
|
|
248
|
+
inputs: pick.merge,
|
|
249
|
+
outputs: [await this.changeOutput(total)],
|
|
250
|
+
fee: q.fee,
|
|
251
|
+
relayer: q.relayer,
|
|
252
|
+
...(await this.dummy()),
|
|
253
|
+
prover: await this.prover(),
|
|
254
|
+
});
|
|
255
|
+
const h = await this.submit({ kind: "transact", pool, proof, extData });
|
|
256
|
+
await this.publicClient.waitForTransactionReceipt({ hash: h });
|
|
257
|
+
}
|
|
258
|
+
throw new Error("too many notes to combine");
|
|
259
|
+
}
|
|
260
|
+
// ───────────── public → private ─────────────
|
|
261
|
+
/** Public HYPE → private balance. This one step is public (your wallet deposits); everything after is not. */
|
|
262
|
+
async shieldHype(wallet, amount) {
|
|
263
|
+
const keyIndex = randomKeyIndex();
|
|
264
|
+
const k = await this.keys.key(keyIndex);
|
|
265
|
+
const blinding = randomField();
|
|
266
|
+
const hash = await wallet.writeContract({
|
|
267
|
+
address: this.router,
|
|
268
|
+
abi: privateRouterAbi,
|
|
269
|
+
functionName: "shieldHype",
|
|
270
|
+
args: [k.pubKey, blinding, encryptRouterMemo(this.keys.encPub, blinding, keyIndex)],
|
|
271
|
+
value: amount,
|
|
272
|
+
});
|
|
273
|
+
const r = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
274
|
+
if (r.status !== "success")
|
|
275
|
+
throw new Error("shield reverted");
|
|
276
|
+
return hash;
|
|
277
|
+
}
|
|
278
|
+
// ───────────── batch auctions (hide trade size) ─────────────
|
|
279
|
+
async roundInfo() {
|
|
280
|
+
if (!this.batcher)
|
|
281
|
+
throw new Error("no batcher configured");
|
|
282
|
+
const c = { address: this.batcher, abi: privateBatcherAbi };
|
|
283
|
+
const [round, roundSecs] = await Promise.all([
|
|
284
|
+
this.publicClient.readContract({ ...c, functionName: "currentRound" }),
|
|
285
|
+
this.publicClient.readContract({ ...c, functionName: "roundSecs" }),
|
|
286
|
+
]);
|
|
287
|
+
return { round, roundSecs: Number(roundSecs), endsAt: Number((round + 1n) * roundSecs) };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Batched private buy/sell. The amount is split into standard-size orders (1, 2, 5 × 10ⁿ), each placed from the
|
|
291
|
+
* private balance by the relayer into the current round. When the round ends, buys and sells cross at the pool's
|
|
292
|
+
* mid-price and only the net trades through the pool; outputs arrive as private notes. Observers can't tell how
|
|
293
|
+
* much any one trader moved. Any remainder that isn't a standard size stays in your private balance.
|
|
294
|
+
*/
|
|
295
|
+
async batch(token, side, amount, opts = {}) {
|
|
296
|
+
if (!this.batcher)
|
|
297
|
+
throw new Error("no batcher configured");
|
|
298
|
+
const onStatus = opts.onStatus ?? (() => { });
|
|
299
|
+
const m = this.market(token);
|
|
300
|
+
const poolIn = side === "buy" ? this.hypeMarket.pool : m.pool;
|
|
301
|
+
const maxOrders = opts.maxOrders ?? 6;
|
|
302
|
+
let { sizes, rest } = splitStandard(amount, maxOrders);
|
|
303
|
+
// each order also pays the relayer's fee from the same balance: if the orders plus their fees don't fit (say, when
|
|
304
|
+
// selling everything), shrink the orders so they do
|
|
305
|
+
const fee = (await this.relayFee("placeOrder", poolIn)).fee;
|
|
306
|
+
if (fee > 0n) {
|
|
307
|
+
const have = await this.balance(poolIn);
|
|
308
|
+
if (sizes.reduce((a, x) => a + x, 0n) + fee * BigInt(sizes.length) > have) {
|
|
309
|
+
const budget = have - fee * BigInt(maxOrders);
|
|
310
|
+
if (budget <= 0n)
|
|
311
|
+
throw new Error("insufficient private balance");
|
|
312
|
+
({ sizes } = splitStandard(budget < amount ? budget : amount, maxOrders));
|
|
313
|
+
rest = amount - sizes.reduce((a, x) => a + x, 0n);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (sizes.length === 0)
|
|
317
|
+
throw new Error("amount too small for a standard-size order");
|
|
318
|
+
const placed = [];
|
|
319
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
320
|
+
const size = sizes[i];
|
|
321
|
+
onStatus(`Order ${i + 1}/${sizes.length}: syncing…`);
|
|
322
|
+
const q = await this.relayFee("placeOrder", poolIn);
|
|
323
|
+
const inputs = await this.inputsFor(poolIn, size + q.fee, onStatus);
|
|
324
|
+
const keyIndex = randomKeyIndex();
|
|
325
|
+
const outKey = await this.keys.key(keyIndex);
|
|
326
|
+
const outBlinding = randomField();
|
|
327
|
+
const data = encodeAbiParameters(ORDER_DATA_ABI, [
|
|
328
|
+
{ token, outPubKey: outKey.pubKey, outBlinding, outMemo: encryptRouterMemo(this.keys.encPub, outBlinding, keyIndex) },
|
|
329
|
+
]);
|
|
330
|
+
const change = inputs.reduce((a, x) => a + x.amount, 0n) - size - q.fee;
|
|
331
|
+
onStatus(`Order ${i + 1}/${sizes.length}: proving on this device…`);
|
|
332
|
+
const { proof, extData } = await buildTransaction({
|
|
333
|
+
tree: await this.treeFor(poolIn, inputs),
|
|
334
|
+
inputs,
|
|
335
|
+
outputs: change > 0n ? [await this.changeOutput(change)] : [],
|
|
336
|
+
extAmount: -size,
|
|
337
|
+
fee: q.fee,
|
|
338
|
+
relayer: q.relayer,
|
|
339
|
+
recipient: this.batcher,
|
|
340
|
+
data,
|
|
341
|
+
...(await this.dummy()),
|
|
342
|
+
prover: await this.prover(),
|
|
343
|
+
});
|
|
344
|
+
onStatus(`Order ${i + 1}/${sizes.length}: relaying…`);
|
|
345
|
+
const txHash = await this.submit({ kind: "placeOrder", pool: poolIn, proof, extData });
|
|
346
|
+
const rc = await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
347
|
+
const ev = parseEventLogs({ abi: privateBatcherAbi, logs: rc.logs, eventName: "OrderPlaced" })[0];
|
|
348
|
+
placed.push({ id: ev.args.id, size, round: ev.args.round, txHash });
|
|
349
|
+
}
|
|
350
|
+
return { orders: placed, rest };
|
|
351
|
+
}
|
|
352
|
+
// ───────────── private trades ─────────────
|
|
353
|
+
async quote(token, side, amountIn) {
|
|
354
|
+
const pair = this.market(token).pair;
|
|
355
|
+
const [rH, rT] = await Promise.all([
|
|
356
|
+
this.publicClient.readContract({ address: pair, abi: cloakPairAbi, functionName: "reserveHype" }),
|
|
357
|
+
this.publicClient.readContract({ address: pair, abi: cloakPairAbi, functionName: "reserveToken" }),
|
|
358
|
+
]);
|
|
359
|
+
return this.publicClient.readContract({ address: pair, abi: cloakPairAbi, functionName: "getAmountOut", args: side === "buy" ? [amountIn, rH, rT] : [amountIn, rT, rH] });
|
|
360
|
+
}
|
|
361
|
+
/** Private buy: spend private HYPE, receive private tokens. `slippageBps` defaults to 1%. */
|
|
362
|
+
buy(token, hypeIn, opts = {}) {
|
|
363
|
+
return this.swap(token, "buy", hypeIn, opts);
|
|
364
|
+
}
|
|
365
|
+
/** Private sell: spend private tokens, receive private HYPE. */
|
|
366
|
+
sell(token, tokensIn, opts = {}) {
|
|
367
|
+
return this.swap(token, "sell", tokensIn, opts);
|
|
368
|
+
}
|
|
369
|
+
async swap(token, side, amountIn, opts) {
|
|
370
|
+
const onStatus = opts.onStatus ?? (() => { });
|
|
371
|
+
const m = this.market(token);
|
|
372
|
+
const poolIn = side === "buy" ? this.hypeMarket.pool : m.pool;
|
|
373
|
+
const poolOut = side === "buy" ? m.pool : this.hypeMarket.pool;
|
|
374
|
+
onStatus("Syncing your private balance…");
|
|
375
|
+
const q = await this.relayFee("privateSwap", poolIn);
|
|
376
|
+
const inputs = await this.inputsFor(poolIn, amountIn + q.fee, onStatus);
|
|
377
|
+
// the router trades what the pool releases after its withdrawal fee
|
|
378
|
+
const expected = await this.quote(token, side, await this.afterFee(poolIn, amountIn));
|
|
379
|
+
const minOut = (expected * BigInt(10_000 - (opts.slippageBps ?? 100))) / 10000n;
|
|
380
|
+
const keyIndex = randomKeyIndex();
|
|
381
|
+
const outKey = await this.keys.key(keyIndex);
|
|
382
|
+
const outBlinding = randomField();
|
|
383
|
+
const data = encodeAbiParameters(SWAP_DATA_ABI, [
|
|
384
|
+
{
|
|
385
|
+
pair: m.pair,
|
|
386
|
+
poolOut,
|
|
387
|
+
minOut,
|
|
388
|
+
outPubKey: outKey.pubKey,
|
|
389
|
+
outBlinding,
|
|
390
|
+
outMemo: encryptRouterMemo(this.keys.encPub, outBlinding, keyIndex),
|
|
391
|
+
deadline: BigInt(Math.floor(Date.now() / 1000) + 600),
|
|
392
|
+
},
|
|
393
|
+
]);
|
|
394
|
+
const change = inputs.reduce((a, x) => a + x.amount, 0n) - amountIn - q.fee;
|
|
395
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
396
|
+
const { proof, extData } = await buildTransaction({
|
|
397
|
+
tree: await this.treeFor(poolIn, inputs),
|
|
398
|
+
inputs,
|
|
399
|
+
outputs: change > 0n ? [await this.changeOutput(change)] : [],
|
|
400
|
+
extAmount: -amountIn,
|
|
401
|
+
fee: q.fee,
|
|
402
|
+
relayer: q.relayer,
|
|
403
|
+
recipient: this.router,
|
|
404
|
+
data,
|
|
405
|
+
...(await this.dummy()),
|
|
406
|
+
prover: await this.prover(),
|
|
407
|
+
});
|
|
408
|
+
onStatus("Relaying private trade…");
|
|
409
|
+
const txHash = await this.submit({ kind: "privateSwap", pool: poolIn, proof, extData });
|
|
410
|
+
return { txHash, expectedOut: expected, minOut, relayerFee: q.fee };
|
|
411
|
+
}
|
|
412
|
+
/** Private HYPE → native HYPE at `to` (use a fresh address to stay unlinkable). */
|
|
413
|
+
async unshieldHype(to, amount, onStatus = () => { }) {
|
|
414
|
+
const pool = this.hypeMarket.pool;
|
|
415
|
+
const q = await this.relayFee("unshieldHype", pool);
|
|
416
|
+
const inputs = await this.inputsFor(pool, amount + q.fee, onStatus);
|
|
417
|
+
const change = inputs.reduce((a, x) => a + x.amount, 0n) - amount - q.fee;
|
|
418
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
419
|
+
const { proof, extData } = await buildTransaction({
|
|
420
|
+
tree: await this.treeFor(pool, inputs),
|
|
421
|
+
inputs,
|
|
422
|
+
outputs: change > 0n ? [await this.changeOutput(change)] : [],
|
|
423
|
+
extAmount: -amount,
|
|
424
|
+
fee: q.fee,
|
|
425
|
+
relayer: q.relayer,
|
|
426
|
+
recipient: this.router,
|
|
427
|
+
data: encodeAbiParameters([{ type: "address" }], [to]),
|
|
428
|
+
...(await this.dummy()),
|
|
429
|
+
prover: await this.prover(),
|
|
430
|
+
});
|
|
431
|
+
onStatus("Relaying withdrawal…");
|
|
432
|
+
return this.submit({ kind: "unshieldHype", pool, proof, extData });
|
|
433
|
+
}
|
|
434
|
+
/** Your receiving address for private transfers. */
|
|
435
|
+
async address() {
|
|
436
|
+
return formatCloakAddress((await this.keys.main()).pubKey, this.keys.encPub);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Private transfer inside a pool: sender, recipient and amount all stay hidden.
|
|
440
|
+
* `token` = a launch token, or the WHYPE address / hypeMarket.token for HYPE.
|
|
441
|
+
*/
|
|
442
|
+
async send(token, to, amount, onStatus = () => { }) {
|
|
443
|
+
const rcpt = parseCloakAddress(to);
|
|
444
|
+
const pool = this.market(token).pool;
|
|
445
|
+
const q = await this.relayFee("transact", pool);
|
|
446
|
+
const inputs = await this.inputsFor(pool, amount + q.fee, onStatus);
|
|
447
|
+
const change = inputs.reduce((a, x) => a + x.amount, 0n) - amount - q.fee;
|
|
448
|
+
const outputs = [{ amount, pubKey: rcpt.pubKey, memo: (b) => encryptFullMemo(rcpt.encPub, amount, b, MAIN_KEY) }];
|
|
449
|
+
if (change > 0n)
|
|
450
|
+
outputs.push(await this.changeOutput(change));
|
|
451
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
452
|
+
const { proof, extData } = await buildTransaction({
|
|
453
|
+
tree: await this.treeFor(pool, inputs),
|
|
454
|
+
inputs,
|
|
455
|
+
outputs,
|
|
456
|
+
fee: q.fee,
|
|
457
|
+
relayer: q.relayer,
|
|
458
|
+
...(await this.dummy()),
|
|
459
|
+
prover: await this.prover(),
|
|
460
|
+
});
|
|
461
|
+
onStatus("Relaying private transfer…");
|
|
462
|
+
return this.submit({ kind: "transact", pool, proof, extData });
|
|
463
|
+
}
|
|
464
|
+
/** Private launch tokens → any address. */
|
|
465
|
+
async unshieldToken(token, to, amount, onStatus = () => { }) {
|
|
466
|
+
const pool = this.market(token).pool;
|
|
467
|
+
const q = await this.relayFee("transact", pool);
|
|
468
|
+
const inputs = await this.inputsFor(pool, amount + q.fee, onStatus);
|
|
469
|
+
const change = inputs.reduce((a, x) => a + x.amount, 0n) - amount - q.fee;
|
|
470
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
471
|
+
const { proof, extData } = await buildTransaction({
|
|
472
|
+
tree: await this.treeFor(pool, inputs),
|
|
473
|
+
inputs,
|
|
474
|
+
outputs: change > 0n ? [await this.changeOutput(change)] : [],
|
|
475
|
+
extAmount: -amount,
|
|
476
|
+
fee: q.fee,
|
|
477
|
+
relayer: q.relayer,
|
|
478
|
+
recipient: to,
|
|
479
|
+
...(await this.dummy()),
|
|
480
|
+
prover: await this.prover(),
|
|
481
|
+
});
|
|
482
|
+
return this.submit({ kind: "transact", pool, proof, extData });
|
|
483
|
+
}
|
|
484
|
+
// ───────────── moving a balance from an earlier deployment ─────────────
|
|
485
|
+
/**
|
|
486
|
+
* Move every private balance `legacy` (a client on the earlier router, with the earlier keys) can see into this
|
|
487
|
+
* deployment, note by note, through `CloakMigrator`. Each move is one relayed transaction: the old proof withdraws to
|
|
488
|
+
* the migrator, which deposits the same amount here as a new note for one of this account's one-time keys.
|
|
489
|
+
*/
|
|
490
|
+
async migrateFrom(legacy, onStatus = () => { }) {
|
|
491
|
+
const moved = [];
|
|
492
|
+
const markets = await legacy.loadMarkets();
|
|
493
|
+
await this.loadMarkets();
|
|
494
|
+
for (const m of markets) {
|
|
495
|
+
if (!this.markets.some((x) => x.token.toLowerCase() === m.token.toLowerCase()))
|
|
496
|
+
continue; // no pool here yet
|
|
497
|
+
for (;;) {
|
|
498
|
+
const live = (await legacy.sync(m.pool)).sort((a, b) => (a.amount > b.amount ? -1 : 1));
|
|
499
|
+
if (!live.length)
|
|
500
|
+
break;
|
|
501
|
+
const inputs = live.slice(0, 2);
|
|
502
|
+
const total = inputs.reduce((a, x) => a + x.amount, 0n);
|
|
503
|
+
const q = await legacy.relayFee("migrate", m.pool);
|
|
504
|
+
if (total <= q.fee)
|
|
505
|
+
break; // dust that costs more to move than it's worth
|
|
506
|
+
const amount = total - q.fee;
|
|
507
|
+
const keyIndex = randomKeyIndex();
|
|
508
|
+
const outKey = await this.keys.key(keyIndex);
|
|
509
|
+
const outBlinding = randomField();
|
|
510
|
+
const data = encodeAbiParameters(MOVE_DATA_ABI, [
|
|
511
|
+
{ outPubKey: outKey.pubKey, outBlinding, outMemo: encryptRouterMemo(this.keys.encPub, outBlinding, keyIndex) },
|
|
512
|
+
]);
|
|
513
|
+
onStatus(`Moving ${m.symbol ?? "balance"}: proving on this device…`);
|
|
514
|
+
const { proof, extData } = await buildTransaction({
|
|
515
|
+
tree: await legacy.treeFor(m.pool, inputs),
|
|
516
|
+
inputs,
|
|
517
|
+
outputs: [],
|
|
518
|
+
extAmount: -amount,
|
|
519
|
+
fee: q.fee,
|
|
520
|
+
relayer: q.relayer,
|
|
521
|
+
recipient: this.migratorAddress(),
|
|
522
|
+
data,
|
|
523
|
+
...(await legacy.dummy()),
|
|
524
|
+
prover: await this.prover(),
|
|
525
|
+
});
|
|
526
|
+
onStatus(`Moving ${m.symbol ?? "balance"}: relaying…`);
|
|
527
|
+
const txHash = await this.submit({ kind: "migrate", pool: m.pool, proof, extData });
|
|
528
|
+
await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
529
|
+
moved.push({ token: m.token, amount, txHash });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return moved;
|
|
533
|
+
}
|
|
534
|
+
migratorAddress() {
|
|
535
|
+
if (!this.migrator)
|
|
536
|
+
throw new Error("no migrator configured");
|
|
537
|
+
return this.migrator;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type Hex } from "viem";
|
|
2
|
+
export declare const FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
3
|
+
export declare const LEVELS = 20;
|
|
4
|
+
export declare const MAX_NOTE_AMOUNT: bigint;
|
|
5
|
+
/** Key index of the main spending key; any other index is a one-time key used for a single public entry. */
|
|
6
|
+
export declare const MAIN_KEY = 4294967295;
|
|
7
|
+
export declare const randomField: () => bigint;
|
|
8
|
+
export declare const randomKeyIndex: () => number;
|
|
9
|
+
export declare function poseidon(inputs: bigint[]): Promise<bigint>;
|
|
10
|
+
/** The message a wallet signs to derive its Cloak private account (deterministic for standard EOAs). */
|
|
11
|
+
export declare const ACCOUNT_MESSAGE = "Cloak private account v1\n\nSigning this derives your private trading keys on Elysium. It costs nothing and does not move funds.";
|
|
12
|
+
/**
|
|
13
|
+
* EIP-712 request whose signature derives the private keys of a Cloak v2 account. It is bound to the chain and the
|
|
14
|
+
* router, so a signature collected by another site or for another contract can't reproduce the keys.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ACCOUNT_TYPED_DATA: (chainId: number, router: `0x${string}`) => {
|
|
17
|
+
readonly domain: {
|
|
18
|
+
readonly name: "Cloak";
|
|
19
|
+
readonly version: "2";
|
|
20
|
+
readonly chainId: number;
|
|
21
|
+
readonly verifyingContract: `0x${string}`;
|
|
22
|
+
};
|
|
23
|
+
readonly types: {
|
|
24
|
+
readonly PrivateAccount: readonly [{
|
|
25
|
+
readonly name: "purpose";
|
|
26
|
+
readonly type: "string";
|
|
27
|
+
}];
|
|
28
|
+
};
|
|
29
|
+
readonly primaryType: "PrivateAccount";
|
|
30
|
+
readonly message: {
|
|
31
|
+
readonly purpose: "Derive my Cloak private trading keys. This costs nothing and moves no funds. Only sign this on cloak.boblabs.tech.";
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export interface SpendKey {
|
|
35
|
+
index: number;
|
|
36
|
+
privKey: bigint;
|
|
37
|
+
pubKey: bigint;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One seed → a main spending key, a view key (memo encryption) and 2³² one-time keys. One-time keys receive every note
|
|
41
|
+
* that enters the pool publicly (shielding, swap outputs), so on-chain `depositTo` calls never repeat a pubKey and
|
|
42
|
+
* cannot be linked to each other or to the main key.
|
|
43
|
+
*/
|
|
44
|
+
export declare class CloakKeys {
|
|
45
|
+
private seed;
|
|
46
|
+
readonly encPriv: Uint8Array;
|
|
47
|
+
readonly encPub: Uint8Array;
|
|
48
|
+
private cache;
|
|
49
|
+
private constructor();
|
|
50
|
+
static fromSeed(seed: Uint8Array): CloakKeys;
|
|
51
|
+
/** Derive from a wallet signature over ACCOUNT_MESSAGE. */
|
|
52
|
+
static fromSignature(sig: Hex): CloakKeys;
|
|
53
|
+
key(index: number): Promise<SpendKey>;
|
|
54
|
+
main(): Promise<SpendKey>;
|
|
55
|
+
}
|
|
56
|
+
export interface Note {
|
|
57
|
+
amount: bigint;
|
|
58
|
+
pubKey: bigint;
|
|
59
|
+
blinding: bigint;
|
|
60
|
+
}
|
|
61
|
+
export declare const noteCommitment: (n: Note) => Promise<bigint>;
|
|
62
|
+
export declare function nullifierOf(n: Note, leafIndex: number, privKey: bigint): Promise<bigint>;
|
|
63
|
+
export declare function encryptFullMemo(encPub: Uint8Array, amount: bigint, blinding: bigint, keyIndex: number): Hex;
|
|
64
|
+
/** For router-created notes: the router adds `0x02 ‖ amount` in front on-chain. */
|
|
65
|
+
export declare function encryptRouterMemo(encPub: Uint8Array, blinding: bigint, keyIndex: number): Hex;
|
|
66
|
+
export declare function readMemo(encPriv: Uint8Array, memo: Hex): {
|
|
67
|
+
amount: bigint;
|
|
68
|
+
blinding: bigint;
|
|
69
|
+
keyIndex: number;
|
|
70
|
+
} | null;
|
|
71
|
+
export declare function formatCloakAddress(pubKey: bigint, encPub: Uint8Array): string;
|
|
72
|
+
export declare function parseCloakAddress(addr: string): {
|
|
73
|
+
pubKey: bigint;
|
|
74
|
+
encPub: Uint8Array;
|
|
75
|
+
};
|