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