@cloakely/sdk 0.7.0 → 0.7.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.
@@ -48,6 +48,8 @@ export interface CloakDeployment {
48
48
  sharedBlock?: bigint;
49
49
  /** SharedRouter: private buys, sells and hidden sends for the shared vault. */
50
50
  sharedRouter?: Address;
51
+ /** SharedBatcher: batch rounds for the shared vault's tokens. */
52
+ sharedBatcher?: Address;
51
53
  hypePool: Address;
52
54
  whype: Address;
53
55
  /** Hosted relayer (POST /relay, GET /quote). */
@@ -21,6 +21,7 @@ export const deployments = {
21
21
  shared: "0x0ce2ecb0195e07899377e3d7b01e927ab3f87610",
22
22
  sharedBlock: 21426599n,
23
23
  sharedRouter: "0xe2a0769a5acbd987c1b6725247c56187a112dc7e",
24
+ sharedBatcher: "0xdc725bdeb0fca923bf2dc65232f54463b0f80820",
24
25
  router: "0x343ab9a7aac93cee28321410510ea24417cc6386",
25
26
  routerBlock: 19485063n,
26
27
  batcher: "0xc69e4bca18020ae93bc9f17093985bfb139a3203",
@@ -103,6 +104,7 @@ export async function liveDeployment(publicClient, base) {
103
104
  shared: reg.shared ?? base.shared,
104
105
  sharedBlock: reg.sharedBlock ?? base.sharedBlock,
105
106
  sharedRouter: reg.sharedRouter ?? base.sharedRouter,
107
+ sharedBatcher: reg.sharedBatcher ?? base.sharedBatcher,
106
108
  hypePool,
107
109
  whype,
108
110
  previous: reg.previous.length
@@ -39,7 +39,7 @@ export declare const SHARED_SWAP_DATA_ABI: readonly [{
39
39
  readonly type: "uint64";
40
40
  }];
41
41
  }];
42
- export type SharedRelayKind = "sharedTransact" | "sharedBuy" | "sharedSell" | "sharedSend";
42
+ export type SharedRelayKind = "sharedTransact" | "sharedBuy" | "sharedSell" | "sharedSend" | "sharedOrderBuy" | "sharedOrderSell";
43
43
  export interface SharedRelayCall {
44
44
  kind: SharedRelayKind;
45
45
  /** The shared-vault proof (or, for sharedBuy, the HYPE vault proof). */
@@ -56,7 +56,7 @@ export type SharedSubmitter = ((call: SharedRelayCall) => Promise<Hex>) & {
56
56
  /** POST shared-vault calls to a Cloak relayer (`services/keeper` serves `/relay-shared` and `/quote-shared`). */
57
57
  export declare function sharedRelayerSubmitter(url: string): SharedSubmitter;
58
58
  /** Submit shared-vault calls from a wallet (that wallet pays gas and appears as the sender). For tests and relayers. */
59
- export declare function sharedWalletSubmitter(priv: CloakPrivate, wallet: Wallet, vault: Address, router: Address): SharedSubmitter;
59
+ export declare function sharedWalletSubmitter(priv: CloakPrivate, wallet: Wallet, vault: Address, router: Address, batcher?: Address): SharedSubmitter;
60
60
  export interface SharedToken {
61
61
  token: Address;
62
62
  market: Address;
@@ -69,12 +69,16 @@ export declare class SharedVaultClient {
69
69
  readonly prover: () => Promise<Prover>;
70
70
  readonly submit: SharedSubmitter;
71
71
  readonly fromBlock: bigint;
72
+ /** SharedBatcher: batch rounds that hide trade size for these tokens. */
73
+ readonly batcher?: Address | undefined;
72
74
  tokens: SharedToken[];
73
75
  private leaves;
74
76
  private scannedUpTo;
75
77
  private notes;
76
78
  private nextBlock;
77
- constructor(priv: CloakPrivate, vault: Address, router: Address, prover: () => Promise<Prover>, submit: SharedSubmitter, fromBlock?: bigint);
79
+ constructor(priv: CloakPrivate, vault: Address, router: Address, prover: () => Promise<Prover>, submit: SharedSubmitter, fromBlock?: bigint,
80
+ /** SharedBatcher: batch rounds that hide trade size for these tokens. */
81
+ batcher?: Address | undefined);
78
82
  private get pub();
79
83
  /** Every token admitted to the vault, with its Uniswap market. */
80
84
  loadTokens(): Promise<SharedToken[]>;
@@ -124,4 +128,27 @@ export declare class SharedVaultClient {
124
128
  send(token: Address, amount: bigint, to: string, onStatus?: Status): Promise<Hex>;
125
129
  /** Private `token` → `to` (any address). Names the token, as any withdrawal must. */
126
130
  unshield(token: Address, amount: bigint, to: Address, onStatus?: Status): Promise<Hex>;
131
+ roundInfo(): Promise<{
132
+ round: bigint;
133
+ endsAt: number;
134
+ roundSecs: number;
135
+ }>;
136
+ /**
137
+ * Batched private buy or sell of a shared-vault token: the amount is split into standard sizes (1, 2, 5 × 10ⁿ), each
138
+ * placed by the relayer into the current round. When the round ends, buys and sells cross at the pool's average price
139
+ * and only the net trades on Uniswap; outputs arrive as private notes. Nobody can tell how much any one trader moved.
140
+ * Any remainder that isn't a standard size stays in the private balance.
141
+ */
142
+ batch(token: Address, side: "buy" | "sell", amount: bigint, opts?: {
143
+ maxOrders?: number;
144
+ onStatus?: Status;
145
+ }): Promise<{
146
+ orders: {
147
+ id: bigint;
148
+ size: bigint;
149
+ round: bigint;
150
+ txHash: Hex;
151
+ }[];
152
+ rest: bigint;
153
+ }>;
127
154
  }
@@ -1,9 +1,9 @@
1
- import { encodeAbiParameters, formatEther, keccak256, zeroAddress } from "viem";
2
- import { cloakAuctionAbi, cloakPairAbi, sharedRouterAbi, sharedVaultAbi } from "../abis.js";
1
+ import { encodeAbiParameters, formatEther, keccak256, parseEventLogs, zeroAddress } from "viem";
2
+ import { cloakAuctionAbi, cloakPairAbi, sharedBatcherAbi, sharedRouterAbi, sharedVaultAbi } from "../abis.js";
3
3
  import { Cloak } from "../cloak.js";
4
4
  import { MAIN_KEY, encryptFullMemo, encryptRouterMemo, noteCommitment, nullifierOf, parseCloakAddress, randomField, randomKeyIndex, readMemo } from "./crypto.js";
5
5
  import { MerkleTree } from "./tree.js";
6
- import { buildTransaction, selectNotes } from "./tx.js";
6
+ import { ORDER_DATA_ABI, buildTransaction, selectNotes, splitStandard } from "./tx.js";
7
7
  /**
8
8
  * The shared launch-token vault (testnet experiment): one private vault for every Cloak launch token that opens on
9
9
  * Uniswap from now on, next to the single-token vaults of HYPE and USDC.
@@ -52,15 +52,21 @@ export function sharedRelayerSubmitter(url) {
52
52
  return submit;
53
53
  }
54
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) {
55
+ export function sharedWalletSubmitter(priv, wallet, vault, router, batcher) {
56
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] };
57
+ if ((call.kind === "sharedOrderBuy" || call.kind === "sharedOrderSell") && !batcher)
58
+ throw new Error("no shared batcher configured");
59
+ const req = call.kind === "sharedOrderBuy"
60
+ ? { address: batcher, abi: sharedBatcherAbi, functionName: "placeBuy", args: [call.proof, call.extData] }
61
+ : call.kind === "sharedOrderSell"
62
+ ? { address: batcher, abi: sharedBatcherAbi, functionName: "placeSell", args: [call.proof, call.extData] }
63
+ : call.kind === "sharedTransact"
64
+ ? { address: vault, abi: sharedVaultAbi, functionName: "transact", args: [call.proof, call.extData] }
65
+ : call.kind === "sharedBuy"
66
+ ? { address: router, abi: sharedRouterAbi, functionName: "buy", args: [call.proof, call.extData] }
67
+ : call.kind === "sharedSell"
68
+ ? { address: router, abi: sharedRouterAbi, functionName: "sell", args: [call.proof, call.extData] }
69
+ : { address: router, abi: sharedRouterAbi, functionName: "send", args: [call.proof, call.extData, call.feeProof, call.feeExtData] };
64
70
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
71
  const est = await priv.publicClient.estimateContractGas({ ...req, account: wallet.account.address });
66
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -86,18 +92,22 @@ export class SharedVaultClient {
86
92
  prover;
87
93
  submit;
88
94
  fromBlock;
95
+ batcher;
89
96
  tokens = [];
90
97
  leaves = [];
91
98
  scannedUpTo = 0;
92
99
  notes = [];
93
100
  nextBlock;
94
- constructor(priv, vault, router, prover, submit, fromBlock = 0n) {
101
+ constructor(priv, vault, router, prover, submit, fromBlock = 0n,
102
+ /** SharedBatcher: batch rounds that hide trade size for these tokens. */
103
+ batcher) {
95
104
  this.priv = priv;
96
105
  this.vault = vault;
97
106
  this.router = router;
98
107
  this.prover = prover;
99
108
  this.submit = submit;
100
109
  this.fromBlock = fromBlock;
110
+ this.batcher = batcher;
101
111
  this.nextBlock = fromBlock;
102
112
  }
103
113
  get pub() {
@@ -456,4 +466,93 @@ export class SharedVaultClient {
456
466
  onStatus("Relaying…");
457
467
  return this.submit({ kind: "sharedTransact", proof, extData });
458
468
  }
469
+ // ───────────── batch rounds (hide trade size) ─────────────
470
+ async roundInfo() {
471
+ if (!this.batcher)
472
+ throw new Error("no shared batcher configured");
473
+ const c = { address: this.batcher, abi: sharedBatcherAbi };
474
+ const [round, roundSecs] = await Promise.all([this.pub.readContract({ ...c, functionName: "currentRound" }), this.pub.readContract({ ...c, functionName: "roundSecs" })]);
475
+ return { round, roundSecs: Number(roundSecs), endsAt: Number((round + 1n) * roundSecs) };
476
+ }
477
+ /**
478
+ * Batched private buy or sell of a shared-vault token: the amount is split into standard sizes (1, 2, 5 × 10ⁿ), each
479
+ * placed by the relayer into the current round. When the round ends, buys and sells cross at the pool's average price
480
+ * and only the net trades on Uniswap; outputs arrive as private notes. Nobody can tell how much any one trader moved.
481
+ * Any remainder that isn't a standard size stays in the private balance.
482
+ */
483
+ async batch(token, side, amount, opts = {}) {
484
+ if (!this.batcher)
485
+ throw new Error("no shared batcher configured");
486
+ if (!this.tokens.length)
487
+ await this.loadTokens();
488
+ this.info(token);
489
+ const onStatus = opts.onStatus ?? (() => { });
490
+ const asset = asNum(token);
491
+ const kind = side === "buy" ? "sharedOrderBuy" : "sharedOrderSell";
492
+ const hypePool = this.priv.hypeMarket.pool;
493
+ const maxOrders = opts.maxOrders ?? 6;
494
+ let { sizes, rest } = splitStandard(amount, maxOrders);
495
+ const fee = (await this.quote(kind, token)).fee;
496
+ if (fee > 0n) {
497
+ const have = side === "buy" ? await this.priv.balance(hypePool) : await this.balance(token);
498
+ const reserve = (n) => (fee * BigInt(n) * 5n) / 4n + fee;
499
+ if (sizes.reduce((a, x) => a + x, 0n) + reserve(sizes.length) > have) {
500
+ // every order pays its own relayer fee: use fewer orders when that's all the balance can pay for
501
+ let fit = false;
502
+ for (let n = maxOrders; n >= 1 && !fit; n--) {
503
+ const budget = have - reserve(n);
504
+ if (budget <= 0n)
505
+ continue;
506
+ ({ sizes } = splitStandard(budget < amount ? budget : amount, n));
507
+ fit = sizes.length > 0;
508
+ }
509
+ if (!fit)
510
+ throw new Error("insufficient private balance");
511
+ rest = amount - sizes.reduce((a, x) => a + x, 0n);
512
+ }
513
+ }
514
+ if (sizes.length === 0)
515
+ throw new Error("amount too small for a standard-size order");
516
+ const placed = [];
517
+ for (let i = 0; i < sizes.length; i++) {
518
+ const size = sizes[i];
519
+ onStatus(`Order ${i + 1}/${sizes.length}: syncing…`);
520
+ const q = await this.quote(kind, token);
521
+ let inputs;
522
+ try {
523
+ inputs = side === "buy" ? await this.priv.inputsFor(hypePool, size + q.fee, onStatus) : await this.inputsFor(token, size + q.fee, onStatus);
524
+ }
525
+ catch (e) {
526
+ if (!placed.length || !/insufficient private balance/.test(String(e.message)))
527
+ throw e;
528
+ rest += sizes.slice(i).reduce((a, x) => a + x, 0n);
529
+ break;
530
+ }
531
+ const keyIndex = randomKeyIndex();
532
+ const outKey = await this.priv.keys.key(keyIndex);
533
+ const outBlinding = randomField();
534
+ const data = encodeAbiParameters(ORDER_DATA_ABI, [{ token, outPubKey: outKey.pubKey, outBlinding, outMemo: encryptRouterMemo(this.priv.keys.encPub, outBlinding, keyIndex) }]);
535
+ const change = inputs.reduce((a, x) => a + x.amount, 0n) - size - q.fee;
536
+ onStatus(`Order ${i + 1}/${sizes.length}: proving on this device…`);
537
+ const common = {
538
+ inputs,
539
+ outputs: change > 0n ? [await this.priv.changeOutput(change)] : [],
540
+ extAmount: -size,
541
+ fee: q.fee,
542
+ relayer: q.relayer,
543
+ recipient: this.batcher,
544
+ data,
545
+ ...(await this.priv.dummy()),
546
+ };
547
+ const { proof, extData } = side === "buy"
548
+ ? await buildTransaction({ ...common, tree: await this.priv.treeFor(hypePool, inputs), prover: await this.priv.prover() })
549
+ : await buildTransaction({ ...common, tree: await this.tree(inputs[0].epoch ?? 0), asset, publicAsset: asset, prover: await this.prover() });
550
+ onStatus(`Order ${i + 1}/${sizes.length}: relaying…`);
551
+ const txHash = await this.submit({ kind, proof, extData });
552
+ const rc = await this.pub.waitForTransactionReceipt({ hash: txHash });
553
+ const ev = parseEventLogs({ abi: sharedBatcherAbi, logs: rc.logs, eventName: "OrderPlaced" })[0];
554
+ placed.push({ id: ev.args.id, size, round: ev.args.round, txHash });
555
+ }
556
+ return { orders: placed, rest };
557
+ }
459
558
  }
@@ -42,6 +42,8 @@ export interface CloakRegistry {
42
42
  sharedBlock?: bigint;
43
43
  /** SharedRouter: private buys, sells and hidden sends for the shared vault. */
44
44
  sharedRouter?: Address;
45
+ /** SharedBatcher: batch rounds for the shared vault's tokens. */
46
+ sharedBatcher?: Address;
45
47
  factory: Address;
46
48
  factoryBlock: bigint;
47
49
  /** Earlier factories of the current version (a factory added next to the first), newest first. */
package/dist/registry.js CHANGED
@@ -35,6 +35,7 @@ export async function readRegistry(publicClient, config) {
35
35
  shared: newest("shared")?.addr,
36
36
  sharedBlock: newest("shared")?.startBlock,
37
37
  sharedRouter: newest("sharedRouter")?.addr,
38
+ sharedBatcher: newest("sharedBatcher")?.addr,
38
39
  factory: factory.addr,
39
40
  factoryBlock: factory.startBlock,
40
41
  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.7.0",
3
+ "version": "0.7.1",
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",