@cloakely/sdk 0.8.5 → 0.9.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/dist/abis.d.ts +498 -0
- package/dist/abis.js +647 -0
- package/dist/bytecode.d.ts +17 -14
- package/dist/bytecode.js +1 -1
- package/dist/deployments.d.ts +2 -0
- package/dist/deployments.js +2 -0
- package/dist/drops.d.ts +11 -1
- package/dist/private/client.d.ts +75 -1
- package/dist/private/client.js +255 -1
- package/dist/private/crypto.d.ts +15 -0
- package/dist/private/crypto.js +20 -4
- package/dist/private/funds.d.ts +58 -0
- package/dist/private/funds.js +65 -0
- package/dist/private/fundsVk.d.ts +13 -0
- package/dist/private/fundsVk.js +121 -0
- package/dist/private/index.d.ts +3 -0
- package/dist/private/index.js +3 -0
- package/dist/private/prover.d.ts +29 -0
- package/dist/private/prover.js +22 -0
- package/dist/private/receive.d.ts +52 -0
- package/dist/private/receive.js +45 -0
- package/dist/private/shared.d.ts +12 -0
- package/dist/private/shared.js +70 -4
- package/dist/private/syncState.d.ts +28 -0
- package/dist/private/syncState.js +40 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/spots.js +1 -1
- package/package.json +1 -1
package/dist/deployments.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface CloakDeployment {
|
|
|
54
54
|
sharedRouter?: Address;
|
|
55
55
|
/** SharedBatcher: batch rounds for the shared vault's tokens. */
|
|
56
56
|
sharedBatcher?: Address;
|
|
57
|
+
/** CloakReceive: private receive addresses. */
|
|
58
|
+
receive?: Address;
|
|
57
59
|
/** CloakDropFactory: sealed NFT drops. */
|
|
58
60
|
dropFactory?: Address;
|
|
59
61
|
/** Block the drop factory was deployed in: scan drop events from here. */
|
package/dist/deployments.js
CHANGED
|
@@ -26,6 +26,7 @@ export const deployments = {
|
|
|
26
26
|
sharedBlock: 21426599n,
|
|
27
27
|
sharedRouter: "0xe2a0769a5acbd987c1b6725247c56187a112dc7e",
|
|
28
28
|
sharedBatcher: "0x11598b35fabc90fdd45d8a77ac41e2057c32ea14",
|
|
29
|
+
receive: "0x852675b2e127fcce3b5efd3ef817623db7aa44f3",
|
|
29
30
|
// sealed NFT drops (Nov 3): GTD, then a sealed auction with the creator's price as the floor and a WL bid boost
|
|
30
31
|
dropFactory: "0x2afb7d9a5baa02a2d72a7c761521fc66dce9d783",
|
|
31
32
|
dropFactoryBlock: 22329730n,
|
|
@@ -115,6 +116,7 @@ export async function liveDeployment(publicClient, base) {
|
|
|
115
116
|
sharedBlock: reg.sharedBlock ?? base.sharedBlock,
|
|
116
117
|
sharedRouter: reg.sharedRouter ?? base.sharedRouter,
|
|
117
118
|
sharedBatcher: reg.sharedBatcher ?? base.sharedBatcher,
|
|
119
|
+
receive: reg.receive ?? base.receive,
|
|
118
120
|
hypePool,
|
|
119
121
|
whype,
|
|
120
122
|
previous: reg.previous.length
|
package/dist/drops.d.ts
CHANGED
|
@@ -179,7 +179,7 @@ export interface CreateDropOptions {
|
|
|
179
179
|
};
|
|
180
180
|
biddingSecs: number;
|
|
181
181
|
/** Longest time after bidding closes for the keeper to open the bids (default 30 minutes). The drop settles as soon
|
|
182
|
-
* as every bid is open, so a long window only matters if the keeper is down; a bid still sealed at its end loses 10
|
|
182
|
+
* as every bid is open, so a long window only matters if the keeper is down; a bid still sealed at its end loses 25% (10% on drops made before Nov 7). */
|
|
183
183
|
revealSecs?: number;
|
|
184
184
|
image?: string;
|
|
185
185
|
/** A wide banner for the drop page (an https:, ipfs: or small data: image). */
|
|
@@ -1294,6 +1294,16 @@ export declare class CloakDrops {
|
|
|
1294
1294
|
readonly internalType: "uint256";
|
|
1295
1295
|
}];
|
|
1296
1296
|
readonly stateMutability: "view";
|
|
1297
|
+
}, {
|
|
1298
|
+
readonly type: "function";
|
|
1299
|
+
readonly name: "seedBlock";
|
|
1300
|
+
readonly inputs: readonly [];
|
|
1301
|
+
readonly outputs: readonly [{
|
|
1302
|
+
readonly name: "";
|
|
1303
|
+
readonly type: "uint64";
|
|
1304
|
+
readonly internalType: "uint64";
|
|
1305
|
+
}];
|
|
1306
|
+
readonly stateMutability: "view";
|
|
1297
1307
|
}, {
|
|
1298
1308
|
readonly type: "function";
|
|
1299
1309
|
readonly name: "setApprovalForAll";
|
package/dist/private/client.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { type Address, type Hex, type PublicClient } from "viem";
|
|
|
2
2
|
import { type Wallet } from "../cloak.js";
|
|
3
3
|
import { CloakKeys } from "./crypto.js";
|
|
4
4
|
import { MerkleTree } from "./tree.js";
|
|
5
|
+
import { type PublicLeaves } from "./syncState.js";
|
|
6
|
+
import { type FundsProof } from "./funds.js";
|
|
7
|
+
import type { FundsProver } from "./prover.js";
|
|
5
8
|
import type { ProofArgs, Prover } from "./prover.js";
|
|
6
9
|
import { type ExtData, type OwnedNote } from "./tx.js";
|
|
7
10
|
/** A shielded call the relayer submits so the trader's wallet never appears on-chain. */
|
|
@@ -45,6 +48,30 @@ export declare function relayerSubmitter(url: string): Submitter;
|
|
|
45
48
|
/** Submit from a wallet directly — works, but that wallet pays gas and appears as the sender. Use for tests/relayers. */
|
|
46
49
|
export declare function walletSubmitter(publicClient: PublicClient, wallet: Wallet, router: Address, batcher?: Address, migrator?: Address): Submitter;
|
|
47
50
|
export type Status = (msg: string) => void;
|
|
51
|
+
/** One piece of a split withdrawal: a proved relay call, and the earliest time a relayer may send it (unix seconds). */
|
|
52
|
+
export interface ScheduledPiece {
|
|
53
|
+
call: RelayCall;
|
|
54
|
+
notBefore: number;
|
|
55
|
+
/** what this piece withdraws (before the vault's withdrawal fee) */
|
|
56
|
+
amount: bigint;
|
|
57
|
+
to: Address;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `amount` in `k` pieces of different, ordinary-looking sizes: random shares rounded to 2 significant digits, the
|
|
61
|
+
* last one taking the remainder. Every piece is above zero.
|
|
62
|
+
*/
|
|
63
|
+
export declare function splitAmount(amount: bigint, k: number): bigint[];
|
|
64
|
+
/** Hand split-withdrawal pieces to a Cloak relayer (`POST /schedule`). Returns an id per piece, for `scheduleStatus`. */
|
|
65
|
+
export declare function scheduleRelay(url: string, pieces: ScheduledPiece[]): Promise<string[]>;
|
|
66
|
+
export interface ScheduledStatus {
|
|
67
|
+
id: string;
|
|
68
|
+
state: "waiting" | "sent" | "dropped" | "unknown";
|
|
69
|
+
notBefore?: number;
|
|
70
|
+
txHash?: Hex;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
/** Where a relayer's scheduled pieces stand (`GET /schedule?ids=`). "unknown": it doesn't have it (re-send it). */
|
|
74
|
+
export declare function scheduleStatus(url: string, ids: string[]): Promise<ScheduledStatus[]>;
|
|
48
75
|
/**
|
|
49
76
|
* Private trading for Cloak launch tokens on Elysium.
|
|
50
77
|
*
|
|
@@ -102,8 +129,23 @@ export declare class CloakPrivate {
|
|
|
102
129
|
/** Register a shielded pool that isn't a launch market, such as USDC (`deploymentFor(chainId).assets`). */
|
|
103
130
|
addAsset(a: PrivateAsset): PrivateMarket;
|
|
104
131
|
private state;
|
|
105
|
-
|
|
132
|
+
private inflight;
|
|
133
|
+
/** Pull new leaves from the pool's events, trial-decrypt them, and refresh spent flags. One sync per pool at a time
|
|
134
|
+
* (callers at the same moment share it), so a new note is never counted twice. */
|
|
106
135
|
sync(pool: Address): Promise<OwnedNote[]>;
|
|
136
|
+
private syncOnce;
|
|
137
|
+
/**
|
|
138
|
+
* Start `pool` from public leaves (a keeper's GET /leaves) instead of scanning its history. Used only if nothing is
|
|
139
|
+
* synced yet and the leaves hash to a root the vault knows; returns whether they were used.
|
|
140
|
+
*/
|
|
141
|
+
seedLeaves(pool: Address, src: PublicLeaves): Promise<boolean>;
|
|
142
|
+
/**
|
|
143
|
+
* What sync has learned so far, to keep on this device (encrypted: it lists your notes) and hand back to
|
|
144
|
+
* `importSync` next time, so only new blocks are scanned. Spending keys are left out and derived again on import.
|
|
145
|
+
*/
|
|
146
|
+
exportSync(): string;
|
|
147
|
+
/** Load a state from `exportSync` (this account's only: notes that don't match its keys are dropped). */
|
|
148
|
+
importSync(data: string): Promise<boolean>;
|
|
107
149
|
/** Every note this account has received in `pool`, spent ones included (each with `spent` and its nullifier). */
|
|
108
150
|
history(pool: Address): Promise<OwnedNote[]>;
|
|
109
151
|
balance(pool: Address): Promise<bigint>;
|
|
@@ -248,6 +290,38 @@ export declare class CloakPrivate {
|
|
|
248
290
|
* `token` = a launch token, an asset such as USDC, or the WHYPE address / hypeMarket.token for HYPE.
|
|
249
291
|
*/
|
|
250
292
|
send(token: Address, to: string, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
293
|
+
/**
|
|
294
|
+
* A withdrawal sent later in 2–4 pieces of different sizes, so the amount coming out doesn't match anything that went
|
|
295
|
+
* in. Done now, on this device:
|
|
296
|
+
* 1. private self-sends split the balance into one exact note per piece (pieces − 1 relayed sends),
|
|
297
|
+
* 2. each piece's withdrawal is proved, its recipient fixed by the proof.
|
|
298
|
+
* Hand the pieces to a relayer with `scheduleRelay`: it can only send each one (after `notBefore`) or refuse it,
|
|
299
|
+
* never change where it goes. Spending a piece's note before it's sent cancels that piece.
|
|
300
|
+
* `token` is a launch token or asset, or the WHYPE address / hypeMarket.token for HYPE.
|
|
301
|
+
*/
|
|
302
|
+
planSplitWithdraw(token: Address, to: Address, amount: bigint, opts?: {
|
|
303
|
+
pieces?: number;
|
|
304
|
+
windowSecs?: number;
|
|
305
|
+
onStatus?: Status;
|
|
306
|
+
}): Promise<ScheduledPiece[]>;
|
|
307
|
+
/**
|
|
308
|
+
* Prove "I hold at least `threshold` of `token` in its vault" without revealing the balance, the notes or who you
|
|
309
|
+
* are (see funds.ts). Uses 1–3 of your notes; if the balance is spread over more, they're combined first (one
|
|
310
|
+
* relayed private send). `label` says who it's for and `expiresAt` (unix seconds) when it stops counting.
|
|
311
|
+
*/
|
|
312
|
+
proveFunds(token: Address, threshold: bigint, o: {
|
|
313
|
+
label: string;
|
|
314
|
+
expiresAt: number;
|
|
315
|
+
prover: FundsProver;
|
|
316
|
+
symbol?: string;
|
|
317
|
+
decimals?: number;
|
|
318
|
+
onStatus?: Status;
|
|
319
|
+
}): Promise<FundsProof>;
|
|
320
|
+
/**
|
|
321
|
+
* Move notes into a fresh note of your own (a relayed private send to yourself), so whoever holds a proof of funds
|
|
322
|
+
* that named their nullifiers can't tell when you later spend that money.
|
|
323
|
+
*/
|
|
324
|
+
refreshNotes(pool: Address, notes: OwnedNote[], onStatus?: Status): Promise<Hex>;
|
|
251
325
|
/** Private tokens (a launch token, or an asset such as USDC) → any address. */
|
|
252
326
|
unshieldToken(token: Address, to: Address, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
253
327
|
/**
|
package/dist/private/client.js
CHANGED
|
@@ -4,6 +4,8 @@ import { cloakAuctionAbi, cloakConfigAbi, cloakMigratorAbi, cloakPairAbi, privat
|
|
|
4
4
|
import { Cloak } from "../cloak.js";
|
|
5
5
|
import { ACCOUNT_MESSAGE, ACCOUNT_TYPED_DATA, ROUTER_ACCOUNT_TYPED_DATA, CloakKeys, MAIN_KEY, noteCommitment, encryptFullMemo, encryptRouterMemo, formatCloakAddress, nullifierOf, parseCloakAddress, randomField, randomKeyIndex, readMemo, } from "./crypto.js";
|
|
6
6
|
import { MerkleTree } from "./tree.js";
|
|
7
|
+
import { fromJson, leavesRoot, toJson } from "./syncState.js";
|
|
8
|
+
import { FUNDS_NOTES, fundsContext } from "./funds.js";
|
|
7
9
|
import { MOVE_DATA_ABI, ORDER_DATA_ABI, SWAP_DATA_ABI, buildTransaction, selectNotes, splitStandard } from "./tx.js";
|
|
8
10
|
/** Leaf positions per tree epoch (a pool starts a new tree after 2^20 leaves). */
|
|
9
11
|
const EPOCH_BITS = 20;
|
|
@@ -71,6 +73,46 @@ export function walletSubmitter(publicClient, wallet, router, batcher, migrator)
|
|
|
71
73
|
return hash;
|
|
72
74
|
};
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `amount` in `k` pieces of different, ordinary-looking sizes: random shares rounded to 2 significant digits, the
|
|
78
|
+
* last one taking the remainder. Every piece is above zero.
|
|
79
|
+
*/
|
|
80
|
+
export function splitAmount(amount, k) {
|
|
81
|
+
if (amount < BigInt(k) * 100n)
|
|
82
|
+
return [amount];
|
|
83
|
+
const w = Array.from({ length: k }, () => 0.6 + Math.random());
|
|
84
|
+
const sum = w.reduce((a, b) => a + b, 0);
|
|
85
|
+
const round2 = (x) => {
|
|
86
|
+
const d = x.toString().length;
|
|
87
|
+
const unit = d > 2 ? 10n ** BigInt(d - 2) : 1n;
|
|
88
|
+
return (x / unit) * unit;
|
|
89
|
+
};
|
|
90
|
+
const out = [];
|
|
91
|
+
let left = amount;
|
|
92
|
+
for (let i = 0; i < k - 1; i++) {
|
|
93
|
+
const x = round2((amount * BigInt(Math.round((w[i] / sum) * 1e6))) / 1000000n);
|
|
94
|
+
out.push(x);
|
|
95
|
+
left -= x;
|
|
96
|
+
}
|
|
97
|
+
out.push(left);
|
|
98
|
+
return out.every((x) => x > 0n) ? out : [amount];
|
|
99
|
+
}
|
|
100
|
+
/** Hand split-withdrawal pieces to a Cloak relayer (`POST /schedule`). Returns an id per piece, for `scheduleStatus`. */
|
|
101
|
+
export async function scheduleRelay(url, pieces) {
|
|
102
|
+
const res = await fetch(`${url.replace(/\/$/, "")}/schedule`, { method: "POST", headers: { "content-type": "application/json" }, body: json({ pieces: pieces.map((p) => ({ call: p.call, notBefore: p.notBefore })) }), signal: AbortSignal.timeout(120_000) });
|
|
103
|
+
const out = await res.json().catch(() => ({}));
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
throw new Error(out.error ?? `scheduling failed (${res.status})`);
|
|
106
|
+
return out.ids;
|
|
107
|
+
}
|
|
108
|
+
/** Where a relayer's scheduled pieces stand (`GET /schedule?ids=`). "unknown": it doesn't have it (re-send it). */
|
|
109
|
+
export async function scheduleStatus(url, ids) {
|
|
110
|
+
const res = await fetch(`${url.replace(/\/$/, "")}/schedule?ids=${ids.join(",")}`, { signal: AbortSignal.timeout(30_000) });
|
|
111
|
+
const out = await res.json().catch(() => ({}));
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
throw new Error(out.error ?? `status failed (${res.status})`);
|
|
114
|
+
return out.pieces;
|
|
115
|
+
}
|
|
74
116
|
/**
|
|
75
117
|
* Private trading for Cloak launch tokens on Elysium.
|
|
76
118
|
*
|
|
@@ -207,8 +249,19 @@ export class CloakPrivate {
|
|
|
207
249
|
}
|
|
208
250
|
return s;
|
|
209
251
|
}
|
|
210
|
-
|
|
252
|
+
inflight = new Map();
|
|
253
|
+
/** Pull new leaves from the pool's events, trial-decrypt them, and refresh spent flags. One sync per pool at a time
|
|
254
|
+
* (callers at the same moment share it), so a new note is never counted twice. */
|
|
211
255
|
async sync(pool) {
|
|
256
|
+
const k = pool.toLowerCase();
|
|
257
|
+
let p = this.inflight.get(k);
|
|
258
|
+
if (!p) {
|
|
259
|
+
p = this.syncOnce(pool).finally(() => this.inflight.delete(k));
|
|
260
|
+
this.inflight.set(k, p);
|
|
261
|
+
}
|
|
262
|
+
return p;
|
|
263
|
+
}
|
|
264
|
+
async syncOnce(pool) {
|
|
212
265
|
const s = this.state(pool);
|
|
213
266
|
const latest = await this.publicClient.getBlockNumber({ cacheTime: 0 });
|
|
214
267
|
if (latest >= s.fromBlock) {
|
|
@@ -233,6 +286,11 @@ export class CloakPrivate {
|
|
|
233
286
|
let n = 0;
|
|
234
287
|
while (n < s.leaves.length && s.leaves[n].index === n)
|
|
235
288
|
n++;
|
|
289
|
+
if (n < s.leaves.length && s.seeded) {
|
|
290
|
+
// the cache stopped short of where the chain picked up: forget it and scan this vault from the start
|
|
291
|
+
this.pools.delete(pool.toLowerCase());
|
|
292
|
+
return this.syncOnce(pool);
|
|
293
|
+
}
|
|
236
294
|
for (const l of s.leaves.slice(s.scannedUpTo, n)) {
|
|
237
295
|
const m = readMemo(this.keys.encPriv, l.memo);
|
|
238
296
|
if (!m || m.amount === 0n)
|
|
@@ -249,6 +307,60 @@ export class CloakPrivate {
|
|
|
249
307
|
s.notes.forEach((x, i) => (x.spent = Boolean(flags[i])));
|
|
250
308
|
return s.notes.filter((x) => !x.spent);
|
|
251
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Start `pool` from public leaves (a keeper's GET /leaves) instead of scanning its history. Used only if nothing is
|
|
312
|
+
* synced yet and the leaves hash to a root the vault knows; returns whether they were used.
|
|
313
|
+
*/
|
|
314
|
+
async seedLeaves(pool, src) {
|
|
315
|
+
const s = this.state(pool);
|
|
316
|
+
if (s.leaves.length || s.scannedUpTo)
|
|
317
|
+
return false;
|
|
318
|
+
const root = await leavesRoot(src.leaves);
|
|
319
|
+
if (root === null)
|
|
320
|
+
return false;
|
|
321
|
+
const known = await this.publicClient.readContract({ address: pool, abi: shieldedPoolAbi, functionName: "isKnownRoot", args: [root] }).catch(() => false);
|
|
322
|
+
if (!known)
|
|
323
|
+
return false;
|
|
324
|
+
s.leaves = src.leaves.map((l) => ({ ...l }));
|
|
325
|
+
s.fromBlock = src.block + 1n;
|
|
326
|
+
s.seeded = true;
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* What sync has learned so far, to keep on this device (encrypted: it lists your notes) and hand back to
|
|
331
|
+
* `importSync` next time, so only new blocks are scanned. Spending keys are left out and derived again on import.
|
|
332
|
+
*/
|
|
333
|
+
exportSync() {
|
|
334
|
+
// scanned leaves keep only their commitment (the tree needs it; the memo was already read)
|
|
335
|
+
const pools = [...this.pools].map(([k, s]) => [k, { leaves: s.leaves.map((l, i) => (i < s.scannedUpTo ? { ...l, memo: "0x" } : l)), scannedUpTo: s.scannedUpTo, fromBlock: s.fromBlock, notes: s.notes.map(({ privKey: _, ...n }) => n) }]);
|
|
336
|
+
return toJson({ v: 1, pools });
|
|
337
|
+
}
|
|
338
|
+
/** Load a state from `exportSync` (this account's only: notes that don't match its keys are dropped). */
|
|
339
|
+
async importSync(data) {
|
|
340
|
+
try {
|
|
341
|
+
const o = fromJson(data);
|
|
342
|
+
if (o.v !== 1)
|
|
343
|
+
return false;
|
|
344
|
+
for (const [k, st] of o.pools) {
|
|
345
|
+
if (this.pools.has(k))
|
|
346
|
+
continue;
|
|
347
|
+
const notes = [];
|
|
348
|
+
let ok = true;
|
|
349
|
+
for (const n of st.notes) {
|
|
350
|
+
const key = await this.keys.key(n.keyIndex);
|
|
351
|
+
if (key.pubKey !== n.pubKey)
|
|
352
|
+
ok = false;
|
|
353
|
+
notes.push({ ...n, privKey: key.privKey });
|
|
354
|
+
}
|
|
355
|
+
if (ok)
|
|
356
|
+
this.pools.set(k, { leaves: st.leaves, scannedUpTo: st.scannedUpTo, fromBlock: st.fromBlock, notes, seeded: true });
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
252
364
|
/** Every note this account has received in `pool`, spent ones included (each with `spent` and its nullifier). */
|
|
253
365
|
async history(pool) {
|
|
254
366
|
await this.sync(pool);
|
|
@@ -742,6 +854,148 @@ export class CloakPrivate {
|
|
|
742
854
|
onStatus("Relaying private transfer…");
|
|
743
855
|
return this.submit({ kind: "transact", pool, proof, extData });
|
|
744
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* A withdrawal sent later in 2–4 pieces of different sizes, so the amount coming out doesn't match anything that went
|
|
859
|
+
* in. Done now, on this device:
|
|
860
|
+
* 1. private self-sends split the balance into one exact note per piece (pieces − 1 relayed sends),
|
|
861
|
+
* 2. each piece's withdrawal is proved, its recipient fixed by the proof.
|
|
862
|
+
* Hand the pieces to a relayer with `scheduleRelay`: it can only send each one (after `notBefore`) or refuse it,
|
|
863
|
+
* never change where it goes. Spending a piece's note before it's sent cancels that piece.
|
|
864
|
+
* `token` is a launch token or asset, or the WHYPE address / hypeMarket.token for HYPE.
|
|
865
|
+
*/
|
|
866
|
+
async planSplitWithdraw(token, to, amount, opts = {}) {
|
|
867
|
+
const onStatus = opts.onStatus ?? (() => { });
|
|
868
|
+
const k = Math.min(4, Math.max(2, opts.pieces ?? 3));
|
|
869
|
+
const windowSecs = Math.max(600, opts.windowSecs ?? 3600);
|
|
870
|
+
const isHype = token.toLowerCase() === this.hypeMarket.token.toLowerCase();
|
|
871
|
+
const pool = isHype ? this.hypeMarket.pool : this.market(token).pool;
|
|
872
|
+
const kind = isHype ? "unshieldHype" : "transact";
|
|
873
|
+
const amounts = splitAmount(amount, k);
|
|
874
|
+
// each piece carries twice today's relayer fee, so a gas rise before it goes out doesn't get it refused
|
|
875
|
+
const q = await this.relayFee(kind, pool);
|
|
876
|
+
const pieceFee = q.fee * 2n;
|
|
877
|
+
const notes = [];
|
|
878
|
+
let rest = null;
|
|
879
|
+
for (let i = 0; i < k - 1; i++) {
|
|
880
|
+
onStatus(`Splitting your balance privately (${i + 1} of ${k - 1})…`);
|
|
881
|
+
const sq = await this.relayFee("transact", pool);
|
|
882
|
+
const remaining = amounts.slice(i).reduce((a, x) => a + x + pieceFee, 0n);
|
|
883
|
+
const inputs = rest ?? (await this.inputsFor(pool, remaining + sq.fee * BigInt(2 * (k - 1)), onStatus));
|
|
884
|
+
const total = inputs.reduce((a, x) => a + x.amount, 0n);
|
|
885
|
+
const first = amounts[i] + pieceFee;
|
|
886
|
+
const outputs = [await this.changeOutput(first), await this.changeOutput(total - sq.fee - first)];
|
|
887
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
888
|
+
const { proof, extData } = await buildTransaction({ tree: await this.treeFor(pool, inputs), inputs, outputs, fee: sq.fee, relayer: sq.relayer, ...(await this.dummy()), prover: await this.prover() });
|
|
889
|
+
const h = await this.submit({ kind: "transact", pool, proof, extData });
|
|
890
|
+
await this.publicClient.waitForTransactionReceipt({ hash: h });
|
|
891
|
+
const [c0, c1] = proof.outputCommitments.map(BigInt);
|
|
892
|
+
let mine = [];
|
|
893
|
+
for (let t = 0; t < 10 && mine.length < 2; t++) {
|
|
894
|
+
mine = (await this.sync(pool)).filter((n) => n.commitment === c0 || n.commitment === c1);
|
|
895
|
+
if (mine.length < 2)
|
|
896
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
897
|
+
}
|
|
898
|
+
const piece = mine.find((n) => n.commitment === c0);
|
|
899
|
+
const left = mine.find((n) => n.commitment === c1);
|
|
900
|
+
if (!piece || !left)
|
|
901
|
+
throw new Error("The split landed, but the new notes haven't shown up yet. Try again in a minute: nothing was lost.");
|
|
902
|
+
notes.push(piece);
|
|
903
|
+
rest = [left];
|
|
904
|
+
}
|
|
905
|
+
// the last piece's note is what the splits left: its amount plus anything spare, which comes back as change
|
|
906
|
+
notes.push(rest[0]);
|
|
907
|
+
const now = Math.floor(Date.now() / 1000);
|
|
908
|
+
const times = Array.from({ length: k }, () => now + 60 + Math.floor(Math.random() * (windowSecs - 60))).sort((a, b) => a - b);
|
|
909
|
+
const out = [];
|
|
910
|
+
for (let i = 0; i < k; i++) {
|
|
911
|
+
onStatus(`Proving withdrawal ${i + 1} of ${k}…`);
|
|
912
|
+
const n = notes[i];
|
|
913
|
+
const pieceAmount = amounts[i];
|
|
914
|
+
const change = n.amount - pieceAmount - pieceFee;
|
|
915
|
+
if (change < 0n)
|
|
916
|
+
throw new Error("The split came out short (the relayer's fee rose meanwhile). Your balance is intact: try again.");
|
|
917
|
+
const { proof, extData } = await buildTransaction({
|
|
918
|
+
tree: await this.treeFor(pool, [n]),
|
|
919
|
+
inputs: [n],
|
|
920
|
+
outputs: change > 0n ? [await this.changeOutput(change)] : [],
|
|
921
|
+
extAmount: -pieceAmount,
|
|
922
|
+
fee: pieceFee,
|
|
923
|
+
relayer: q.relayer,
|
|
924
|
+
recipient: isHype ? this.router : to,
|
|
925
|
+
...(isHype ? { data: encodeAbiParameters([{ type: "address" }], [to]) } : {}),
|
|
926
|
+
...(await this.dummy()),
|
|
927
|
+
prover: await this.prover(),
|
|
928
|
+
});
|
|
929
|
+
out.push({ call: { kind, pool, proof, extData }, notBefore: times[i], amount: pieceAmount, to });
|
|
930
|
+
}
|
|
931
|
+
return out;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Prove "I hold at least `threshold` of `token` in its vault" without revealing the balance, the notes or who you
|
|
935
|
+
* are (see funds.ts). Uses 1–3 of your notes; if the balance is spread over more, they're combined first (one
|
|
936
|
+
* relayed private send). `label` says who it's for and `expiresAt` (unix seconds) when it stops counting.
|
|
937
|
+
*/
|
|
938
|
+
async proveFunds(token, threshold, o) {
|
|
939
|
+
const onStatus = o.onStatus ?? (() => { });
|
|
940
|
+
if (threshold <= 0n)
|
|
941
|
+
throw new Error("Prove an amount above zero.");
|
|
942
|
+
if (!o.label.trim() || o.label.length > 80)
|
|
943
|
+
throw new Error("Say who the proof is for, in up to 80 characters.");
|
|
944
|
+
const isHype = token.toLowerCase() === this.hypeMarket.token.toLowerCase();
|
|
945
|
+
const pool = isHype ? this.hypeMarket.pool : this.market(token).pool;
|
|
946
|
+
const notes = await this.inputsFor(pool, threshold, onStatus);
|
|
947
|
+
if (notes.length > FUNDS_NOTES)
|
|
948
|
+
throw new Error("Your balance is in too many pieces: combine it first.");
|
|
949
|
+
const tree = await this.treeFor(pool, notes);
|
|
950
|
+
const root = await tree.root();
|
|
951
|
+
const paths = await Promise.all(notes.map((n) => tree.path(n.leafIndex)));
|
|
952
|
+
const block = await this.publicClient.getBlockNumber({ cacheTime: 0 });
|
|
953
|
+
const chainId = this.publicClient.chain?.id ?? (await this.publicClient.getChainId());
|
|
954
|
+
const context = fundsContext(chainId, pool, o.label, o.expiresAt);
|
|
955
|
+
const slot = (i, f, empty) => (i < notes.length ? f(notes[i]) : empty);
|
|
956
|
+
const idx = [0, 1, 2];
|
|
957
|
+
onStatus("Generating the proof on this device…");
|
|
958
|
+
const { proof, publicSignals } = await o.prover({
|
|
959
|
+
root,
|
|
960
|
+
threshold,
|
|
961
|
+
context,
|
|
962
|
+
nullifier: idx.map((i) => slot(i, (n) => n.nullifier, 0n)),
|
|
963
|
+
used: idx.map((i) => (i < notes.length ? 1n : 0n)),
|
|
964
|
+
amount: idx.map((i) => slot(i, (n) => n.amount, 0n)),
|
|
965
|
+
privKey: idx.map((i) => slot(i, (n) => n.privKey, 1n)),
|
|
966
|
+
blinding: idx.map((i) => slot(i, (n) => n.blinding, 0n)),
|
|
967
|
+
pathIndices: idx.map((i) => slot(i, (n) => BigInt(n.leafIndex), 0n)),
|
|
968
|
+
pathElements: idx.map((i) => (i < notes.length ? paths[i].pathElements : new Array(paths[0].pathElements.length).fill(0n))),
|
|
969
|
+
});
|
|
970
|
+
return {
|
|
971
|
+
v: 1,
|
|
972
|
+
chainId,
|
|
973
|
+
pool,
|
|
974
|
+
token: isHype ? this.hypeMarket.token : token,
|
|
975
|
+
symbol: o.symbol ?? (isHype ? "HYPE" : this.market(token).symbol ?? "tokens"),
|
|
976
|
+
decimals: o.decimals ?? (isHype ? 18 : this.market(token).decimals ?? 18),
|
|
977
|
+
threshold: threshold.toString(),
|
|
978
|
+
label: o.label,
|
|
979
|
+
expiresAt: o.expiresAt,
|
|
980
|
+
block: block.toString(),
|
|
981
|
+
root: publicSignals[0],
|
|
982
|
+
context: publicSignals[2],
|
|
983
|
+
nullifiers: publicSignals.slice(3, 3 + FUNDS_NOTES),
|
|
984
|
+
proof,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Move notes into a fresh note of your own (a relayed private send to yourself), so whoever holds a proof of funds
|
|
989
|
+
* that named their nullifiers can't tell when you later spend that money.
|
|
990
|
+
*/
|
|
991
|
+
async refreshNotes(pool, notes, onStatus = () => { }) {
|
|
992
|
+
const q = await this.relayFee("transact", pool);
|
|
993
|
+
const total = notes.reduce((a, x) => a + x.amount, 0n) - q.fee;
|
|
994
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
995
|
+
const { proof, extData } = await buildTransaction({ tree: await this.treeFor(pool, notes), inputs: notes, outputs: [await this.changeOutput(total)], fee: q.fee, relayer: q.relayer, ...(await this.dummy()), prover: await this.prover() });
|
|
996
|
+
onStatus("Relaying…");
|
|
997
|
+
return this.submit({ kind: "transact", pool, proof, extData });
|
|
998
|
+
}
|
|
745
999
|
/** Private tokens (a launch token, or an asset such as USDC) → any address. */
|
|
746
1000
|
async unshieldToken(token, to, amount, onStatus = () => { }) {
|
|
747
1001
|
const pool = this.market(token).pool;
|
package/dist/private/crypto.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export declare const LEVELS = 20;
|
|
|
4
4
|
export declare const MAX_NOTE_AMOUNT: bigint;
|
|
5
5
|
/** Key index of the main spending key; any other index is a one-time key used for a single public entry. */
|
|
6
6
|
export declare const MAIN_KEY = 4294967295;
|
|
7
|
+
/** One-time keys of private receive addresses: RECEIVE_KEY_BASE + n (random one-time keys stay below it in practice). */
|
|
8
|
+
export declare const RECEIVE_KEY_BASE = 3221225472;
|
|
9
|
+
export declare const RECEIVE_KEY_COUNT = 1073741823;
|
|
7
10
|
export declare const randomField: () => bigint;
|
|
8
11
|
export declare const randomKeyIndex: () => number;
|
|
9
12
|
export declare function poseidon(inputs: bigint[]): Promise<bigint>;
|
|
@@ -75,6 +78,18 @@ export declare class CloakKeys {
|
|
|
75
78
|
* bid can't be linked to the account's wallet. Derived from this account's seed, so the same signature always finds
|
|
76
79
|
* it again (on any device).
|
|
77
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Everything private receive address `n` needs, derived from the seed alone, so any device that unlocks the account
|
|
83
|
+
* finds the same address again: a one-time key (index RECEIVE_KEY_BASE + n), a blinding, the memo encrypted to this
|
|
84
|
+
* account's view key (deterministic, so the address is too) and the key that may sign a rescue for it.
|
|
85
|
+
*/
|
|
86
|
+
receive(n: number): Promise<{
|
|
87
|
+
keyIndex: number;
|
|
88
|
+
pubKey: bigint;
|
|
89
|
+
blinding: bigint;
|
|
90
|
+
memo: Hex;
|
|
91
|
+
rescueKey: Hex;
|
|
92
|
+
}>;
|
|
78
93
|
bidWalletKey(auction: Hex, n: number): Hex;
|
|
79
94
|
}
|
|
80
95
|
export interface Note {
|
package/dist/private/crypto.js
CHANGED
|
@@ -10,9 +10,12 @@ export const LEVELS = 20;
|
|
|
10
10
|
export const MAX_NOTE_AMOUNT = 2n ** 128n;
|
|
11
11
|
/** Key index of the main spending key; any other index is a one-time key used for a single public entry. */
|
|
12
12
|
export const MAIN_KEY = 0xffffffff;
|
|
13
|
+
/** One-time keys of private receive addresses: RECEIVE_KEY_BASE + n (random one-time keys stay below it in practice). */
|
|
14
|
+
export const RECEIVE_KEY_BASE = 0xc0000000;
|
|
15
|
+
export const RECEIVE_KEY_COUNT = 0x3fffffff;
|
|
13
16
|
const fieldMod = (x) => ((x % FIELD_SIZE) + FIELD_SIZE) % FIELD_SIZE;
|
|
14
17
|
export const randomField = () => BigInt(toHex(randomBytes(31)));
|
|
15
|
-
export const randomKeyIndex = () => Number(BigInt(toHex(randomBytes(4))) %
|
|
18
|
+
export const randomKeyIndex = () => Number(BigInt(toHex(randomBytes(4))) % 0xc0000000n); // below RECEIVE_KEY_BASE
|
|
16
19
|
let _poseidon;
|
|
17
20
|
export async function poseidon(inputs) {
|
|
18
21
|
_poseidon ??= await (await import("circomlibjs")).buildPoseidon(); // loaded on first use
|
|
@@ -82,6 +85,21 @@ export class CloakKeys {
|
|
|
82
85
|
* bid can't be linked to the account's wallet. Derived from this account's seed, so the same signature always finds
|
|
83
86
|
* it again (on any device).
|
|
84
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* Everything private receive address `n` needs, derived from the seed alone, so any device that unlocks the account
|
|
90
|
+
* finds the same address again: a one-time key (index RECEIVE_KEY_BASE + n), a blinding, the memo encrypted to this
|
|
91
|
+
* account's view key (deterministic, so the address is too) and the key that may sign a rescue for it.
|
|
92
|
+
*/
|
|
93
|
+
async receive(n) {
|
|
94
|
+
if (!Number.isInteger(n) || n < 0 || n >= RECEIVE_KEY_COUNT)
|
|
95
|
+
throw new Error("receive address number out of range");
|
|
96
|
+
const tag = (t) => toBytes(keccak256(concat([toBytes(t), this.seed, toBytes(toHex(n, { size: 4 }))])));
|
|
97
|
+
const keyIndex = RECEIVE_KEY_BASE + n;
|
|
98
|
+
const k = await this.key(keyIndex);
|
|
99
|
+
const blinding = fieldMod(BigInt(toHex(tag("cloak-receive-blinding-v1"))));
|
|
100
|
+
const memo = toHex(seal(this.encPub, concat([toBytes(toHex(blinding, { size: 32 })), u32(keyIndex)]), tag("cloak-receive-eph-v1"), tag("cloak-receive-nonce-v1").slice(0, 24)));
|
|
101
|
+
return { keyIndex, pubKey: k.pubKey, blinding, memo, rescueKey: toHex(tag("cloak-receive-rescue-v1")) };
|
|
102
|
+
}
|
|
85
103
|
bidWalletKey(auction, n) {
|
|
86
104
|
return keccak256(concat([toBytes("cloak-bidwallet-v1"), this.seed, toBytes(auction.toLowerCase()), toBytes(toHex(n, { size: 4 }))]));
|
|
87
105
|
}
|
|
@@ -97,10 +115,8 @@ export async function nullifierOf(n, leafIndex, privKey) {
|
|
|
97
115
|
// 0x02 | amount(16) | eph(32) | nonce(24) | enc(blinding32 ‖ keyIndex4) + tag(16) — notes created by the router
|
|
98
116
|
// (the router prepends the public amount; the client only encrypts blinding + key index)
|
|
99
117
|
const INFO = toBytes("cloak-note-v1");
|
|
100
|
-
function seal(encPub, pt) {
|
|
101
|
-
const eph = randomBytes(32);
|
|
118
|
+
function seal(encPub, pt, eph = randomBytes(32), nonce = randomBytes(24)) {
|
|
102
119
|
const key = hkdf(sha256, x25519.getSharedSecret(eph, encPub), undefined, INFO, 32);
|
|
103
|
-
const nonce = randomBytes(24);
|
|
104
120
|
return concat([x25519.getPublicKey(eph), nonce, xchacha20poly1305(key, nonce).encrypt(pt)]);
|
|
105
121
|
}
|
|
106
122
|
function open(encPriv, b) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Address, type PublicClient } from "viem";
|
|
2
|
+
import type { RawGroth16 } from "./prover.js";
|
|
3
|
+
import { FUNDS_VERIFYING_KEY } from "./fundsVk.js";
|
|
4
|
+
export { FUNDS_VERIFYING_KEY };
|
|
5
|
+
/**
|
|
6
|
+
* Proof of funds: "this vault holds at least `threshold` for me, as of block `block`", from up to 3 of the prover's
|
|
7
|
+
* notes, without the balance, the notes or the owner. The proof reveals those notes' nullifiers, so the verifier can
|
|
8
|
+
* check none was spent; the flip side is that the verifier could notice when they're spent later, which is why the
|
|
9
|
+
* app offers to refresh them (a private send to yourself) after proving.
|
|
10
|
+
*
|
|
11
|
+
* `context` binds a proof to one purpose: the chain, the vault, a free-text label (who it's for) and an expiry. A proof
|
|
12
|
+
* shown with a different label or after its expiry fails.
|
|
13
|
+
*/
|
|
14
|
+
export declare const FUNDS_NOTES = 3;
|
|
15
|
+
export interface FundsProof {
|
|
16
|
+
v: 1;
|
|
17
|
+
chainId: number;
|
|
18
|
+
pool: Address;
|
|
19
|
+
/** the vault's asset, for display (WHYPE for HYPE) */
|
|
20
|
+
token: Address;
|
|
21
|
+
symbol: string;
|
|
22
|
+
decimals: number;
|
|
23
|
+
threshold: string;
|
|
24
|
+
label: string;
|
|
25
|
+
/** unix seconds */
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
/** the block the notes were checked unspent at, when proving */
|
|
28
|
+
block: string;
|
|
29
|
+
root: string;
|
|
30
|
+
context: string;
|
|
31
|
+
nullifiers: string[];
|
|
32
|
+
proof: RawGroth16;
|
|
33
|
+
}
|
|
34
|
+
export declare function fundsContext(chainId: number, pool: Address, label: string, expiresAt: number): bigint;
|
|
35
|
+
/** Public signals in the circuit's order: root, threshold, context, nullifier[3]. */
|
|
36
|
+
export declare const fundsSignals: (p: FundsProof) => string[];
|
|
37
|
+
/** A proof as a short shareable string (base64url JSON), for a link like `cloak.boblabs.tech/proof#<this>`. */
|
|
38
|
+
export declare function encodeFundsProof(p: FundsProof): string;
|
|
39
|
+
export declare function decodeFundsProof(s: string): FundsProof;
|
|
40
|
+
export interface FundsCheck {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
checks: {
|
|
43
|
+
name: string;
|
|
44
|
+
ok: boolean;
|
|
45
|
+
detail: string;
|
|
46
|
+
}[];
|
|
47
|
+
/** a backing note was spent after the proof's block (the claim held when made, not now) */
|
|
48
|
+
spentSince: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check a proof of funds: the zero-knowledge proof itself (against `vk`, by default the key built into the SDK), that it's bound
|
|
52
|
+
* to this chain, vault, label and expiry, that the vault is a Cloak vault and knows the root, and that no backing note
|
|
53
|
+
* was spent as of the proof's block. A note spent since is reported, not failed: the claim was true when made.
|
|
54
|
+
*/
|
|
55
|
+
export declare function verifyFundsProof(publicClient: PublicClient, p: FundsProof, vk?: unknown, opts?: {
|
|
56
|
+
vaults?: Address;
|
|
57
|
+
now?: number;
|
|
58
|
+
}): Promise<FundsCheck>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { encodeAbiParameters, keccak256 } from "viem";
|
|
2
|
+
import { cloakVaultsAbi, shieldedPoolAbi } from "../abis.js";
|
|
3
|
+
import { FIELD_SIZE } from "./crypto.js";
|
|
4
|
+
import { FUNDS_VERIFYING_KEY } from "./fundsVk.js";
|
|
5
|
+
export { FUNDS_VERIFYING_KEY };
|
|
6
|
+
/**
|
|
7
|
+
* Proof of funds: "this vault holds at least `threshold` for me, as of block `block`", from up to 3 of the prover's
|
|
8
|
+
* notes, without the balance, the notes or the owner. The proof reveals those notes' nullifiers, so the verifier can
|
|
9
|
+
* check none was spent; the flip side is that the verifier could notice when they're spent later, which is why the
|
|
10
|
+
* app offers to refresh them (a private send to yourself) after proving.
|
|
11
|
+
*
|
|
12
|
+
* `context` binds a proof to one purpose: the chain, the vault, a free-text label (who it's for) and an expiry. A proof
|
|
13
|
+
* shown with a different label or after its expiry fails.
|
|
14
|
+
*/
|
|
15
|
+
export const FUNDS_NOTES = 3;
|
|
16
|
+
export function fundsContext(chainId, pool, label, expiresAt) {
|
|
17
|
+
return BigInt(keccak256(encodeAbiParameters([{ type: "string" }, { type: "uint256" }, { type: "address" }, { type: "string" }, { type: "uint64" }], ["cloak-funds-v1", BigInt(chainId), pool, label, BigInt(expiresAt)]))) % FIELD_SIZE;
|
|
18
|
+
}
|
|
19
|
+
/** Public signals in the circuit's order: root, threshold, context, nullifier[3]. */
|
|
20
|
+
export const fundsSignals = (p) => [p.root, p.threshold, p.context, ...p.nullifiers];
|
|
21
|
+
/** A proof as a short shareable string (base64url JSON), for a link like `cloak.boblabs.tech/proof#<this>`. */
|
|
22
|
+
export function encodeFundsProof(p) {
|
|
23
|
+
const b = new TextEncoder().encode(JSON.stringify(p));
|
|
24
|
+
return btoa(Array.from(b, (c) => String.fromCharCode(c)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
|
+
}
|
|
26
|
+
export function decodeFundsProof(s) {
|
|
27
|
+
const b = Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
28
|
+
const p = JSON.parse(new TextDecoder().decode(b));
|
|
29
|
+
if (p?.v !== 1 || !Array.isArray(p.nullifiers) || p.nullifiers.length !== FUNDS_NOTES)
|
|
30
|
+
throw new Error("not a Cloak proof of funds");
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check a proof of funds: the zero-knowledge proof itself (against `vk`, by default the key built into the SDK), that it's bound
|
|
35
|
+
* to this chain, vault, label and expiry, that the vault is a Cloak vault and knows the root, and that no backing note
|
|
36
|
+
* was spent as of the proof's block. A note spent since is reported, not failed: the claim was true when made.
|
|
37
|
+
*/
|
|
38
|
+
export async function verifyFundsProof(publicClient, p, vk = FUNDS_VERIFYING_KEY, opts = {}) {
|
|
39
|
+
const checks = [];
|
|
40
|
+
const add = (name, ok, detail) => checks.push({ name, ok, detail });
|
|
41
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
42
|
+
const chainId = publicClient.chain?.id ?? (await publicClient.getChainId());
|
|
43
|
+
add("Right network", chainId === p.chainId, chainId === p.chainId ? "The proof is for this chain." : `The proof is for chain ${p.chainId}.`);
|
|
44
|
+
const ctx = fundsContext(p.chainId, p.pool, p.label, p.expiresAt).toString();
|
|
45
|
+
add("Made for this purpose", ctx === p.context, ctx === p.context ? `Label "${p.label}".` : "The label, vault or expiry was changed after proving.");
|
|
46
|
+
add("Not expired", p.expiresAt > now, p.expiresAt > now ? `Valid until ${new Date(p.expiresAt * 1000).toISOString().slice(0, 16).replace("T", " ")} UTC.` : "This proof has expired.");
|
|
47
|
+
const snarkjs = await import("snarkjs");
|
|
48
|
+
const valid = await snarkjs.groth16.verify(vk, fundsSignals(p), p.proof).catch(() => false);
|
|
49
|
+
add("Valid zero-knowledge proof", valid, valid ? "The math checks out: the notes exist and add up." : "The proof doesn't verify.");
|
|
50
|
+
if (opts.vaults) {
|
|
51
|
+
const isPool = await publicClient.readContract({ address: opts.vaults, abi: cloakVaultsAbi, functionName: "isPool", args: [p.pool] }).catch(() => false);
|
|
52
|
+
add("A Cloak vault", isPool, isPool ? "The notes are in one of Cloak's permanent vaults." : "That address isn't a Cloak vault.");
|
|
53
|
+
}
|
|
54
|
+
const rootOk = await publicClient.readContract({ address: p.pool, abi: shieldedPoolAbi, functionName: "isKnownRoot", args: [BigInt(p.root)] }).catch(() => false);
|
|
55
|
+
add("Real vault state", rootOk, rootOk ? "The vault knows the tree the notes are in." : "The vault doesn't know this tree.");
|
|
56
|
+
const live = p.nullifiers.filter((n) => n !== "0");
|
|
57
|
+
const block = BigInt(p.block);
|
|
58
|
+
const spentAt = (n, blockNumber) => publicClient.readContract({ address: p.pool, abi: shieldedPoolAbi, functionName: "isSpent", args: [BigInt(n)], ...(blockNumber ? { blockNumber } : {}) });
|
|
59
|
+
const atBlock = await Promise.all(live.map((n) => spentAt(n, block).catch(() => spentAt(n))));
|
|
60
|
+
const unspentThen = atBlock.every((x) => !x);
|
|
61
|
+
add("Unspent when proved", unspentThen && live.length > 0, unspentThen ? `None of the ${live.length} backing note${live.length > 1 ? "s" : ""} had been spent at block ${block}.` : "A backing note had already been spent when the proof was made.");
|
|
62
|
+
const nowSpent = await Promise.all(live.map((n) => spentAt(n).catch(() => false)));
|
|
63
|
+
const spentSince = nowSpent.some(Boolean);
|
|
64
|
+
return { ok: checks.every((c) => c.ok), checks, spentSince };
|
|
65
|
+
}
|