@cloak.ag/sdk 1.0.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 +180 -0
- package/dist/index.cjs +3707 -0
- package/dist/index.d.cts +1990 -0
- package/dist/index.d.ts +1990 -0
- package/dist/index.js +3595 -0
- package/package.json +91 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3707 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ArtifactProverService: () => ArtifactProverService,
|
|
34
|
+
CLOAK_PROGRAM_ID: () => CLOAK_PROGRAM_ID,
|
|
35
|
+
CloakError: () => CloakError,
|
|
36
|
+
CloakSDK: () => CloakSDK,
|
|
37
|
+
DepositRecoveryService: () => DepositRecoveryService,
|
|
38
|
+
FIXED_FEE_LAMPORTS: () => FIXED_FEE_LAMPORTS,
|
|
39
|
+
IndexerService: () => IndexerService,
|
|
40
|
+
LAMPORTS_PER_SOL: () => LAMPORTS_PER_SOL,
|
|
41
|
+
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
42
|
+
MemoryStorageAdapter: () => MemoryStorageAdapter,
|
|
43
|
+
ProverService: () => ProverService,
|
|
44
|
+
RelayService: () => RelayService,
|
|
45
|
+
VARIABLE_FEE_RATE: () => VARIABLE_FEE_RATE,
|
|
46
|
+
VERSION: () => VERSION,
|
|
47
|
+
bigintToBytes32: () => bigintToBytes32,
|
|
48
|
+
buildPublicInputsBytes: () => buildPublicInputsBytes,
|
|
49
|
+
bytesToHex: () => bytesToHex,
|
|
50
|
+
calculateFee: () => calculateFee2,
|
|
51
|
+
calculateRelayFee: () => calculateRelayFee,
|
|
52
|
+
computeCommitment: () => computeCommitment,
|
|
53
|
+
computeMerkleRoot: () => computeMerkleRoot,
|
|
54
|
+
computeNullifier: () => computeNullifier,
|
|
55
|
+
computeNullifierAsync: () => computeNullifierAsync,
|
|
56
|
+
computeNullifierSync: () => computeNullifierSync,
|
|
57
|
+
computeOutputsHash: () => computeOutputsHash,
|
|
58
|
+
computeOutputsHashAsync: () => computeOutputsHashAsync,
|
|
59
|
+
computeOutputsHashSync: () => computeOutputsHashSync,
|
|
60
|
+
computeSwapOutputsHash: () => computeSwapOutputsHash,
|
|
61
|
+
computeSwapOutputsHashAsync: () => computeSwapOutputsHashAsync,
|
|
62
|
+
computeSwapOutputsHashSync: () => computeSwapOutputsHashSync,
|
|
63
|
+
copyNoteToClipboard: () => copyNoteToClipboard,
|
|
64
|
+
createCloakError: () => createCloakError,
|
|
65
|
+
createDepositInstruction: () => createDepositInstruction,
|
|
66
|
+
deriveSpendKey: () => deriveSpendKey,
|
|
67
|
+
deriveViewKey: () => deriveViewKey,
|
|
68
|
+
detectNetworkFromRpcUrl: () => detectNetworkFromRpcUrl,
|
|
69
|
+
downloadNote: () => downloadNote,
|
|
70
|
+
encodeNoteSimple: () => encodeNoteSimple,
|
|
71
|
+
encryptNoteForRecipient: () => encryptNoteForRecipient,
|
|
72
|
+
exportKeys: () => exportKeys,
|
|
73
|
+
exportNote: () => exportNote,
|
|
74
|
+
exportWalletKeys: () => exportWalletKeys,
|
|
75
|
+
filterNotesByNetwork: () => filterNotesByNetwork,
|
|
76
|
+
filterWithdrawableNotes: () => filterWithdrawableNotes,
|
|
77
|
+
findNoteByCommitment: () => findNoteByCommitment,
|
|
78
|
+
formatAmount: () => formatAmount,
|
|
79
|
+
formatErrorForLogging: () => formatErrorForLogging,
|
|
80
|
+
generateCloakKeys: () => generateCloakKeys,
|
|
81
|
+
generateCommitment: () => generateCommitment,
|
|
82
|
+
generateCommitmentAsync: () => generateCommitmentAsync,
|
|
83
|
+
generateMasterSeed: () => generateMasterSeed,
|
|
84
|
+
generateNote: () => generateNote,
|
|
85
|
+
generateNoteFromWallet: () => generateNoteFromWallet,
|
|
86
|
+
getAddressExplorerUrl: () => getAddressExplorerUrl,
|
|
87
|
+
getDistributableAmount: () => getDistributableAmount2,
|
|
88
|
+
getExplorerUrl: () => getExplorerUrl,
|
|
89
|
+
getPublicKey: () => getPublicKey,
|
|
90
|
+
getPublicViewKey: () => getPublicViewKey,
|
|
91
|
+
getRecipientAmount: () => getRecipientAmount,
|
|
92
|
+
getRpcUrlForNetwork: () => getRpcUrlForNetwork,
|
|
93
|
+
getShieldPoolPDAs: () => getShieldPoolPDAs,
|
|
94
|
+
getViewKey: () => getViewKey,
|
|
95
|
+
hexToBigint: () => hexToBigint,
|
|
96
|
+
hexToBytes: () => hexToBytes,
|
|
97
|
+
importKeys: () => importKeys,
|
|
98
|
+
importWalletKeys: () => importWalletKeys,
|
|
99
|
+
isValidHex: () => isValidHex,
|
|
100
|
+
isValidRpcUrl: () => isValidRpcUrl,
|
|
101
|
+
isValidSolanaAddress: () => isValidSolanaAddress,
|
|
102
|
+
isWithdrawable: () => isWithdrawable,
|
|
103
|
+
keypairToAdapter: () => keypairToAdapter,
|
|
104
|
+
parseAmount: () => parseAmount,
|
|
105
|
+
parseNote: () => parseNote,
|
|
106
|
+
parseTransactionError: () => parseTransactionError,
|
|
107
|
+
poseidonHash: () => poseidonHash,
|
|
108
|
+
prepareEncryptedOutput: () => prepareEncryptedOutput,
|
|
109
|
+
prepareEncryptedOutputForRecipient: () => prepareEncryptedOutputForRecipient,
|
|
110
|
+
proofToBytes: () => proofToBytes,
|
|
111
|
+
pubkeyToLimbs: () => pubkeyToLimbs,
|
|
112
|
+
randomBytes: () => randomBytes,
|
|
113
|
+
scanNotesForWallet: () => scanNotesForWallet,
|
|
114
|
+
sendTransaction: () => sendTransaction,
|
|
115
|
+
serializeNote: () => serializeNote,
|
|
116
|
+
signTransaction: () => signTransaction,
|
|
117
|
+
splitTo2Limbs: () => splitTo2Limbs,
|
|
118
|
+
tryDecryptNote: () => tryDecryptNote,
|
|
119
|
+
updateNoteWithDeposit: () => updateNoteWithDeposit,
|
|
120
|
+
validateDepositParams: () => validateDepositParams,
|
|
121
|
+
validateNote: () => validateNote,
|
|
122
|
+
validateOutputsSum: () => validateOutputsSum,
|
|
123
|
+
validateTransfers: () => validateTransfers,
|
|
124
|
+
validateWalletConnected: () => validateWalletConnected,
|
|
125
|
+
validateWithdrawableNote: () => validateWithdrawableNote
|
|
126
|
+
});
|
|
127
|
+
module.exports = __toCommonJS(index_exports);
|
|
128
|
+
|
|
129
|
+
// src/core/CloakSDK.ts
|
|
130
|
+
var import_web36 = require("@solana/web3.js");
|
|
131
|
+
|
|
132
|
+
// src/core/types.ts
|
|
133
|
+
var CloakError = class extends Error {
|
|
134
|
+
constructor(message, category, retryable = false, originalError) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.category = category;
|
|
137
|
+
this.retryable = retryable;
|
|
138
|
+
this.originalError = originalError;
|
|
139
|
+
this.name = "CloakError";
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/core/keys.ts
|
|
144
|
+
var import_blake3 = require("@noble/hashes/blake3.js");
|
|
145
|
+
var import_tweetnacl = __toESM(require("tweetnacl"), 1);
|
|
146
|
+
|
|
147
|
+
// src/utils/crypto.ts
|
|
148
|
+
var import_web3 = require("@solana/web3.js");
|
|
149
|
+
var import_circomlibjs = require("circomlibjs");
|
|
150
|
+
var poseidon = null;
|
|
151
|
+
async function getPoseidon() {
|
|
152
|
+
if (!poseidon) {
|
|
153
|
+
poseidon = await (0, import_circomlibjs.buildPoseidon)();
|
|
154
|
+
}
|
|
155
|
+
return poseidon;
|
|
156
|
+
}
|
|
157
|
+
async function poseidonHash(inputs) {
|
|
158
|
+
const p = await getPoseidon();
|
|
159
|
+
const hash = p(inputs.map((x) => p.F.e(x)));
|
|
160
|
+
return p.F.toObject(hash);
|
|
161
|
+
}
|
|
162
|
+
function splitTo2Limbs(value) {
|
|
163
|
+
const mask = (1n << 128n) - 1n;
|
|
164
|
+
const lo = value & mask;
|
|
165
|
+
const hi = value >> 128n;
|
|
166
|
+
return [lo, hi];
|
|
167
|
+
}
|
|
168
|
+
function pubkeyToLimbs(pubkey) {
|
|
169
|
+
const bytes = pubkey instanceof import_web3.PublicKey ? pubkey.toBytes() : pubkey;
|
|
170
|
+
const value = BigInt("0x" + Buffer.from(bytes).toString("hex"));
|
|
171
|
+
return splitTo2Limbs(value);
|
|
172
|
+
}
|
|
173
|
+
async function computeMerkleRoot(leaf, pathElements, pathIndices) {
|
|
174
|
+
let current = leaf;
|
|
175
|
+
for (let i = 0; i < pathElements.length; i++) {
|
|
176
|
+
if (pathIndices[i] === 0) {
|
|
177
|
+
current = await poseidonHash([current, pathElements[i]]);
|
|
178
|
+
} else {
|
|
179
|
+
current = await poseidonHash([pathElements[i], current]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return current;
|
|
183
|
+
}
|
|
184
|
+
function hexToBigint(hex) {
|
|
185
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
186
|
+
return BigInt("0x" + cleanHex);
|
|
187
|
+
}
|
|
188
|
+
async function computeCommitment(amount, r, sk_spend) {
|
|
189
|
+
const [sk0, sk1] = splitTo2Limbs(sk_spend);
|
|
190
|
+
const [r0, r1] = splitTo2Limbs(r);
|
|
191
|
+
const pk_spend = await poseidonHash([sk0, sk1]);
|
|
192
|
+
return await poseidonHash([amount, r0, r1, pk_spend]);
|
|
193
|
+
}
|
|
194
|
+
async function generateCommitmentAsync(amountLamports, r, skSpend) {
|
|
195
|
+
const amount = BigInt(amountLamports);
|
|
196
|
+
const rValue = hexToBigint(bytesToHex(r));
|
|
197
|
+
const skValue = hexToBigint(bytesToHex(skSpend));
|
|
198
|
+
return await computeCommitment(amount, rValue, skValue);
|
|
199
|
+
}
|
|
200
|
+
function generateCommitment(_amountLamports, _r, _skSpend) {
|
|
201
|
+
throw new Error("generateCommitment is deprecated. Use generateCommitmentAsync instead.");
|
|
202
|
+
}
|
|
203
|
+
async function computeNullifier(sk_spend, leafIndex) {
|
|
204
|
+
const [sk0, sk1] = splitTo2Limbs(sk_spend);
|
|
205
|
+
return await poseidonHash([sk0, sk1, leafIndex]);
|
|
206
|
+
}
|
|
207
|
+
async function computeNullifierAsync(skSpend, leafIndex) {
|
|
208
|
+
const skValue = typeof skSpend === "string" ? hexToBigint(skSpend) : hexToBigint(bytesToHex(skSpend));
|
|
209
|
+
return await computeNullifier(skValue, BigInt(leafIndex));
|
|
210
|
+
}
|
|
211
|
+
function computeNullifierSync(_skSpend, _leafIndex) {
|
|
212
|
+
throw new Error("computeNullifierSync is deprecated. Use computeNullifierAsync instead.");
|
|
213
|
+
}
|
|
214
|
+
async function computeOutputsHashAsync(outputs) {
|
|
215
|
+
let hash = 0n;
|
|
216
|
+
for (const output of outputs) {
|
|
217
|
+
const [lo, hi] = pubkeyToLimbs(output.recipient);
|
|
218
|
+
hash = await poseidonHash([hash, lo, hi, BigInt(output.amount)]);
|
|
219
|
+
}
|
|
220
|
+
return hash;
|
|
221
|
+
}
|
|
222
|
+
async function computeOutputsHash(outAddr, outAmount, outFlags) {
|
|
223
|
+
let hash = 0n;
|
|
224
|
+
for (let i = 0; i < 5; i++) {
|
|
225
|
+
if (outFlags[i] === 1) {
|
|
226
|
+
hash = await poseidonHash([hash, outAddr[i][0], outAddr[i][1], outAmount[i]]);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return hash;
|
|
230
|
+
}
|
|
231
|
+
function computeOutputsHashSync(_outputs) {
|
|
232
|
+
throw new Error("computeOutputsHashSync is deprecated. Use computeOutputsHashAsync instead.");
|
|
233
|
+
}
|
|
234
|
+
async function computeSwapOutputsHash(inputMintLimbs, outputMintLimbs, recipientAtaLimbs, minOutputAmount, publicAmount) {
|
|
235
|
+
return await poseidonHash([
|
|
236
|
+
inputMintLimbs[0],
|
|
237
|
+
inputMintLimbs[1],
|
|
238
|
+
outputMintLimbs[0],
|
|
239
|
+
outputMintLimbs[1],
|
|
240
|
+
recipientAtaLimbs[0],
|
|
241
|
+
recipientAtaLimbs[1],
|
|
242
|
+
minOutputAmount,
|
|
243
|
+
publicAmount
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
async function computeSwapOutputsHashAsync(inputMint, outputMint, recipientAta, minOutputAmount, amount) {
|
|
247
|
+
const inputMintLimbs = pubkeyToLimbs(inputMint);
|
|
248
|
+
const outputMintLimbs = pubkeyToLimbs(outputMint);
|
|
249
|
+
const recipientAtaLimbs = pubkeyToLimbs(recipientAta);
|
|
250
|
+
return await computeSwapOutputsHash(
|
|
251
|
+
inputMintLimbs,
|
|
252
|
+
outputMintLimbs,
|
|
253
|
+
recipientAtaLimbs,
|
|
254
|
+
BigInt(minOutputAmount),
|
|
255
|
+
BigInt(amount)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function computeSwapOutputsHashSync(_outputMint, _recipientAta, _minOutputAmount, _amount) {
|
|
259
|
+
throw new Error("computeSwapOutputsHashSync is deprecated. Use computeSwapOutputsHashAsync instead.");
|
|
260
|
+
}
|
|
261
|
+
function bigintToBytes32(n) {
|
|
262
|
+
const hex = n.toString(16).padStart(64, "0");
|
|
263
|
+
const bytes = new Uint8Array(32);
|
|
264
|
+
for (let i = 0; i < 32; i++) {
|
|
265
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
266
|
+
}
|
|
267
|
+
return bytes;
|
|
268
|
+
}
|
|
269
|
+
function hexToBytes(hex) {
|
|
270
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
271
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
272
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
273
|
+
bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);
|
|
274
|
+
}
|
|
275
|
+
return bytes;
|
|
276
|
+
}
|
|
277
|
+
function bytesToHex(bytes, prefix = false) {
|
|
278
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
279
|
+
return prefix ? `0x${hex}` : hex;
|
|
280
|
+
}
|
|
281
|
+
function randomBytes(length) {
|
|
282
|
+
const bytes = new Uint8Array(length);
|
|
283
|
+
const g = globalThis;
|
|
284
|
+
try {
|
|
285
|
+
const cryptoObj = g?.crypto || g?.window?.crypto || g?.self?.crypto;
|
|
286
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
|
|
287
|
+
cryptoObj.getRandomValues(bytes);
|
|
288
|
+
return bytes;
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const nodeCrypto = require("crypto");
|
|
294
|
+
if (nodeCrypto?.randomBytes) {
|
|
295
|
+
const buffer = nodeCrypto.randomBytes(length);
|
|
296
|
+
bytes.set(buffer);
|
|
297
|
+
return bytes;
|
|
298
|
+
}
|
|
299
|
+
if (nodeCrypto?.webcrypto?.getRandomValues) {
|
|
300
|
+
nodeCrypto.webcrypto.getRandomValues(bytes);
|
|
301
|
+
return bytes;
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
306
|
+
return bytes;
|
|
307
|
+
}
|
|
308
|
+
function isValidHex(hex, expectedLength) {
|
|
309
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
310
|
+
if (!/^[0-9a-f]*$/i.test(cleanHex)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (cleanHex.length % 2 !== 0) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (expectedLength !== void 0) {
|
|
317
|
+
return cleanHex.length === expectedLength * 2;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
var BN254_MODULUS = BigInt("0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47");
|
|
322
|
+
function proofToBytes(proof) {
|
|
323
|
+
const pi_a_x = BigInt(proof.pi_a[0]);
|
|
324
|
+
const pi_a_y = BigInt(proof.pi_a[1]);
|
|
325
|
+
const pi_a_y_neg = (BN254_MODULUS - pi_a_y) % BN254_MODULUS;
|
|
326
|
+
const pi_a_x_le = bigintToBytes32LE(pi_a_x);
|
|
327
|
+
const pi_a_y_neg_le = bigintToBytes32LE(pi_a_y_neg);
|
|
328
|
+
const pi_a_le = new Uint8Array(64);
|
|
329
|
+
pi_a_le.set(pi_a_x_le, 0);
|
|
330
|
+
pi_a_le.set(pi_a_y_neg_le, 32);
|
|
331
|
+
const pi_a_be = convertEndianness32(pi_a_le);
|
|
332
|
+
const pi_b_x1 = BigInt(proof.pi_b[0][0]);
|
|
333
|
+
const pi_b_x2 = BigInt(proof.pi_b[0][1]);
|
|
334
|
+
const pi_b_y1 = BigInt(proof.pi_b[1][0]);
|
|
335
|
+
const pi_b_y2 = BigInt(proof.pi_b[1][1]);
|
|
336
|
+
const pi_b_x1_le = bigintToBytes32LE(pi_b_x1);
|
|
337
|
+
const pi_b_x2_le = bigintToBytes32LE(pi_b_x2);
|
|
338
|
+
const pi_b_y1_le = bigintToBytes32LE(pi_b_y1);
|
|
339
|
+
const pi_b_y2_le = bigintToBytes32LE(pi_b_y2);
|
|
340
|
+
const pi_b_le = new Uint8Array(128);
|
|
341
|
+
pi_b_le.set(pi_b_x1_le, 0);
|
|
342
|
+
pi_b_le.set(pi_b_x2_le, 32);
|
|
343
|
+
pi_b_le.set(pi_b_y1_le, 64);
|
|
344
|
+
pi_b_le.set(pi_b_y2_le, 96);
|
|
345
|
+
const pi_b_be = convertEndianness64(pi_b_le);
|
|
346
|
+
const pi_c_x = BigInt(proof.pi_c[0]);
|
|
347
|
+
const pi_c_y = BigInt(proof.pi_c[1]);
|
|
348
|
+
const pi_c_x_le = bigintToBytes32LE(pi_c_x);
|
|
349
|
+
const pi_c_y_le = bigintToBytes32LE(pi_c_y);
|
|
350
|
+
const pi_c_le = new Uint8Array(64);
|
|
351
|
+
pi_c_le.set(pi_c_x_le, 0);
|
|
352
|
+
pi_c_le.set(pi_c_y_le, 32);
|
|
353
|
+
const pi_c_be = convertEndianness32(pi_c_le);
|
|
354
|
+
const result = new Uint8Array(256);
|
|
355
|
+
result.set(pi_a_be, 0);
|
|
356
|
+
result.set(pi_b_be, 64);
|
|
357
|
+
result.set(pi_c_be, 192);
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function bigintToBytes32LE(n) {
|
|
361
|
+
const bytes = new Uint8Array(32);
|
|
362
|
+
let value = n % BN254_MODULUS;
|
|
363
|
+
if (value < 0) {
|
|
364
|
+
value = (value + BN254_MODULUS) % BN254_MODULUS;
|
|
365
|
+
}
|
|
366
|
+
for (let i = 0; i < 32; i++) {
|
|
367
|
+
bytes[i] = Number(value & BigInt(255));
|
|
368
|
+
value = value >> BigInt(8);
|
|
369
|
+
}
|
|
370
|
+
return bytes;
|
|
371
|
+
}
|
|
372
|
+
function convertEndianness32(bytes) {
|
|
373
|
+
if (bytes.length !== 64) {
|
|
374
|
+
throw new Error("convertEndianness32 expects 64 bytes");
|
|
375
|
+
}
|
|
376
|
+
const result = new Uint8Array(64);
|
|
377
|
+
for (let i = 0; i < 32; i++) {
|
|
378
|
+
result[i] = bytes[31 - i];
|
|
379
|
+
}
|
|
380
|
+
for (let i = 0; i < 32; i++) {
|
|
381
|
+
result[32 + i] = bytes[63 - i];
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
function convertEndianness64(bytes) {
|
|
386
|
+
if (bytes.length !== 128) {
|
|
387
|
+
throw new Error("convertEndianness64 expects 128 bytes");
|
|
388
|
+
}
|
|
389
|
+
const result = new Uint8Array(128);
|
|
390
|
+
for (let i = 0; i < 64; i++) {
|
|
391
|
+
result[i] = bytes[63 - i];
|
|
392
|
+
}
|
|
393
|
+
for (let i = 0; i < 64; i++) {
|
|
394
|
+
result[64 + i] = bytes[127 - i];
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
function buildPublicInputsBytes(root, nullifier, outputsHash, publicAmount) {
|
|
399
|
+
const result = new Uint8Array(104);
|
|
400
|
+
result.set(bigintToBytes32(root), 0);
|
|
401
|
+
result.set(bigintToBytes32(nullifier), 32);
|
|
402
|
+
result.set(bigintToBytes32(outputsHash), 64);
|
|
403
|
+
const amountBytes = new Uint8Array(8);
|
|
404
|
+
let amt = publicAmount;
|
|
405
|
+
for (let i = 7; i >= 0; i--) {
|
|
406
|
+
amountBytes[i] = Number(amt & 0xffn);
|
|
407
|
+
amt = amt >> 8n;
|
|
408
|
+
}
|
|
409
|
+
result.set(amountBytes, 96);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/core/keys.ts
|
|
414
|
+
function generateMasterSeed() {
|
|
415
|
+
const seed = new Uint8Array(32);
|
|
416
|
+
const g = globalThis;
|
|
417
|
+
const cryptoObj = g?.crypto || g?.window?.crypto || g?.self?.crypto;
|
|
418
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
|
|
419
|
+
cryptoObj.getRandomValues(seed);
|
|
420
|
+
} else {
|
|
421
|
+
try {
|
|
422
|
+
const nodeCrypto = require("crypto");
|
|
423
|
+
const buffer = nodeCrypto.randomBytes(32);
|
|
424
|
+
seed.set(buffer);
|
|
425
|
+
} catch {
|
|
426
|
+
throw new Error("No secure random number generator available");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
seed,
|
|
431
|
+
seedHex: bytesToHex(seed)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function deriveSpendKey(masterSeed) {
|
|
435
|
+
const context = new TextEncoder().encode("cloak_spend_key");
|
|
436
|
+
const preimage = new Uint8Array(masterSeed.length + context.length);
|
|
437
|
+
preimage.set(masterSeed, 0);
|
|
438
|
+
preimage.set(context, masterSeed.length);
|
|
439
|
+
const sk_spend = (0, import_blake3.blake3)(preimage);
|
|
440
|
+
const pk_spend = (0, import_blake3.blake3)(sk_spend);
|
|
441
|
+
return {
|
|
442
|
+
sk_spend,
|
|
443
|
+
pk_spend,
|
|
444
|
+
sk_spend_hex: bytesToHex(sk_spend),
|
|
445
|
+
pk_spend_hex: bytesToHex(pk_spend)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function deriveViewKey(sk_spend) {
|
|
449
|
+
const context = new TextEncoder().encode("cloak_view_key_secret");
|
|
450
|
+
const preimage = new Uint8Array(sk_spend.length + context.length);
|
|
451
|
+
preimage.set(sk_spend, 0);
|
|
452
|
+
preimage.set(context, sk_spend.length);
|
|
453
|
+
const vk_secret = (0, import_blake3.blake3)(preimage);
|
|
454
|
+
const x25519Keypair = import_tweetnacl.default.box.keyPair.fromSecretKey(vk_secret);
|
|
455
|
+
return {
|
|
456
|
+
vk_secret,
|
|
457
|
+
pvk: x25519Keypair.publicKey,
|
|
458
|
+
vk_secret_hex: bytesToHex(vk_secret),
|
|
459
|
+
pvk_hex: bytesToHex(x25519Keypair.publicKey)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function generateCloakKeys(masterSeed) {
|
|
463
|
+
const master = masterSeed ? { seed: masterSeed, seedHex: bytesToHex(masterSeed) } : generateMasterSeed();
|
|
464
|
+
const spend = deriveSpendKey(master.seed);
|
|
465
|
+
const view = deriveViewKey(spend.sk_spend);
|
|
466
|
+
return {
|
|
467
|
+
master,
|
|
468
|
+
spend,
|
|
469
|
+
view
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function encryptNoteForRecipient(noteData, recipientPvk) {
|
|
473
|
+
const ephemeralKeypair = import_tweetnacl.default.box.keyPair();
|
|
474
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPvk, ephemeralKeypair.secretKey);
|
|
475
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(noteData));
|
|
476
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
477
|
+
const ciphertext = import_tweetnacl.default.secretbox(plaintext, nonce, sharedSecret);
|
|
478
|
+
return {
|
|
479
|
+
ephemeral_pk: bytesToHex(ephemeralKeypair.publicKey),
|
|
480
|
+
ciphertext: bytesToHex(ciphertext),
|
|
481
|
+
nonce: bytesToHex(nonce)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function tryDecryptNote(encryptedNote, viewKey) {
|
|
485
|
+
try {
|
|
486
|
+
const ephemeralPk = hexToBytes(encryptedNote.ephemeral_pk);
|
|
487
|
+
const ciphertext = hexToBytes(encryptedNote.ciphertext);
|
|
488
|
+
const nonce = hexToBytes(encryptedNote.nonce);
|
|
489
|
+
const x25519Secret = viewKey.vk_secret;
|
|
490
|
+
const sharedSecret = import_tweetnacl.default.box.before(ephemeralPk, x25519Secret);
|
|
491
|
+
const plaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, sharedSecret);
|
|
492
|
+
if (!plaintext) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const noteData = JSON.parse(new TextDecoder().decode(plaintext));
|
|
496
|
+
return noteData;
|
|
497
|
+
} catch (e) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function scanNotesForWallet(encryptedOutputs, viewKey) {
|
|
502
|
+
const foundNotes = [];
|
|
503
|
+
for (const encryptedOutput of encryptedOutputs) {
|
|
504
|
+
try {
|
|
505
|
+
const decoded = atob(encryptedOutput);
|
|
506
|
+
const encryptedNote = JSON.parse(decoded);
|
|
507
|
+
const noteData = tryDecryptNote(encryptedNote, viewKey);
|
|
508
|
+
if (noteData) {
|
|
509
|
+
foundNotes.push(noteData);
|
|
510
|
+
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return foundNotes;
|
|
516
|
+
}
|
|
517
|
+
function exportKeys(keys) {
|
|
518
|
+
return JSON.stringify({
|
|
519
|
+
version: "2.0",
|
|
520
|
+
master_seed: keys.master.seedHex,
|
|
521
|
+
sk_spend: keys.spend.sk_spend_hex,
|
|
522
|
+
pk_spend: keys.spend.pk_spend_hex,
|
|
523
|
+
vk_secret: keys.view.vk_secret_hex,
|
|
524
|
+
pvk: keys.view.pvk_hex
|
|
525
|
+
}, null, 2);
|
|
526
|
+
}
|
|
527
|
+
function importKeys(exported) {
|
|
528
|
+
const parsed = JSON.parse(exported);
|
|
529
|
+
const masterSeed = hexToBytes(parsed.master_seed);
|
|
530
|
+
return generateCloakKeys(masterSeed);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/utils/network.ts
|
|
534
|
+
function detectNetworkFromRpcUrl(rpcUrl) {
|
|
535
|
+
const url = rpcUrl || process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "";
|
|
536
|
+
const lowerUrl = url.toLowerCase();
|
|
537
|
+
if (lowerUrl.includes("mainnet") || lowerUrl.includes("api.mainnet-beta") || lowerUrl.includes("mainnet-beta")) {
|
|
538
|
+
return "mainnet";
|
|
539
|
+
}
|
|
540
|
+
if (lowerUrl.includes("testnet") || lowerUrl.includes("api.testnet")) {
|
|
541
|
+
return "testnet";
|
|
542
|
+
}
|
|
543
|
+
if (lowerUrl.includes("devnet") || lowerUrl.includes("api.devnet")) {
|
|
544
|
+
return "devnet";
|
|
545
|
+
}
|
|
546
|
+
if (lowerUrl.includes("localhost") || lowerUrl.includes("127.0.0.1") || lowerUrl.includes("local")) {
|
|
547
|
+
return "localnet";
|
|
548
|
+
}
|
|
549
|
+
return "devnet";
|
|
550
|
+
}
|
|
551
|
+
function getRpcUrlForNetwork(network) {
|
|
552
|
+
switch (network) {
|
|
553
|
+
case "mainnet":
|
|
554
|
+
return "https://api.mainnet-beta.solana.com";
|
|
555
|
+
case "testnet":
|
|
556
|
+
return "https://api.testnet.solana.com";
|
|
557
|
+
case "devnet":
|
|
558
|
+
return "https://api.devnet.solana.com";
|
|
559
|
+
case "localnet":
|
|
560
|
+
return "http://localhost:8899";
|
|
561
|
+
default:
|
|
562
|
+
return "https://api.devnet.solana.com";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function isValidRpcUrl(url) {
|
|
566
|
+
try {
|
|
567
|
+
const parsed = new URL(url);
|
|
568
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function getExplorerUrl(signature, network = "devnet") {
|
|
574
|
+
const cluster = network === "mainnet" ? "" : `?cluster=${network}`;
|
|
575
|
+
return `https://explorer.solana.com/tx/${signature}${cluster}`;
|
|
576
|
+
}
|
|
577
|
+
function getAddressExplorerUrl(address, network = "devnet") {
|
|
578
|
+
const cluster = network === "mainnet" ? "" : `?cluster=${network}`;
|
|
579
|
+
return `https://explorer.solana.com/address/${address}${cluster}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/core/note-manager.ts
|
|
583
|
+
async function generateNote(amountLamports, network) {
|
|
584
|
+
const actualNetwork = network || detectNetworkFromRpcUrl();
|
|
585
|
+
const skSpend = randomBytes(32);
|
|
586
|
+
const rBytes = randomBytes(32);
|
|
587
|
+
const commitmentBigint = await generateCommitmentAsync(amountLamports, rBytes, skSpend);
|
|
588
|
+
const commitmentHex = commitmentBigint.toString(16).padStart(64, "0");
|
|
589
|
+
const skSpendHex = bytesToHex(skSpend);
|
|
590
|
+
const rHex = bytesToHex(rBytes);
|
|
591
|
+
return {
|
|
592
|
+
version: "1.0",
|
|
593
|
+
amount: amountLamports,
|
|
594
|
+
commitment: commitmentHex,
|
|
595
|
+
sk_spend: skSpendHex,
|
|
596
|
+
r: rHex,
|
|
597
|
+
timestamp: Date.now(),
|
|
598
|
+
network: actualNetwork
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function generateNoteFromWallet(amountLamports, keys, network) {
|
|
602
|
+
const actualNetwork = network || detectNetworkFromRpcUrl();
|
|
603
|
+
const rBytes = randomBytes(32);
|
|
604
|
+
const sk_spend = hexToBytes(keys.spend.sk_spend_hex);
|
|
605
|
+
const commitmentBigint = await generateCommitmentAsync(amountLamports, rBytes, sk_spend);
|
|
606
|
+
const commitmentHex = commitmentBigint.toString(16).padStart(64, "0");
|
|
607
|
+
return {
|
|
608
|
+
version: "2.0",
|
|
609
|
+
amount: amountLamports,
|
|
610
|
+
commitment: commitmentHex,
|
|
611
|
+
sk_spend: keys.spend.sk_spend_hex,
|
|
612
|
+
r: bytesToHex(rBytes),
|
|
613
|
+
timestamp: Date.now(),
|
|
614
|
+
network: actualNetwork
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function parseNote(jsonString) {
|
|
618
|
+
const note = JSON.parse(jsonString);
|
|
619
|
+
if (!note.version || !note.amount || !note.commitment || !note.sk_spend || !note.r) {
|
|
620
|
+
throw new CloakError(
|
|
621
|
+
"Invalid note format: missing required fields",
|
|
622
|
+
"validation",
|
|
623
|
+
false
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (!/^[0-9a-f]{64}$/i.test(note.commitment)) {
|
|
627
|
+
throw new CloakError("Invalid commitment format", "validation", false);
|
|
628
|
+
}
|
|
629
|
+
if (!/^[0-9a-f]{64}$/i.test(note.sk_spend)) {
|
|
630
|
+
throw new CloakError("Invalid sk_spend format", "validation", false);
|
|
631
|
+
}
|
|
632
|
+
if (!/^[0-9a-f]{64}$/i.test(note.r)) {
|
|
633
|
+
throw new CloakError("Invalid r format", "validation", false);
|
|
634
|
+
}
|
|
635
|
+
return note;
|
|
636
|
+
}
|
|
637
|
+
function exportNote(note, pretty = false) {
|
|
638
|
+
return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
|
|
639
|
+
}
|
|
640
|
+
function isWithdrawable(note) {
|
|
641
|
+
return !!(note.depositSignature && note.leafIndex !== void 0 && note.root && note.merkleProof);
|
|
642
|
+
}
|
|
643
|
+
function updateNoteWithDeposit(note, depositInfo) {
|
|
644
|
+
return {
|
|
645
|
+
...note,
|
|
646
|
+
depositSignature: depositInfo.signature,
|
|
647
|
+
depositSlot: depositInfo.slot,
|
|
648
|
+
leafIndex: depositInfo.leafIndex,
|
|
649
|
+
root: depositInfo.root,
|
|
650
|
+
merkleProof: depositInfo.merkleProof
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function findNoteByCommitment(notes, commitment) {
|
|
654
|
+
return notes.find((n) => n.commitment === commitment);
|
|
655
|
+
}
|
|
656
|
+
function filterNotesByNetwork(notes, network) {
|
|
657
|
+
return notes.filter((n) => n.network === network);
|
|
658
|
+
}
|
|
659
|
+
function filterWithdrawableNotes(notes) {
|
|
660
|
+
return notes.filter(isWithdrawable);
|
|
661
|
+
}
|
|
662
|
+
function exportWalletKeys(keys) {
|
|
663
|
+
return exportKeys(keys);
|
|
664
|
+
}
|
|
665
|
+
function importWalletKeys(keysJson) {
|
|
666
|
+
return importKeys(keysJson);
|
|
667
|
+
}
|
|
668
|
+
function getPublicViewKey(keys) {
|
|
669
|
+
return keys.view.pvk_hex;
|
|
670
|
+
}
|
|
671
|
+
function getViewKey(keys) {
|
|
672
|
+
return keys.view;
|
|
673
|
+
}
|
|
674
|
+
function calculateFee(amountLamports) {
|
|
675
|
+
const FIXED_FEE_LAMPORTS2 = 25e5;
|
|
676
|
+
const variableFee = Math.floor(amountLamports * 5 / 1e3);
|
|
677
|
+
return FIXED_FEE_LAMPORTS2 + variableFee;
|
|
678
|
+
}
|
|
679
|
+
function getDistributableAmount(amountLamports) {
|
|
680
|
+
return amountLamports - calculateFee(amountLamports);
|
|
681
|
+
}
|
|
682
|
+
function getRecipientAmount(amountLamports) {
|
|
683
|
+
return getDistributableAmount(amountLamports);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/core/storage.ts
|
|
687
|
+
var MemoryStorageAdapter = class {
|
|
688
|
+
constructor() {
|
|
689
|
+
this.notes = /* @__PURE__ */ new Map();
|
|
690
|
+
this.keys = null;
|
|
691
|
+
}
|
|
692
|
+
saveNote(note) {
|
|
693
|
+
this.notes.set(note.commitment, note);
|
|
694
|
+
}
|
|
695
|
+
loadAllNotes() {
|
|
696
|
+
return Array.from(this.notes.values());
|
|
697
|
+
}
|
|
698
|
+
updateNote(commitment, updates) {
|
|
699
|
+
const existing = this.notes.get(commitment);
|
|
700
|
+
if (existing) {
|
|
701
|
+
this.notes.set(commitment, { ...existing, ...updates });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
deleteNote(commitment) {
|
|
705
|
+
this.notes.delete(commitment);
|
|
706
|
+
}
|
|
707
|
+
clearAllNotes() {
|
|
708
|
+
this.notes.clear();
|
|
709
|
+
}
|
|
710
|
+
saveKeys(keys) {
|
|
711
|
+
this.keys = keys;
|
|
712
|
+
}
|
|
713
|
+
loadKeys() {
|
|
714
|
+
return this.keys;
|
|
715
|
+
}
|
|
716
|
+
deleteKeys() {
|
|
717
|
+
this.keys = null;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
var LocalStorageAdapter = class {
|
|
721
|
+
constructor(notesKey = "cloak_notes", keysKey = "cloak_wallet_keys") {
|
|
722
|
+
this.notesKey = notesKey;
|
|
723
|
+
this.keysKey = keysKey;
|
|
724
|
+
}
|
|
725
|
+
getStorage() {
|
|
726
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
727
|
+
return globalThis.localStorage;
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
saveNote(note) {
|
|
732
|
+
const storage = this.getStorage();
|
|
733
|
+
if (!storage) throw new Error("localStorage not available");
|
|
734
|
+
const notes = this.loadAllNotes();
|
|
735
|
+
notes.push(note);
|
|
736
|
+
storage.setItem(this.notesKey, JSON.stringify(notes));
|
|
737
|
+
}
|
|
738
|
+
loadAllNotes() {
|
|
739
|
+
const storage = this.getStorage();
|
|
740
|
+
if (!storage) return [];
|
|
741
|
+
const stored = storage.getItem(this.notesKey);
|
|
742
|
+
if (!stored) return [];
|
|
743
|
+
try {
|
|
744
|
+
return JSON.parse(stored);
|
|
745
|
+
} catch {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
updateNote(commitment, updates) {
|
|
750
|
+
const storage = this.getStorage();
|
|
751
|
+
if (!storage) return;
|
|
752
|
+
const notes = this.loadAllNotes();
|
|
753
|
+
const index = notes.findIndex((n) => n.commitment === commitment);
|
|
754
|
+
if (index !== -1) {
|
|
755
|
+
notes[index] = { ...notes[index], ...updates };
|
|
756
|
+
storage.setItem(this.notesKey, JSON.stringify(notes));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
deleteNote(commitment) {
|
|
760
|
+
const storage = this.getStorage();
|
|
761
|
+
if (!storage) return;
|
|
762
|
+
const notes = this.loadAllNotes();
|
|
763
|
+
const filtered = notes.filter((n) => n.commitment !== commitment);
|
|
764
|
+
storage.setItem(this.notesKey, JSON.stringify(filtered));
|
|
765
|
+
}
|
|
766
|
+
clearAllNotes() {
|
|
767
|
+
const storage = this.getStorage();
|
|
768
|
+
if (storage) {
|
|
769
|
+
storage.removeItem(this.notesKey);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
saveKeys(keys) {
|
|
773
|
+
const storage = this.getStorage();
|
|
774
|
+
if (!storage) throw new Error("localStorage not available");
|
|
775
|
+
storage.setItem(this.keysKey, exportKeys(keys));
|
|
776
|
+
}
|
|
777
|
+
loadKeys() {
|
|
778
|
+
const storage = this.getStorage();
|
|
779
|
+
if (!storage) return null;
|
|
780
|
+
const stored = storage.getItem(this.keysKey);
|
|
781
|
+
if (!stored) return null;
|
|
782
|
+
try {
|
|
783
|
+
return importKeys(stored);
|
|
784
|
+
} catch {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
deleteKeys() {
|
|
789
|
+
const storage = this.getStorage();
|
|
790
|
+
if (storage) {
|
|
791
|
+
storage.removeItem(this.keysKey);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// src/utils/validation.ts
|
|
797
|
+
var import_web32 = require("@solana/web3.js");
|
|
798
|
+
function isValidSolanaAddress(address) {
|
|
799
|
+
try {
|
|
800
|
+
new import_web32.PublicKey(address);
|
|
801
|
+
return true;
|
|
802
|
+
} catch {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function validateNote(note) {
|
|
807
|
+
if (!note || typeof note !== "object") {
|
|
808
|
+
throw new Error("Note must be an object");
|
|
809
|
+
}
|
|
810
|
+
const requiredFields = ["version", "amount", "commitment", "sk_spend", "r", "timestamp", "network"];
|
|
811
|
+
for (const field of requiredFields) {
|
|
812
|
+
if (!(field in note)) {
|
|
813
|
+
throw new Error(`Missing required field: ${field}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (typeof note.version !== "string") {
|
|
817
|
+
throw new Error("Version must be a string");
|
|
818
|
+
}
|
|
819
|
+
if (typeof note.amount !== "number" || note.amount <= 0) {
|
|
820
|
+
throw new Error("Amount must be a positive number");
|
|
821
|
+
}
|
|
822
|
+
if (!isValidHex(note.commitment, 32)) {
|
|
823
|
+
throw new Error("Invalid commitment format (expected 64 hex characters)");
|
|
824
|
+
}
|
|
825
|
+
if (!isValidHex(note.sk_spend, 32)) {
|
|
826
|
+
throw new Error("Invalid sk_spend format (expected 64 hex characters)");
|
|
827
|
+
}
|
|
828
|
+
if (!isValidHex(note.r, 32)) {
|
|
829
|
+
throw new Error("Invalid r format (expected 64 hex characters)");
|
|
830
|
+
}
|
|
831
|
+
if (typeof note.timestamp !== "number" || note.timestamp <= 0) {
|
|
832
|
+
throw new Error("Timestamp must be a positive number");
|
|
833
|
+
}
|
|
834
|
+
if (!["localnet", "devnet", "testnet", "mainnet"].includes(note.network)) {
|
|
835
|
+
throw new Error("Network must be localnet, devnet, testnet, or mainnet");
|
|
836
|
+
}
|
|
837
|
+
if (note.depositSignature !== void 0 && typeof note.depositSignature !== "string") {
|
|
838
|
+
throw new Error("Deposit signature must be a string");
|
|
839
|
+
}
|
|
840
|
+
if (note.depositSlot !== void 0 && typeof note.depositSlot !== "number") {
|
|
841
|
+
throw new Error("Deposit slot must be a number");
|
|
842
|
+
}
|
|
843
|
+
if (note.leafIndex !== void 0) {
|
|
844
|
+
if (typeof note.leafIndex !== "number" || note.leafIndex < 0) {
|
|
845
|
+
throw new Error("Leaf index must be a non-negative number");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (note.root !== void 0 && !isValidHex(note.root, 32)) {
|
|
849
|
+
throw new Error("Invalid root format (expected 64 hex characters)");
|
|
850
|
+
}
|
|
851
|
+
if (note.merkleProof !== void 0) {
|
|
852
|
+
if (!Array.isArray(note.merkleProof.pathElements)) {
|
|
853
|
+
throw new Error("Merkle proof pathElements must be an array");
|
|
854
|
+
}
|
|
855
|
+
if (!Array.isArray(note.merkleProof.pathIndices)) {
|
|
856
|
+
throw new Error("Merkle proof pathIndices must be an array");
|
|
857
|
+
}
|
|
858
|
+
if (note.merkleProof.pathElements.length !== note.merkleProof.pathIndices.length) {
|
|
859
|
+
throw new Error("Merkle proof pathElements and pathIndices must have same length");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function parseNote2(jsonString) {
|
|
864
|
+
let parsed;
|
|
865
|
+
try {
|
|
866
|
+
parsed = JSON.parse(jsonString);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
throw new Error("Invalid JSON format");
|
|
869
|
+
}
|
|
870
|
+
validateNote(parsed);
|
|
871
|
+
return parsed;
|
|
872
|
+
}
|
|
873
|
+
function validateWithdrawableNote(note) {
|
|
874
|
+
if (!note.depositSignature) {
|
|
875
|
+
throw new Error("Note must be deposited before withdrawal (missing depositSignature)");
|
|
876
|
+
}
|
|
877
|
+
if (note.leafIndex === void 0) {
|
|
878
|
+
throw new Error("Note must be deposited before withdrawal (missing leafIndex)");
|
|
879
|
+
}
|
|
880
|
+
if (!note.root) {
|
|
881
|
+
throw new Error("Note must have historical root for withdrawal");
|
|
882
|
+
}
|
|
883
|
+
if (!note.merkleProof) {
|
|
884
|
+
throw new Error("Note must have Merkle proof for withdrawal");
|
|
885
|
+
}
|
|
886
|
+
if (note.merkleProof.pathElements.length === 0) {
|
|
887
|
+
throw new Error("Merkle proof is empty");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function validateTransfers(recipients, totalAmount) {
|
|
891
|
+
if (recipients.length === 0) {
|
|
892
|
+
throw new Error("At least one recipient is required");
|
|
893
|
+
}
|
|
894
|
+
if (recipients.length > 5) {
|
|
895
|
+
throw new Error("Maximum 5 recipients allowed");
|
|
896
|
+
}
|
|
897
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
898
|
+
const transfer = recipients[i];
|
|
899
|
+
if (!transfer.recipient || !(transfer.recipient instanceof import_web32.PublicKey)) {
|
|
900
|
+
throw new Error(`Recipient ${i} must be a PublicKey`);
|
|
901
|
+
}
|
|
902
|
+
if (typeof transfer.amount !== "number" || transfer.amount <= 0) {
|
|
903
|
+
throw new Error(`Recipient ${i} amount must be a positive number`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const sum = recipients.reduce((acc, t) => acc + t.amount, 0);
|
|
907
|
+
if (sum !== totalAmount) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Recipients sum (${sum}) does not match expected total (${totalAmount})`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/utils/fees.ts
|
|
915
|
+
var FIXED_FEE_LAMPORTS = 25e5;
|
|
916
|
+
var VARIABLE_FEE_RATE = 5 / 1e3;
|
|
917
|
+
var LAMPORTS_PER_SOL = 1e9;
|
|
918
|
+
function calculateFee2(amountLamports) {
|
|
919
|
+
const variableFee = Math.floor(amountLamports * 5 / 1e3);
|
|
920
|
+
return FIXED_FEE_LAMPORTS + variableFee;
|
|
921
|
+
}
|
|
922
|
+
function getDistributableAmount2(amountLamports) {
|
|
923
|
+
return amountLamports - calculateFee2(amountLamports);
|
|
924
|
+
}
|
|
925
|
+
function formatAmount(lamports, decimals = 9) {
|
|
926
|
+
return (lamports / LAMPORTS_PER_SOL).toFixed(decimals);
|
|
927
|
+
}
|
|
928
|
+
function parseAmount(sol) {
|
|
929
|
+
const num = parseFloat(sol);
|
|
930
|
+
if (isNaN(num) || num < 0) {
|
|
931
|
+
throw new Error(`Invalid SOL amount: ${sol}`);
|
|
932
|
+
}
|
|
933
|
+
return Math.floor(num * LAMPORTS_PER_SOL);
|
|
934
|
+
}
|
|
935
|
+
function validateOutputsSum(outputs, expectedTotal) {
|
|
936
|
+
const sum = outputs.reduce((acc, out) => acc + out.amount, 0);
|
|
937
|
+
return sum === expectedTotal;
|
|
938
|
+
}
|
|
939
|
+
function calculateRelayFee(amountLamports, feeBps) {
|
|
940
|
+
if (feeBps < 0 || feeBps > 1e4) {
|
|
941
|
+
throw new Error("Fee basis points must be between 0 and 10000");
|
|
942
|
+
}
|
|
943
|
+
return Math.floor(amountLamports * feeBps / 1e4);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/services/IndexerService.ts
|
|
947
|
+
var IndexerService = class {
|
|
948
|
+
/**
|
|
949
|
+
* Create a new Indexer Service client
|
|
950
|
+
*
|
|
951
|
+
* @param baseUrl - Indexer API base URL
|
|
952
|
+
*/
|
|
953
|
+
constructor(baseUrl) {
|
|
954
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Get current Merkle root and next available index
|
|
958
|
+
*
|
|
959
|
+
* @returns Current root and next index
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* ```typescript
|
|
963
|
+
* const { root, next_index } = await indexer.getMerkleRoot();
|
|
964
|
+
* console.log(`Current root: ${root}, Next index: ${next_index}`);
|
|
965
|
+
* ```
|
|
966
|
+
*/
|
|
967
|
+
async getMerkleRoot() {
|
|
968
|
+
const response = await fetch(`${this.baseUrl}/api/v1/merkle/root`);
|
|
969
|
+
if (!response.ok) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`Failed to get Merkle root: ${response.status} ${response.statusText}`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
const json = await response.json();
|
|
975
|
+
return json;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Get Merkle proof for a specific leaf
|
|
979
|
+
*
|
|
980
|
+
* @param leafIndex - Index of the leaf in the tree
|
|
981
|
+
* @returns Merkle proof with path elements and indices
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* ```typescript
|
|
985
|
+
* const proof = await indexer.getMerkleProof(42);
|
|
986
|
+
* console.log(`Proof has ${proof.pathElements.length} siblings`);
|
|
987
|
+
* ```
|
|
988
|
+
*/
|
|
989
|
+
async getMerkleProof(leafIndex) {
|
|
990
|
+
const response = await fetch(
|
|
991
|
+
`${this.baseUrl}/api/v1/merkle/proof/${leafIndex}`
|
|
992
|
+
);
|
|
993
|
+
if (!response.ok) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`Failed to get Merkle proof: ${response.status} ${response.statusText}`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const data = await response.json();
|
|
999
|
+
return {
|
|
1000
|
+
pathElements: data.pathElements ?? data.path_elements,
|
|
1001
|
+
pathIndices: data.pathIndices ?? data.path_indices,
|
|
1002
|
+
root: data.root
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Get notes in a specific range
|
|
1007
|
+
*
|
|
1008
|
+
* Useful for scanning the tree or fetching notes in batches.
|
|
1009
|
+
*
|
|
1010
|
+
* @param start - Start index (inclusive)
|
|
1011
|
+
* @param end - End index (inclusive)
|
|
1012
|
+
* @param limit - Maximum number of notes to return (default: 100)
|
|
1013
|
+
* @returns Notes in the range
|
|
1014
|
+
*
|
|
1015
|
+
* @example
|
|
1016
|
+
* ```typescript
|
|
1017
|
+
* const { notes, has_more } = await indexer.getNotesRange(0, 99, 100);
|
|
1018
|
+
* console.log(`Fetched ${notes.length} notes`);
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
async getNotesRange(start, end, limit = 100) {
|
|
1022
|
+
const url = new URL(`${this.baseUrl}/api/v1/notes/range`);
|
|
1023
|
+
url.searchParams.set("start", start.toString());
|
|
1024
|
+
url.searchParams.set("end", end.toString());
|
|
1025
|
+
url.searchParams.set("limit", limit.toString());
|
|
1026
|
+
const response = await fetch(url.toString());
|
|
1027
|
+
if (!response.ok) {
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
`Failed to get notes range: ${response.status} ${response.statusText}`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
const json = await response.json();
|
|
1033
|
+
return json;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Get all notes from the tree
|
|
1037
|
+
*
|
|
1038
|
+
* Fetches all notes in batches. Use with caution for large trees.
|
|
1039
|
+
*
|
|
1040
|
+
* @param batchSize - Size of each batch (default: 100)
|
|
1041
|
+
* @returns All encrypted notes
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* ```typescript
|
|
1045
|
+
* const allNotes = await indexer.getAllNotes();
|
|
1046
|
+
* console.log(`Total notes: ${allNotes.length}`);
|
|
1047
|
+
* ```
|
|
1048
|
+
*/
|
|
1049
|
+
async getAllNotes(batchSize = 100) {
|
|
1050
|
+
const rootResponse = await this.getMerkleRoot();
|
|
1051
|
+
const totalNotes = rootResponse.next_index;
|
|
1052
|
+
if (totalNotes === 0) {
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
const allNotes = [];
|
|
1056
|
+
for (let start = 0; start < totalNotes; start += batchSize) {
|
|
1057
|
+
const end = Math.min(start + batchSize - 1, totalNotes - 1);
|
|
1058
|
+
const response = await this.getNotesRange(start, end, batchSize);
|
|
1059
|
+
allNotes.push(...response.notes);
|
|
1060
|
+
}
|
|
1061
|
+
return allNotes;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Submit a deposit to the indexer
|
|
1065
|
+
*
|
|
1066
|
+
* Registers a new deposit transaction with the indexer, which will
|
|
1067
|
+
* return the leaf index and current root.
|
|
1068
|
+
*
|
|
1069
|
+
* @param params - Deposit parameters
|
|
1070
|
+
* @returns Success response with leaf index and root
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```typescript
|
|
1074
|
+
* const result = await indexer.submitDeposit({
|
|
1075
|
+
* leafCommit: note.commitment,
|
|
1076
|
+
* encryptedOutput: btoa(JSON.stringify(noteData)),
|
|
1077
|
+
* txSignature: signature,
|
|
1078
|
+
* slot: txSlot
|
|
1079
|
+
* });
|
|
1080
|
+
* console.log(`Leaf index: ${result.leafIndex}`);
|
|
1081
|
+
* ```
|
|
1082
|
+
*/
|
|
1083
|
+
async submitDeposit(params) {
|
|
1084
|
+
const response = await fetch(`${this.baseUrl}/api/v1/deposit`, {
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
headers: {
|
|
1087
|
+
"Content-Type": "application/json"
|
|
1088
|
+
},
|
|
1089
|
+
body: JSON.stringify({
|
|
1090
|
+
leaf_commit: params.leafCommit,
|
|
1091
|
+
encrypted_output: params.encryptedOutput,
|
|
1092
|
+
tx_signature: params.txSignature,
|
|
1093
|
+
slot: params.slot
|
|
1094
|
+
})
|
|
1095
|
+
});
|
|
1096
|
+
let responseData;
|
|
1097
|
+
try {
|
|
1098
|
+
responseData = await response.json();
|
|
1099
|
+
} catch {
|
|
1100
|
+
try {
|
|
1101
|
+
const text = await response.text();
|
|
1102
|
+
responseData = text ? { error: text } : null;
|
|
1103
|
+
} catch {
|
|
1104
|
+
responseData = null;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (!response.ok) {
|
|
1108
|
+
let errorMessage = `${response.status} ${response.statusText}`;
|
|
1109
|
+
if (responseData) {
|
|
1110
|
+
if (typeof responseData === "string") {
|
|
1111
|
+
errorMessage = responseData;
|
|
1112
|
+
} else {
|
|
1113
|
+
errorMessage = responseData?.error || responseData?.message || errorMessage;
|
|
1114
|
+
if (responseData?.details) {
|
|
1115
|
+
errorMessage += ` (${JSON.stringify(responseData.details)})`;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
throw new Error(`Failed to submit deposit: ${errorMessage}`);
|
|
1120
|
+
}
|
|
1121
|
+
const data = responseData;
|
|
1122
|
+
return {
|
|
1123
|
+
success: data.success ?? true,
|
|
1124
|
+
leafIndex: data.leafIndex ?? data.leaf_index,
|
|
1125
|
+
root: data.root
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Check indexer health
|
|
1130
|
+
*
|
|
1131
|
+
* @returns Health status
|
|
1132
|
+
*/
|
|
1133
|
+
async healthCheck() {
|
|
1134
|
+
const response = await fetch(`${this.baseUrl}/health`);
|
|
1135
|
+
if (!response.ok) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
`Health check failed: ${response.status} ${response.statusText}`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
const json = await response.json();
|
|
1141
|
+
return json;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
// src/services/ArtifactProverService.ts
|
|
1146
|
+
var ArtifactProverService = class {
|
|
1147
|
+
/**
|
|
1148
|
+
* Create a new Artifact Prover Service client
|
|
1149
|
+
*
|
|
1150
|
+
* @param indexerUrl - Indexer service base URL
|
|
1151
|
+
* @param timeout - Proof generation timeout in ms (default: 5 minutes)
|
|
1152
|
+
* @param pollInterval - Polling interval for status checks (default: 2 seconds)
|
|
1153
|
+
*/
|
|
1154
|
+
constructor(indexerUrl, timeout = 5 * 60 * 1e3, pollInterval = 2e3) {
|
|
1155
|
+
this.indexerUrl = indexerUrl.replace(/\/$/, "");
|
|
1156
|
+
this.timeout = timeout;
|
|
1157
|
+
this.pollInterval = pollInterval;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Generate a zero-knowledge proof using artifact-based flow
|
|
1161
|
+
*
|
|
1162
|
+
* This process typically takes 30-180 seconds depending on the TEE.
|
|
1163
|
+
* Private inputs are uploaded directly to TEE, never passing through backend.
|
|
1164
|
+
*
|
|
1165
|
+
* @param inputs - Circuit inputs (private + public + outputs)
|
|
1166
|
+
* @param options - Optional progress tracking and callbacks
|
|
1167
|
+
* @returns Proof result with hex-encoded proof and public inputs
|
|
1168
|
+
*
|
|
1169
|
+
* @example
|
|
1170
|
+
* ```typescript
|
|
1171
|
+
* const result = await prover.generateProof(inputs);
|
|
1172
|
+
* if (result.success) {
|
|
1173
|
+
* console.log(`Proof: ${result.proof}`);
|
|
1174
|
+
* }
|
|
1175
|
+
* ```
|
|
1176
|
+
*/
|
|
1177
|
+
async generateProof(inputs, options) {
|
|
1178
|
+
const startTime = Date.now();
|
|
1179
|
+
const actualTimeout = options?.timeout || this.timeout;
|
|
1180
|
+
const pollInterval = options?.pollInterval || this.pollInterval;
|
|
1181
|
+
options?.onStart?.();
|
|
1182
|
+
options?.onProgress?.(5);
|
|
1183
|
+
try {
|
|
1184
|
+
options?.onProgress?.(10);
|
|
1185
|
+
const artifactResponse = await fetch(`${this.indexerUrl}/api/v1/tee/artifact`, {
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
headers: {
|
|
1188
|
+
"Content-Type": "application/json"
|
|
1189
|
+
},
|
|
1190
|
+
body: JSON.stringify({
|
|
1191
|
+
program_id: null
|
|
1192
|
+
// Optional, can be null
|
|
1193
|
+
})
|
|
1194
|
+
});
|
|
1195
|
+
if (!artifactResponse.ok) {
|
|
1196
|
+
const errorText = await artifactResponse.text();
|
|
1197
|
+
const errorMessage2 = `Failed to create artifact: ${errorText}`;
|
|
1198
|
+
options?.onError?.(errorMessage2);
|
|
1199
|
+
return {
|
|
1200
|
+
success: false,
|
|
1201
|
+
generationTimeMs: Date.now() - startTime,
|
|
1202
|
+
error: errorMessage2
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
const artifactData = await artifactResponse.json();
|
|
1206
|
+
const { artifact_id, upload_url } = artifactData;
|
|
1207
|
+
if (!artifact_id || !upload_url) {
|
|
1208
|
+
const errorMessage2 = "Invalid artifact response: missing artifact_id or upload_url";
|
|
1209
|
+
options?.onError?.(errorMessage2);
|
|
1210
|
+
return {
|
|
1211
|
+
success: false,
|
|
1212
|
+
generationTimeMs: Date.now() - startTime,
|
|
1213
|
+
error: errorMessage2
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const fullUploadUrl = upload_url.startsWith("http") ? upload_url : `${this.indexerUrl}${upload_url}`;
|
|
1217
|
+
options?.onProgress?.(20);
|
|
1218
|
+
const stdinPayload = JSON.stringify({
|
|
1219
|
+
private: inputs.privateInputs,
|
|
1220
|
+
public: inputs.publicInputs,
|
|
1221
|
+
outputs: inputs.outputs,
|
|
1222
|
+
// Include swap_params if present (for swap transactions)
|
|
1223
|
+
...inputs.swapParams && { swap_params: inputs.swapParams }
|
|
1224
|
+
});
|
|
1225
|
+
const uploadResponse = await fetch(fullUploadUrl, {
|
|
1226
|
+
method: "POST",
|
|
1227
|
+
headers: {
|
|
1228
|
+
"Content-Type": "application/json"
|
|
1229
|
+
},
|
|
1230
|
+
body: stdinPayload
|
|
1231
|
+
});
|
|
1232
|
+
if (!uploadResponse.ok) {
|
|
1233
|
+
const errorText = await uploadResponse.text();
|
|
1234
|
+
const errorMessage2 = `Failed to upload stdin to TEE: ${errorText}`;
|
|
1235
|
+
options?.onError?.(errorMessage2);
|
|
1236
|
+
return {
|
|
1237
|
+
success: false,
|
|
1238
|
+
generationTimeMs: Date.now() - startTime,
|
|
1239
|
+
error: errorMessage2
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
options?.onProgress?.(30);
|
|
1243
|
+
const requestProofBody = {
|
|
1244
|
+
artifact_id,
|
|
1245
|
+
program_id: null,
|
|
1246
|
+
public_inputs: JSON.stringify(inputs.publicInputs)
|
|
1247
|
+
};
|
|
1248
|
+
const requestProofResponse = await fetch(
|
|
1249
|
+
`${this.indexerUrl}/api/v1/tee/request-proof`,
|
|
1250
|
+
{
|
|
1251
|
+
method: "POST",
|
|
1252
|
+
headers: {
|
|
1253
|
+
"Content-Type": "application/json"
|
|
1254
|
+
},
|
|
1255
|
+
body: JSON.stringify(requestProofBody)
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
if (!requestProofResponse.ok) {
|
|
1259
|
+
const errorText = await requestProofResponse.text();
|
|
1260
|
+
const errorMessage2 = `Failed to request proof: ${errorText}`;
|
|
1261
|
+
options?.onError?.(errorMessage2);
|
|
1262
|
+
return {
|
|
1263
|
+
success: false,
|
|
1264
|
+
generationTimeMs: Date.now() - startTime,
|
|
1265
|
+
error: errorMessage2
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const requestProofData = await requestProofResponse.json();
|
|
1269
|
+
const { request_id } = requestProofData;
|
|
1270
|
+
if (!request_id) {
|
|
1271
|
+
const errorMessage2 = "Invalid proof request response: missing request_id";
|
|
1272
|
+
options?.onError?.(errorMessage2);
|
|
1273
|
+
return {
|
|
1274
|
+
success: false,
|
|
1275
|
+
generationTimeMs: Date.now() - startTime,
|
|
1276
|
+
error: errorMessage2
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
options?.onProgress?.(40);
|
|
1280
|
+
const pollStartTime = Date.now();
|
|
1281
|
+
let lastProgress = 40;
|
|
1282
|
+
while (Date.now() - pollStartTime < actualTimeout) {
|
|
1283
|
+
const statusResponse = await fetch(
|
|
1284
|
+
`${this.indexerUrl}/api/v1/tee/proof-status?request_id=${request_id}`,
|
|
1285
|
+
{
|
|
1286
|
+
method: "GET"
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
if (!statusResponse.ok) {
|
|
1290
|
+
const errorText = await statusResponse.text();
|
|
1291
|
+
const errorMessage2 = `Failed to check proof status: ${errorText}`;
|
|
1292
|
+
options?.onError?.(errorMessage2);
|
|
1293
|
+
return {
|
|
1294
|
+
success: false,
|
|
1295
|
+
generationTimeMs: Date.now() - startTime,
|
|
1296
|
+
error: errorMessage2
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const statusData = await statusResponse.json();
|
|
1300
|
+
const { status, proof, public_inputs, generation_time_ms, error } = statusData;
|
|
1301
|
+
if (status === "ready") {
|
|
1302
|
+
options?.onProgress?.(100);
|
|
1303
|
+
if (!proof || !public_inputs) {
|
|
1304
|
+
const errorMessage2 = "Proof status is 'ready' but proof or public_inputs is missing";
|
|
1305
|
+
options?.onError?.(errorMessage2);
|
|
1306
|
+
return {
|
|
1307
|
+
success: false,
|
|
1308
|
+
generationTimeMs: Date.now() - startTime,
|
|
1309
|
+
error: errorMessage2
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
const result = {
|
|
1313
|
+
success: true,
|
|
1314
|
+
proof,
|
|
1315
|
+
publicInputs: public_inputs,
|
|
1316
|
+
generationTimeMs: generation_time_ms || Date.now() - startTime
|
|
1317
|
+
};
|
|
1318
|
+
options?.onSuccess?.(result);
|
|
1319
|
+
return result;
|
|
1320
|
+
}
|
|
1321
|
+
if (status === "failed") {
|
|
1322
|
+
const errorMessage2 = error || "Proof generation failed";
|
|
1323
|
+
options?.onError?.(errorMessage2);
|
|
1324
|
+
return {
|
|
1325
|
+
success: false,
|
|
1326
|
+
generationTimeMs: Date.now() - startTime,
|
|
1327
|
+
error: errorMessage2
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
const elapsed = Date.now() - pollStartTime;
|
|
1331
|
+
const progress = Math.min(90, 40 + Math.floor(elapsed / actualTimeout * 50));
|
|
1332
|
+
if (progress > lastProgress) {
|
|
1333
|
+
lastProgress = progress;
|
|
1334
|
+
options?.onProgress?.(progress);
|
|
1335
|
+
}
|
|
1336
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
1337
|
+
}
|
|
1338
|
+
const errorMessage = `Proof generation timed out after ${actualTimeout}ms`;
|
|
1339
|
+
options?.onError?.(errorMessage);
|
|
1340
|
+
return {
|
|
1341
|
+
success: false,
|
|
1342
|
+
generationTimeMs: Date.now() - startTime,
|
|
1343
|
+
error: errorMessage
|
|
1344
|
+
};
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
const totalTime = Date.now() - startTime;
|
|
1347
|
+
let errorMessage;
|
|
1348
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1349
|
+
errorMessage = `Proof generation timed out after ${actualTimeout}ms`;
|
|
1350
|
+
} else {
|
|
1351
|
+
errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1352
|
+
}
|
|
1353
|
+
options?.onError?.(errorMessage);
|
|
1354
|
+
return {
|
|
1355
|
+
success: false,
|
|
1356
|
+
generationTimeMs: totalTime,
|
|
1357
|
+
error: errorMessage
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Check if the artifact prover service is available
|
|
1363
|
+
*
|
|
1364
|
+
* @returns True if service is healthy
|
|
1365
|
+
*/
|
|
1366
|
+
async healthCheck() {
|
|
1367
|
+
try {
|
|
1368
|
+
const response = await fetch(`${this.indexerUrl}/health`, {
|
|
1369
|
+
method: "GET"
|
|
1370
|
+
});
|
|
1371
|
+
return response.ok;
|
|
1372
|
+
} catch {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Get the configured timeout
|
|
1378
|
+
*/
|
|
1379
|
+
getTimeout() {
|
|
1380
|
+
return this.timeout;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Set a new timeout
|
|
1384
|
+
*/
|
|
1385
|
+
setTimeout(timeout) {
|
|
1386
|
+
if (timeout <= 0) {
|
|
1387
|
+
throw new Error("Timeout must be positive");
|
|
1388
|
+
}
|
|
1389
|
+
this.timeout = timeout;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
// src/services/RelayService.ts
|
|
1394
|
+
var RelayService = class {
|
|
1395
|
+
/**
|
|
1396
|
+
* Create a new Relay Service client
|
|
1397
|
+
*
|
|
1398
|
+
* @param baseUrl - Relay service base URL
|
|
1399
|
+
*/
|
|
1400
|
+
constructor(baseUrl) {
|
|
1401
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Submit a withdrawal transaction via relay
|
|
1405
|
+
*
|
|
1406
|
+
* The relay service will validate the proof, pay for transaction fees,
|
|
1407
|
+
* and submit the transaction on-chain.
|
|
1408
|
+
*
|
|
1409
|
+
* @param params - Withdrawal parameters
|
|
1410
|
+
* @param onStatusUpdate - Optional callback for status updates
|
|
1411
|
+
* @returns Transaction signature when completed
|
|
1412
|
+
*
|
|
1413
|
+
* @example
|
|
1414
|
+
* ```typescript
|
|
1415
|
+
* const signature = await relay.submitWithdraw({
|
|
1416
|
+
* proof: proofHex,
|
|
1417
|
+
* publicInputs: { root, nf, outputs_hash, amount },
|
|
1418
|
+
* outputs: [{ recipient: addr, amount: lamports }],
|
|
1419
|
+
* feeBps: 50
|
|
1420
|
+
* }, (status) => console.log(`Status: ${status}`));
|
|
1421
|
+
* console.log(`Transaction: ${signature}`);
|
|
1422
|
+
* ```
|
|
1423
|
+
*/
|
|
1424
|
+
async submitWithdraw(params, onStatusUpdate) {
|
|
1425
|
+
const proofBytes = hexToBytes(params.proof);
|
|
1426
|
+
const proofBase64 = this.bytesToBase64(proofBytes);
|
|
1427
|
+
const requestBody = {
|
|
1428
|
+
outputs: params.outputs,
|
|
1429
|
+
policy: {
|
|
1430
|
+
fee_bps: params.feeBps
|
|
1431
|
+
},
|
|
1432
|
+
public_inputs: {
|
|
1433
|
+
root: params.publicInputs.root,
|
|
1434
|
+
nf: params.publicInputs.nf,
|
|
1435
|
+
amount: params.publicInputs.amount,
|
|
1436
|
+
fee_bps: params.feeBps,
|
|
1437
|
+
outputs_hash: params.publicInputs.outputs_hash
|
|
1438
|
+
},
|
|
1439
|
+
proof_bytes: proofBase64
|
|
1440
|
+
};
|
|
1441
|
+
const response = await fetch(`${this.baseUrl}/withdraw`, {
|
|
1442
|
+
method: "POST",
|
|
1443
|
+
headers: {
|
|
1444
|
+
"Content-Type": "application/json"
|
|
1445
|
+
},
|
|
1446
|
+
body: JSON.stringify(requestBody)
|
|
1447
|
+
});
|
|
1448
|
+
if (!response.ok) {
|
|
1449
|
+
let errorMessage = `${response.status} ${response.statusText}`;
|
|
1450
|
+
try {
|
|
1451
|
+
const errorText = await response.text();
|
|
1452
|
+
errorMessage = errorText || errorMessage;
|
|
1453
|
+
} catch {
|
|
1454
|
+
}
|
|
1455
|
+
throw new Error(`Relay withdraw failed: ${errorMessage}`);
|
|
1456
|
+
}
|
|
1457
|
+
const json = await response.json();
|
|
1458
|
+
if (!json.success) {
|
|
1459
|
+
throw new Error(json.error || "Relay withdraw failed");
|
|
1460
|
+
}
|
|
1461
|
+
const requestId = json.data?.request_id;
|
|
1462
|
+
if (!requestId) {
|
|
1463
|
+
throw new Error("Relay response missing request_id");
|
|
1464
|
+
}
|
|
1465
|
+
return this.pollForCompletion(requestId, onStatusUpdate);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Poll for withdrawal completion
|
|
1469
|
+
*
|
|
1470
|
+
* @param requestId - Request ID from relay service
|
|
1471
|
+
* @param onStatusUpdate - Optional callback for status updates
|
|
1472
|
+
* @returns Transaction signature when completed
|
|
1473
|
+
*/
|
|
1474
|
+
async pollForCompletion(requestId, onStatusUpdate) {
|
|
1475
|
+
let attempts = 0;
|
|
1476
|
+
const maxAttempts = 120;
|
|
1477
|
+
const pollInterval = 5e3;
|
|
1478
|
+
while (attempts < maxAttempts) {
|
|
1479
|
+
await this.sleep(pollInterval);
|
|
1480
|
+
attempts++;
|
|
1481
|
+
try {
|
|
1482
|
+
const statusResp = await fetch(`${this.baseUrl}/status/${requestId}`);
|
|
1483
|
+
if (!statusResp.ok) {
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const statusJson = await statusResp.json();
|
|
1487
|
+
const statusData = statusJson.data;
|
|
1488
|
+
const status = statusData?.status;
|
|
1489
|
+
if (onStatusUpdate && status) {
|
|
1490
|
+
onStatusUpdate(status);
|
|
1491
|
+
}
|
|
1492
|
+
if (status === "completed") {
|
|
1493
|
+
const txId = statusData?.tx_id;
|
|
1494
|
+
if (!txId) {
|
|
1495
|
+
throw new Error("Relay completed without tx_id");
|
|
1496
|
+
}
|
|
1497
|
+
return txId;
|
|
1498
|
+
}
|
|
1499
|
+
if (status === "failed") {
|
|
1500
|
+
throw new Error(statusData?.error || "Relay job failed");
|
|
1501
|
+
}
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
if (error instanceof Error && error.message.includes("failed")) {
|
|
1504
|
+
throw error;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`Withdrawal polling timed out after ${maxAttempts * pollInterval}ms`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Submit a swap transaction via relay
|
|
1514
|
+
*
|
|
1515
|
+
* Similar to submitWithdraw but includes swap parameters for token swaps.
|
|
1516
|
+
* The relay service will validate the proof, execute the swap, pay for fees,
|
|
1517
|
+
* and submit the transaction on-chain.
|
|
1518
|
+
*
|
|
1519
|
+
* @param params - Swap parameters
|
|
1520
|
+
* @param onStatusUpdate - Optional callback for status updates
|
|
1521
|
+
* @returns Transaction signature when completed
|
|
1522
|
+
*
|
|
1523
|
+
* @example
|
|
1524
|
+
* ```typescript
|
|
1525
|
+
* const signature = await relay.submitSwap({
|
|
1526
|
+
* proof: proofHex,
|
|
1527
|
+
* publicInputs: { root, nf, outputs_hash, amount },
|
|
1528
|
+
* outputs: [{ recipient: addr, amount: lamports }],
|
|
1529
|
+
* feeBps: 50,
|
|
1530
|
+
* swap: {
|
|
1531
|
+
* output_mint: tokenMint.toBase58(),
|
|
1532
|
+
* slippage_bps: 100,
|
|
1533
|
+
* min_output_amount: minAmount
|
|
1534
|
+
* }
|
|
1535
|
+
* }, (status) => console.log(`Status: ${status}`));
|
|
1536
|
+
* console.log(`Transaction: ${signature}`);
|
|
1537
|
+
* ```
|
|
1538
|
+
*/
|
|
1539
|
+
async submitSwap(params, onStatusUpdate) {
|
|
1540
|
+
const proofBytes = hexToBytes(params.proof);
|
|
1541
|
+
const proofBase64 = this.bytesToBase64(proofBytes);
|
|
1542
|
+
const requestBody = {
|
|
1543
|
+
outputs: params.outputs,
|
|
1544
|
+
swap: params.swap,
|
|
1545
|
+
policy: {
|
|
1546
|
+
fee_bps: params.feeBps
|
|
1547
|
+
},
|
|
1548
|
+
public_inputs: {
|
|
1549
|
+
root: params.publicInputs.root,
|
|
1550
|
+
nf: params.publicInputs.nf,
|
|
1551
|
+
amount: params.publicInputs.amount,
|
|
1552
|
+
fee_bps: params.feeBps,
|
|
1553
|
+
outputs_hash: params.publicInputs.outputs_hash
|
|
1554
|
+
},
|
|
1555
|
+
proof_bytes: proofBase64
|
|
1556
|
+
};
|
|
1557
|
+
const response = await fetch(`${this.baseUrl}/withdraw`, {
|
|
1558
|
+
method: "POST",
|
|
1559
|
+
headers: {
|
|
1560
|
+
"Content-Type": "application/json"
|
|
1561
|
+
},
|
|
1562
|
+
body: JSON.stringify(requestBody)
|
|
1563
|
+
});
|
|
1564
|
+
if (!response.ok) {
|
|
1565
|
+
let errorMessage = `${response.status} ${response.statusText}`;
|
|
1566
|
+
try {
|
|
1567
|
+
const errorText = await response.text();
|
|
1568
|
+
errorMessage = errorText || errorMessage;
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
throw new Error(`Relay swap failed: ${errorMessage}`);
|
|
1572
|
+
}
|
|
1573
|
+
const json = await response.json();
|
|
1574
|
+
if (!json.success) {
|
|
1575
|
+
throw new Error(json.error || "Relay swap failed");
|
|
1576
|
+
}
|
|
1577
|
+
const requestId = json.data?.request_id;
|
|
1578
|
+
if (!requestId) {
|
|
1579
|
+
throw new Error("Relay response missing request_id");
|
|
1580
|
+
}
|
|
1581
|
+
return this.pollForCompletion(requestId, onStatusUpdate);
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Get transaction status
|
|
1585
|
+
*
|
|
1586
|
+
* @param requestId - Request ID from previous submission
|
|
1587
|
+
* @returns Current status
|
|
1588
|
+
*
|
|
1589
|
+
* @example
|
|
1590
|
+
* ```typescript
|
|
1591
|
+
* const status = await relay.getStatus(requestId);
|
|
1592
|
+
* console.log(`Status: ${status.status}`);
|
|
1593
|
+
* if (status.status === 'completed') {
|
|
1594
|
+
* console.log(`TX: ${status.txId}`);
|
|
1595
|
+
* }
|
|
1596
|
+
* ```
|
|
1597
|
+
*/
|
|
1598
|
+
async getStatus(requestId) {
|
|
1599
|
+
const response = await fetch(`${this.baseUrl}/status/${requestId}`);
|
|
1600
|
+
if (!response.ok) {
|
|
1601
|
+
throw new Error(
|
|
1602
|
+
`Failed to get status: ${response.status} ${response.statusText}`
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
const json = await response.json();
|
|
1606
|
+
const data = json.data;
|
|
1607
|
+
return {
|
|
1608
|
+
status: data?.status || "pending",
|
|
1609
|
+
txId: data?.tx_id,
|
|
1610
|
+
error: data?.error
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Convert bytes to base64 string
|
|
1615
|
+
*/
|
|
1616
|
+
bytesToBase64(bytes) {
|
|
1617
|
+
if (typeof Buffer !== "undefined") {
|
|
1618
|
+
return Buffer.from(bytes).toString("base64");
|
|
1619
|
+
} else if (typeof btoa !== "undefined") {
|
|
1620
|
+
const binary = Array.from(bytes).map((b) => String.fromCharCode(b)).join("");
|
|
1621
|
+
return btoa(binary);
|
|
1622
|
+
} else {
|
|
1623
|
+
throw new Error("No base64 encoding method available");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Sleep utility
|
|
1628
|
+
*/
|
|
1629
|
+
sleep(ms) {
|
|
1630
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
// src/services/DepositRecoveryService.ts
|
|
1635
|
+
var import_web33 = require("@solana/web3.js");
|
|
1636
|
+
|
|
1637
|
+
// src/helpers/encrypted-output.ts
|
|
1638
|
+
function prepareEncryptedOutput(note, cloakKeys) {
|
|
1639
|
+
const noteData = {
|
|
1640
|
+
amount: note.amount,
|
|
1641
|
+
r: note.r,
|
|
1642
|
+
sk_spend: note.sk_spend,
|
|
1643
|
+
commitment: note.commitment
|
|
1644
|
+
};
|
|
1645
|
+
const encrypted = encryptNoteForRecipient(noteData, cloakKeys.view.pvk);
|
|
1646
|
+
return btoa(JSON.stringify(encrypted));
|
|
1647
|
+
}
|
|
1648
|
+
function prepareEncryptedOutputForRecipient(note, recipientPvkHex) {
|
|
1649
|
+
const noteData = {
|
|
1650
|
+
amount: note.amount,
|
|
1651
|
+
r: note.r,
|
|
1652
|
+
sk_spend: note.sk_spend,
|
|
1653
|
+
commitment: note.commitment
|
|
1654
|
+
};
|
|
1655
|
+
const recipientPvk = hexToBytes(recipientPvkHex);
|
|
1656
|
+
const encrypted = encryptNoteForRecipient(noteData, recipientPvk);
|
|
1657
|
+
return btoa(JSON.stringify(encrypted));
|
|
1658
|
+
}
|
|
1659
|
+
function encodeNoteSimple(note) {
|
|
1660
|
+
const data = {
|
|
1661
|
+
amount: note.amount,
|
|
1662
|
+
r: note.r,
|
|
1663
|
+
sk_spend: note.sk_spend,
|
|
1664
|
+
commitment: note.commitment
|
|
1665
|
+
};
|
|
1666
|
+
const json = JSON.stringify(data);
|
|
1667
|
+
if (typeof Buffer !== "undefined") {
|
|
1668
|
+
return Buffer.from(json).toString("base64");
|
|
1669
|
+
} else if (typeof btoa !== "undefined") {
|
|
1670
|
+
return btoa(json);
|
|
1671
|
+
} else {
|
|
1672
|
+
throw new Error("No base64 encoding method available");
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// src/services/DepositRecoveryService.ts
|
|
1677
|
+
var DepositRecoveryService = class {
|
|
1678
|
+
constructor(indexer, apiUrl) {
|
|
1679
|
+
this.indexer = indexer;
|
|
1680
|
+
this.apiUrl = apiUrl;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Recover a deposit that completed on-chain but failed to register
|
|
1684
|
+
*
|
|
1685
|
+
* @param options Recovery options
|
|
1686
|
+
* @returns Recovery result with updated note
|
|
1687
|
+
*/
|
|
1688
|
+
async recoverDeposit(options) {
|
|
1689
|
+
const { signature, commitment, note, onProgress } = options;
|
|
1690
|
+
try {
|
|
1691
|
+
onProgress?.("Validating inputs...");
|
|
1692
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{87,88}$/.test(signature)) {
|
|
1693
|
+
throw new CloakError(
|
|
1694
|
+
"Invalid transaction signature format",
|
|
1695
|
+
"validation",
|
|
1696
|
+
false
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
if (!/^[0-9a-f]{64}$/i.test(commitment)) {
|
|
1700
|
+
throw new CloakError(
|
|
1701
|
+
"Invalid commitment format",
|
|
1702
|
+
"validation",
|
|
1703
|
+
false
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
onProgress?.("Checking if deposit is already registered...");
|
|
1707
|
+
try {
|
|
1708
|
+
const existingInfo = await this.checkExistingDeposit(commitment);
|
|
1709
|
+
if (existingInfo) {
|
|
1710
|
+
onProgress?.("Deposit already registered!");
|
|
1711
|
+
return {
|
|
1712
|
+
success: true,
|
|
1713
|
+
...existingInfo,
|
|
1714
|
+
note: note ? updateNoteWithDeposit(note, {
|
|
1715
|
+
signature,
|
|
1716
|
+
slot: existingInfo.slot,
|
|
1717
|
+
leafIndex: existingInfo.leafIndex,
|
|
1718
|
+
root: existingInfo.root,
|
|
1719
|
+
merkleProof: existingInfo.merkleProof
|
|
1720
|
+
}) : void 0
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
} catch (e) {
|
|
1724
|
+
}
|
|
1725
|
+
onProgress?.("Fetching transaction details...");
|
|
1726
|
+
const connection = new import_web33.Connection(
|
|
1727
|
+
process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "https://api.devnet.solana.com"
|
|
1728
|
+
);
|
|
1729
|
+
const txDetails = await connection.getTransaction(signature, {
|
|
1730
|
+
commitment: "confirmed",
|
|
1731
|
+
maxSupportedTransactionVersion: 0
|
|
1732
|
+
});
|
|
1733
|
+
if (!txDetails) {
|
|
1734
|
+
throw new CloakError(
|
|
1735
|
+
"Transaction not found on blockchain",
|
|
1736
|
+
"validation",
|
|
1737
|
+
false
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
const slot = txDetails.slot;
|
|
1741
|
+
onProgress?.("Preparing encrypted output...");
|
|
1742
|
+
let encryptedOutput = "";
|
|
1743
|
+
if (note) {
|
|
1744
|
+
encryptedOutput = encodeNoteSimple(note);
|
|
1745
|
+
} else {
|
|
1746
|
+
encryptedOutput = btoa(JSON.stringify({ commitment }));
|
|
1747
|
+
}
|
|
1748
|
+
onProgress?.("Registering deposit with indexer...");
|
|
1749
|
+
const depositResponse = await this.indexer.submitDeposit({
|
|
1750
|
+
leafCommit: commitment,
|
|
1751
|
+
encryptedOutput,
|
|
1752
|
+
txSignature: signature,
|
|
1753
|
+
slot
|
|
1754
|
+
});
|
|
1755
|
+
if (!depositResponse.success) {
|
|
1756
|
+
throw new CloakError(
|
|
1757
|
+
"Failed to register deposit with indexer",
|
|
1758
|
+
"indexer",
|
|
1759
|
+
true
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
const leafIndex = depositResponse.leafIndex;
|
|
1763
|
+
const root = depositResponse.root;
|
|
1764
|
+
onProgress?.("Fetching Merkle proof...");
|
|
1765
|
+
const merkleProof = await this.indexer.getMerkleProof(leafIndex);
|
|
1766
|
+
onProgress?.("Recovery complete!");
|
|
1767
|
+
const updatedNote = note ? updateNoteWithDeposit(note, {
|
|
1768
|
+
signature,
|
|
1769
|
+
slot,
|
|
1770
|
+
leafIndex,
|
|
1771
|
+
root,
|
|
1772
|
+
merkleProof: {
|
|
1773
|
+
pathElements: merkleProof.pathElements,
|
|
1774
|
+
pathIndices: merkleProof.pathIndices
|
|
1775
|
+
}
|
|
1776
|
+
}) : void 0;
|
|
1777
|
+
return {
|
|
1778
|
+
success: true,
|
|
1779
|
+
leafIndex,
|
|
1780
|
+
root,
|
|
1781
|
+
slot,
|
|
1782
|
+
merkleProof: {
|
|
1783
|
+
pathElements: merkleProof.pathElements,
|
|
1784
|
+
pathIndices: merkleProof.pathIndices
|
|
1785
|
+
},
|
|
1786
|
+
note: updatedNote
|
|
1787
|
+
};
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1790
|
+
if (errorMessage.includes("duplicate key") || errorMessage.includes("already exists")) {
|
|
1791
|
+
try {
|
|
1792
|
+
const existingInfo = await this.checkExistingDeposit(commitment);
|
|
1793
|
+
if (existingInfo) {
|
|
1794
|
+
return {
|
|
1795
|
+
success: true,
|
|
1796
|
+
...existingInfo,
|
|
1797
|
+
note: note ? updateNoteWithDeposit(note, {
|
|
1798
|
+
signature,
|
|
1799
|
+
slot: existingInfo.slot,
|
|
1800
|
+
leafIndex: existingInfo.leafIndex,
|
|
1801
|
+
root: existingInfo.root,
|
|
1802
|
+
merkleProof: existingInfo.merkleProof
|
|
1803
|
+
}) : void 0
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return {
|
|
1810
|
+
success: false,
|
|
1811
|
+
error: errorMessage
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Check if a deposit already exists in the indexer
|
|
1817
|
+
*
|
|
1818
|
+
* @private
|
|
1819
|
+
*/
|
|
1820
|
+
async checkExistingDeposit(_commitment) {
|
|
1821
|
+
try {
|
|
1822
|
+
const { next_index } = await this.indexer.getMerkleRoot();
|
|
1823
|
+
const batchSize = 100;
|
|
1824
|
+
for (let i = 0; i < next_index; i += batchSize) {
|
|
1825
|
+
const end = Math.min(i + batchSize - 1, next_index - 1);
|
|
1826
|
+
const { notes } = await this.indexer.getNotesRange(i, end, batchSize);
|
|
1827
|
+
for (let j = 0; j < notes.length; j++) {
|
|
1828
|
+
try {
|
|
1829
|
+
return null;
|
|
1830
|
+
} catch (e) {
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return null;
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Finalize a deposit via server API (alternative recovery method)
|
|
1842
|
+
*
|
|
1843
|
+
* This method calls a server-side endpoint that can handle
|
|
1844
|
+
* the recovery process with elevated permissions.
|
|
1845
|
+
*/
|
|
1846
|
+
async finalizeDepositViaServer(signature, commitment, encryptedOutput) {
|
|
1847
|
+
try {
|
|
1848
|
+
const response = await fetch(`${this.apiUrl}/api/deposit/finalize`, {
|
|
1849
|
+
method: "POST",
|
|
1850
|
+
headers: { "Content-Type": "application/json" },
|
|
1851
|
+
body: JSON.stringify({
|
|
1852
|
+
tx_signature: signature,
|
|
1853
|
+
commitment,
|
|
1854
|
+
encrypted_output: encryptedOutput || btoa(JSON.stringify({ commitment }))
|
|
1855
|
+
})
|
|
1856
|
+
});
|
|
1857
|
+
if (!response.ok) {
|
|
1858
|
+
const errorText = await response.text();
|
|
1859
|
+
throw new Error(`Recovery failed: ${errorText}`);
|
|
1860
|
+
}
|
|
1861
|
+
const data = await response.json();
|
|
1862
|
+
if (!data.success) {
|
|
1863
|
+
throw new Error(data.error || "Recovery failed");
|
|
1864
|
+
}
|
|
1865
|
+
if (!data.leaf_index || !data.root || data.slot === void 0 || !data.merkle_proof) {
|
|
1866
|
+
throw new Error("Recovery response missing required fields");
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
success: true,
|
|
1870
|
+
leafIndex: data.leaf_index,
|
|
1871
|
+
root: data.root,
|
|
1872
|
+
slot: data.slot,
|
|
1873
|
+
merkleProof: {
|
|
1874
|
+
pathElements: data.merkle_proof.path_elements,
|
|
1875
|
+
pathIndices: data.merkle_proof.path_indices
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
return {
|
|
1880
|
+
success: false,
|
|
1881
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// src/solana/instructions.ts
|
|
1888
|
+
var import_web34 = require("@solana/web3.js");
|
|
1889
|
+
function createDepositInstruction(params) {
|
|
1890
|
+
if (params.commitment.length !== 32) {
|
|
1891
|
+
throw new Error(
|
|
1892
|
+
`Invalid commitment length: ${params.commitment.length} (expected 32 bytes)`
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
if (params.amount <= 0) {
|
|
1896
|
+
throw new Error("Amount must be positive");
|
|
1897
|
+
}
|
|
1898
|
+
const discriminant = new Uint8Array([1]);
|
|
1899
|
+
const amountBytes = new Uint8Array(8);
|
|
1900
|
+
new DataView(amountBytes.buffer).setBigUint64(
|
|
1901
|
+
0,
|
|
1902
|
+
BigInt(params.amount),
|
|
1903
|
+
true
|
|
1904
|
+
// little-endian
|
|
1905
|
+
);
|
|
1906
|
+
const data = new Uint8Array(41);
|
|
1907
|
+
data.set(discriminant, 0);
|
|
1908
|
+
data.set(amountBytes, 1);
|
|
1909
|
+
data.set(params.commitment, 9);
|
|
1910
|
+
return new import_web34.TransactionInstruction({
|
|
1911
|
+
programId: params.programId,
|
|
1912
|
+
keys: [
|
|
1913
|
+
// Account 0: Payer (signer, writable) - pays for transaction
|
|
1914
|
+
{ pubkey: params.payer, isSigner: true, isWritable: true },
|
|
1915
|
+
// Account 1: Pool (writable) - receives SOL
|
|
1916
|
+
{ pubkey: params.pool, isSigner: false, isWritable: true },
|
|
1917
|
+
// Account 2: System Program (readonly) - for transfers
|
|
1918
|
+
{ pubkey: import_web34.SystemProgram.programId, isSigner: false, isWritable: false },
|
|
1919
|
+
// Account 3: Merkle Tree (writable) - stores on-chain Merkle tree
|
|
1920
|
+
{ pubkey: params.merkleTree, isSigner: false, isWritable: true }
|
|
1921
|
+
],
|
|
1922
|
+
data: Buffer.from(data)
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function validateDepositParams(params) {
|
|
1926
|
+
if (!(params.programId instanceof import_web34.PublicKey)) {
|
|
1927
|
+
throw new Error("programId must be a PublicKey");
|
|
1928
|
+
}
|
|
1929
|
+
if (!(params.payer instanceof import_web34.PublicKey)) {
|
|
1930
|
+
throw new Error("payer must be a PublicKey");
|
|
1931
|
+
}
|
|
1932
|
+
if (!(params.pool instanceof import_web34.PublicKey)) {
|
|
1933
|
+
throw new Error("pool must be a PublicKey");
|
|
1934
|
+
}
|
|
1935
|
+
if (!(params.merkleTree instanceof import_web34.PublicKey)) {
|
|
1936
|
+
throw new Error("merkleTree must be a PublicKey");
|
|
1937
|
+
}
|
|
1938
|
+
if (typeof params.amount !== "number" || params.amount <= 0) {
|
|
1939
|
+
throw new Error("amount must be a positive number");
|
|
1940
|
+
}
|
|
1941
|
+
if (!(params.commitment instanceof Uint8Array)) {
|
|
1942
|
+
throw new Error("commitment must be a Uint8Array");
|
|
1943
|
+
}
|
|
1944
|
+
if (params.commitment.length !== 32) {
|
|
1945
|
+
throw new Error(
|
|
1946
|
+
`commitment must be 32 bytes (got ${params.commitment.length})`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/utils/pda.ts
|
|
1952
|
+
var import_web35 = require("@solana/web3.js");
|
|
1953
|
+
function getShieldPoolPDAs(programId, mint) {
|
|
1954
|
+
const pid = programId || CLOAK_PROGRAM_ID;
|
|
1955
|
+
const mintKey = mint ?? new import_web35.PublicKey(
|
|
1956
|
+
// 32 zero bytes, matching Rust `Pubkey::default()`
|
|
1957
|
+
new Uint8Array(32)
|
|
1958
|
+
);
|
|
1959
|
+
const [pool] = import_web35.PublicKey.findProgramAddressSync(
|
|
1960
|
+
[Buffer.from("pool"), mintKey.toBytes()],
|
|
1961
|
+
pid
|
|
1962
|
+
);
|
|
1963
|
+
const [merkleTree] = import_web35.PublicKey.findProgramAddressSync(
|
|
1964
|
+
[Buffer.from("merkle_tree"), mintKey.toBytes()],
|
|
1965
|
+
pid
|
|
1966
|
+
);
|
|
1967
|
+
const [commitments] = import_web35.PublicKey.findProgramAddressSync(
|
|
1968
|
+
[Buffer.from("commitments"), mintKey.toBytes()],
|
|
1969
|
+
pid
|
|
1970
|
+
);
|
|
1971
|
+
const [rootsRing] = import_web35.PublicKey.findProgramAddressSync(
|
|
1972
|
+
[Buffer.from("roots_ring"), mintKey.toBytes()],
|
|
1973
|
+
pid
|
|
1974
|
+
);
|
|
1975
|
+
const [nullifierShard] = import_web35.PublicKey.findProgramAddressSync(
|
|
1976
|
+
[Buffer.from("nullifier_shard"), mintKey.toBytes()],
|
|
1977
|
+
pid
|
|
1978
|
+
);
|
|
1979
|
+
const [treasury] = import_web35.PublicKey.findProgramAddressSync(
|
|
1980
|
+
[Buffer.from("treasury"), mintKey.toBytes()],
|
|
1981
|
+
pid
|
|
1982
|
+
);
|
|
1983
|
+
return {
|
|
1984
|
+
pool,
|
|
1985
|
+
merkleTree,
|
|
1986
|
+
commitments,
|
|
1987
|
+
rootsRing,
|
|
1988
|
+
nullifierShard,
|
|
1989
|
+
treasury
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/utils/proof-generation.ts
|
|
1994
|
+
var snarkjs = __toESM(require("snarkjs"), 1);
|
|
1995
|
+
var path = __toESM(require("path"), 1);
|
|
1996
|
+
var fs = __toESM(require("fs"), 1);
|
|
1997
|
+
async function generateWithdrawRegularProof(inputs, circuitsPath) {
|
|
1998
|
+
const wasmPath = path.join(circuitsPath, "build", "withdraw_regular_js", "withdraw_regular.wasm");
|
|
1999
|
+
const zkeyPath = path.join(circuitsPath, "build", "withdraw_regular_final.zkey");
|
|
2000
|
+
if (!fs.existsSync(wasmPath)) {
|
|
2001
|
+
throw new Error(`Circuit WASM not found at ${wasmPath}. Run 'just circuits-compile' in packages-new/circuits first.`);
|
|
2002
|
+
}
|
|
2003
|
+
if (!fs.existsSync(zkeyPath)) {
|
|
2004
|
+
throw new Error(`Circuit zkey not found at ${zkeyPath}. Run circuit setup first.`);
|
|
2005
|
+
}
|
|
2006
|
+
const circuitInputs = {
|
|
2007
|
+
// Public signals
|
|
2008
|
+
root: inputs.root.toString(),
|
|
2009
|
+
nullifier: inputs.nullifier.toString(),
|
|
2010
|
+
outputs_hash: inputs.outputs_hash.toString(),
|
|
2011
|
+
public_amount: inputs.public_amount.toString(),
|
|
2012
|
+
// Private common inputs
|
|
2013
|
+
amount: inputs.amount.toString(),
|
|
2014
|
+
leaf_index: inputs.leaf_index.toString(),
|
|
2015
|
+
sk: inputs.sk.map((x) => x.toString()),
|
|
2016
|
+
r: inputs.r.map((x) => x.toString()),
|
|
2017
|
+
pathElements: inputs.pathElements.map((x) => x.toString()),
|
|
2018
|
+
pathIndices: inputs.pathIndices,
|
|
2019
|
+
// Outputs
|
|
2020
|
+
num_outputs: inputs.num_outputs,
|
|
2021
|
+
out_addr: inputs.out_addr.map((addr) => addr.map((x) => x.toString())),
|
|
2022
|
+
out_amount: inputs.out_amount.map((x) => x.toString()),
|
|
2023
|
+
out_flags: inputs.out_flags,
|
|
2024
|
+
// Fee calculation
|
|
2025
|
+
var_fee: inputs.var_fee.toString(),
|
|
2026
|
+
rem: inputs.rem.toString()
|
|
2027
|
+
};
|
|
2028
|
+
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
|
|
2029
|
+
circuitInputs,
|
|
2030
|
+
wasmPath,
|
|
2031
|
+
zkeyPath
|
|
2032
|
+
);
|
|
2033
|
+
const proofBytes = proofToBytes(proof);
|
|
2034
|
+
return {
|
|
2035
|
+
proof,
|
|
2036
|
+
publicSignals,
|
|
2037
|
+
proofBytes,
|
|
2038
|
+
publicInputsBytes: new Uint8Array(0)
|
|
2039
|
+
// Not used, kept for compatibility
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
async function generateWithdrawSwapProof(inputs, circuitsPath) {
|
|
2043
|
+
const wasmPath = path.join(circuitsPath, "build", "withdraw_swap_js", "withdraw_swap.wasm");
|
|
2044
|
+
const zkeyPath = path.join(circuitsPath, "build", "withdraw_swap_final.zkey");
|
|
2045
|
+
if (!fs.existsSync(wasmPath)) {
|
|
2046
|
+
throw new Error(`Circuit WASM not found at ${wasmPath}. Run 'just circuits-compile' in packages-new/circuits first.`);
|
|
2047
|
+
}
|
|
2048
|
+
if (!fs.existsSync(zkeyPath)) {
|
|
2049
|
+
throw new Error(`Circuit zkey not found at ${zkeyPath}. Run circuit setup first.`);
|
|
2050
|
+
}
|
|
2051
|
+
const sk = splitTo2Limbs(inputs.sk_spend);
|
|
2052
|
+
const r = splitTo2Limbs(inputs.r);
|
|
2053
|
+
const circuitInputs = {
|
|
2054
|
+
// Public signals (must be provided for witness generation)
|
|
2055
|
+
root: inputs.root.toString(),
|
|
2056
|
+
nullifier: inputs.nullifier.toString(),
|
|
2057
|
+
outputs_hash: inputs.outputs_hash.toString(),
|
|
2058
|
+
public_amount: inputs.public_amount.toString(),
|
|
2059
|
+
// Private inputs
|
|
2060
|
+
sk: sk.map((x) => x.toString()),
|
|
2061
|
+
r: r.map((x) => x.toString()),
|
|
2062
|
+
amount: inputs.amount.toString(),
|
|
2063
|
+
leaf_index: inputs.leaf_index.toString(),
|
|
2064
|
+
pathElements: inputs.path_elements.map((x) => x.toString()),
|
|
2065
|
+
pathIndices: inputs.path_indices,
|
|
2066
|
+
input_mint: inputs.input_mint.map((x) => x.toString()),
|
|
2067
|
+
output_mint: inputs.output_mint.map((x) => x.toString()),
|
|
2068
|
+
recipient_ata: inputs.recipient_ata.map((x) => x.toString()),
|
|
2069
|
+
min_output_amount: inputs.min_output_amount.toString(),
|
|
2070
|
+
// Note: swap circuit computes fees internally:
|
|
2071
|
+
// - fixed_fee is hardcoded to 2500000 in the circuit (not an input)
|
|
2072
|
+
// - var_fee and rem are computed from amount * 5 / 1000
|
|
2073
|
+
var_fee: inputs.var_fee.toString(),
|
|
2074
|
+
rem: inputs.rem.toString()
|
|
2075
|
+
};
|
|
2076
|
+
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
|
|
2077
|
+
circuitInputs,
|
|
2078
|
+
wasmPath,
|
|
2079
|
+
zkeyPath
|
|
2080
|
+
);
|
|
2081
|
+
const proofBytes = proofToBytes(proof);
|
|
2082
|
+
return {
|
|
2083
|
+
proof,
|
|
2084
|
+
publicSignals,
|
|
2085
|
+
proofBytes,
|
|
2086
|
+
publicInputsBytes: new Uint8Array(0)
|
|
2087
|
+
// Not used, kept for compatibility
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
function areCircuitsAvailable(circuitsPath) {
|
|
2091
|
+
const wasmPath = path.join(circuitsPath, "build", "withdraw_regular_js", "withdraw_regular.wasm");
|
|
2092
|
+
const zkeyPath = path.join(circuitsPath, "build", "withdraw_regular_final.zkey");
|
|
2093
|
+
return fs.existsSync(wasmPath) && fs.existsSync(zkeyPath);
|
|
2094
|
+
}
|
|
2095
|
+
function getDefaultCircuitsPath() {
|
|
2096
|
+
const possiblePaths = [
|
|
2097
|
+
path.resolve(process.cwd(), "../../packages-new/circuits"),
|
|
2098
|
+
path.resolve(process.cwd(), "../packages-new/circuits"),
|
|
2099
|
+
path.resolve(process.cwd(), "packages-new/circuits"),
|
|
2100
|
+
path.resolve(process.cwd(), "../../circuits"),
|
|
2101
|
+
path.resolve(process.cwd(), "../circuits")
|
|
2102
|
+
];
|
|
2103
|
+
for (const p of possiblePaths) {
|
|
2104
|
+
if (areCircuitsAvailable(p)) {
|
|
2105
|
+
return p;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return possiblePaths[0];
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// src/core/CloakSDK.ts
|
|
2112
|
+
var CLOAK_PROGRAM_ID = new import_web36.PublicKey("c1oak6tetxYnNfvXKFkpn1d98FxtK7B68vBQLYQpWKp");
|
|
2113
|
+
var CLOAK_API_URL = "https://api.cloak.ag";
|
|
2114
|
+
var CloakSDK = class {
|
|
2115
|
+
/**
|
|
2116
|
+
* Create a new Cloak SDK client
|
|
2117
|
+
*
|
|
2118
|
+
* @param config - Client configuration
|
|
2119
|
+
*
|
|
2120
|
+
* @example
|
|
2121
|
+
* ```typescript
|
|
2122
|
+
* const sdk = new CloakSDK({
|
|
2123
|
+
* keypairBytes: keypair.secretKey,
|
|
2124
|
+
* network: "devnet"
|
|
2125
|
+
* });
|
|
2126
|
+
*/
|
|
2127
|
+
constructor(config) {
|
|
2128
|
+
this.keypair = import_web36.Keypair.fromSecretKey(config.keypairBytes);
|
|
2129
|
+
this.cloakKeys = config.cloakKeys;
|
|
2130
|
+
this.storage = config.storage || new MemoryStorageAdapter();
|
|
2131
|
+
const indexerUrl = config.indexerUrl || process.env.CLOAK_INDEXER_URL || CLOAK_API_URL;
|
|
2132
|
+
const relayUrl = config.relayUrl || process.env.CLOAK_RELAY_URL || CLOAK_API_URL;
|
|
2133
|
+
this.indexer = new IndexerService(indexerUrl);
|
|
2134
|
+
this.artifactProver = new ArtifactProverService(indexerUrl, 5 * 60 * 1e3, 2e3);
|
|
2135
|
+
this.relay = new RelayService(relayUrl);
|
|
2136
|
+
this.depositRecovery = new DepositRecoveryService(this.indexer, indexerUrl);
|
|
2137
|
+
if (!this.cloakKeys) {
|
|
2138
|
+
const storedKeys = this.storage.loadKeys();
|
|
2139
|
+
if (storedKeys && !(storedKeys instanceof Promise)) {
|
|
2140
|
+
this.cloakKeys = storedKeys;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const programId = config.programId || CLOAK_PROGRAM_ID;
|
|
2144
|
+
const { pool, merkleTree, commitments, rootsRing, nullifierShard, treasury } = getShieldPoolPDAs(programId);
|
|
2145
|
+
this.config = {
|
|
2146
|
+
network: config.network || "devnet",
|
|
2147
|
+
keypairBytes: config.keypairBytes,
|
|
2148
|
+
cloakKeys: config.cloakKeys,
|
|
2149
|
+
apiUrl: indexerUrl,
|
|
2150
|
+
// Store indexer URL as apiUrl for backwards compatibility
|
|
2151
|
+
programId,
|
|
2152
|
+
poolAddress: pool,
|
|
2153
|
+
merkleTreeAddress: merkleTree,
|
|
2154
|
+
commitmentsAddress: commitments,
|
|
2155
|
+
rootsRingAddress: rootsRing,
|
|
2156
|
+
nullifierShardAddress: nullifierShard,
|
|
2157
|
+
treasuryAddress: treasury
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Deposit SOL into the Cloak protocol
|
|
2162
|
+
*
|
|
2163
|
+
* Creates a new note (or uses a provided one), submits a deposit transaction,
|
|
2164
|
+
* and registers with the indexer.
|
|
2165
|
+
*
|
|
2166
|
+
* @param connection - Solana connection
|
|
2167
|
+
* @param payer - Payer wallet with sendTransaction method
|
|
2168
|
+
* @param amountOrNote - Amount in lamports OR an existing note to deposit
|
|
2169
|
+
* @param options - Optional configuration
|
|
2170
|
+
* @returns Deposit result with note and transaction info
|
|
2171
|
+
*
|
|
2172
|
+
* @example
|
|
2173
|
+
* ```typescript
|
|
2174
|
+
* // Generate and deposit in one step
|
|
2175
|
+
* const result = await client.deposit(
|
|
2176
|
+
* connection,
|
|
2177
|
+
* wallet,
|
|
2178
|
+
* 1_000_000_000,
|
|
2179
|
+
* {
|
|
2180
|
+
* onProgress: (status) => console.log(status)
|
|
2181
|
+
* }
|
|
2182
|
+
* );
|
|
2183
|
+
*
|
|
2184
|
+
* // Or deposit a pre-generated note
|
|
2185
|
+
* const note = client.generateNote(1_000_000_000);
|
|
2186
|
+
* const result = await client.deposit(connection, wallet, note);
|
|
2187
|
+
* ```
|
|
2188
|
+
*/
|
|
2189
|
+
async deposit(connection, amountOrNote, options) {
|
|
2190
|
+
try {
|
|
2191
|
+
let note;
|
|
2192
|
+
if (typeof amountOrNote === "number") {
|
|
2193
|
+
note = await generateNote(amountOrNote, this.config.network);
|
|
2194
|
+
} else {
|
|
2195
|
+
note = amountOrNote;
|
|
2196
|
+
if (note.depositSignature) {
|
|
2197
|
+
throw new Error("Note has already been deposited");
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
const balance = await connection.getBalance(this.keypair.publicKey);
|
|
2201
|
+
const requiredAmount = note.amount + 5e3;
|
|
2202
|
+
if (balance < requiredAmount) {
|
|
2203
|
+
throw new Error(
|
|
2204
|
+
`Insufficient balance. Required: ${requiredAmount} lamports (${note.amount} + fees), Available: ${balance} lamports`
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
const commitmentBytes = hexToBytes(note.commitment);
|
|
2208
|
+
const programId = this.config.programId || CLOAK_PROGRAM_ID;
|
|
2209
|
+
const depositIx = createDepositInstruction({
|
|
2210
|
+
programId,
|
|
2211
|
+
payer: this.keypair.publicKey,
|
|
2212
|
+
pool: this.config.poolAddress,
|
|
2213
|
+
merkleTree: this.config.merkleTreeAddress,
|
|
2214
|
+
amount: note.amount,
|
|
2215
|
+
commitment: commitmentBytes
|
|
2216
|
+
});
|
|
2217
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
2218
|
+
const transaction = new import_web36.Transaction({
|
|
2219
|
+
feePayer: this.keypair.publicKey,
|
|
2220
|
+
blockhash,
|
|
2221
|
+
lastValidBlockHeight
|
|
2222
|
+
}).add(depositIx);
|
|
2223
|
+
if (!options?.skipPreflight) {
|
|
2224
|
+
const simulation = await connection.simulateTransaction(transaction);
|
|
2225
|
+
if (simulation.value.err) {
|
|
2226
|
+
const logs = simulation.value.logs?.join("\n") || "No logs";
|
|
2227
|
+
throw new Error(
|
|
2228
|
+
`Transaction simulation failed: ${JSON.stringify(simulation.value.err)}
|
|
2229
|
+
Logs:
|
|
2230
|
+
${logs}`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const signature = await connection.sendTransaction(transaction, [this.keypair], {
|
|
2235
|
+
skipPreflight: options?.skipPreflight || false,
|
|
2236
|
+
preflightCommitment: "confirmed",
|
|
2237
|
+
maxRetries: 3
|
|
2238
|
+
});
|
|
2239
|
+
const confirmation = await connection.confirmTransaction({
|
|
2240
|
+
signature,
|
|
2241
|
+
blockhash,
|
|
2242
|
+
lastValidBlockHeight
|
|
2243
|
+
});
|
|
2244
|
+
if (confirmation.value.err) {
|
|
2245
|
+
throw new Error(
|
|
2246
|
+
`Transaction failed: ${JSON.stringify(confirmation.value.err)}`
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
const txDetails = await connection.getTransaction(signature, {
|
|
2250
|
+
commitment: "confirmed",
|
|
2251
|
+
maxSupportedTransactionVersion: 0
|
|
2252
|
+
});
|
|
2253
|
+
const depositSlot = txDetails?.slot ?? 0;
|
|
2254
|
+
let leafIndex = null;
|
|
2255
|
+
let root = null;
|
|
2256
|
+
try {
|
|
2257
|
+
if (txDetails?.meta?.logMessages) {
|
|
2258
|
+
for (const log of txDetails.meta.logMessages) {
|
|
2259
|
+
if (log.includes("Program data:")) {
|
|
2260
|
+
try {
|
|
2261
|
+
const parts = log.split("Program data:");
|
|
2262
|
+
if (parts.length > 1) {
|
|
2263
|
+
const base64Data = parts[1].trim();
|
|
2264
|
+
const eventData = Buffer.from(base64Data, "base64");
|
|
2265
|
+
if (eventData.length >= 81 && eventData[0] === 1) {
|
|
2266
|
+
const parsedLeafIndex = Number(eventData.readBigUInt64LE(1));
|
|
2267
|
+
const loggedCommitment = eventData.slice(9, 41);
|
|
2268
|
+
if (Buffer.compare(loggedCommitment, Buffer.from(commitmentBytes)) === 0) {
|
|
2269
|
+
leafIndex = parsedLeafIndex;
|
|
2270
|
+
const loggedRoot = eventData.slice(49, 81);
|
|
2271
|
+
root = Buffer.from(loggedRoot).toString("hex");
|
|
2272
|
+
break;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
} catch (e) {
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
}
|
|
2284
|
+
if (leafIndex === null) {
|
|
2285
|
+
const commitmentHex = Buffer.from(commitmentBytes).toString("hex").toLowerCase();
|
|
2286
|
+
const maxRetries2 = 60;
|
|
2287
|
+
const initialRetryDelay2 = 500;
|
|
2288
|
+
for (let attempt = 0; attempt < maxRetries2; attempt++) {
|
|
2289
|
+
try {
|
|
2290
|
+
const indexerBaseUrl = this.config.apiUrl || CLOAK_API_URL;
|
|
2291
|
+
const response = await fetch(`${indexerBaseUrl}/api/v1/deposit/${commitmentHex}`);
|
|
2292
|
+
if (response.ok) {
|
|
2293
|
+
const data = await response.json();
|
|
2294
|
+
leafIndex = data.leaf_index;
|
|
2295
|
+
const rootResponse = await this.indexer.getMerkleRoot();
|
|
2296
|
+
root = rootResponse.root;
|
|
2297
|
+
break;
|
|
2298
|
+
} else if (response.status === 404 && attempt < maxRetries2 - 1) {
|
|
2299
|
+
const delay = Math.min(initialRetryDelay2 * (attempt + 1), 2e3);
|
|
2300
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
} catch (e) {
|
|
2304
|
+
if (attempt < maxRetries2 - 1) {
|
|
2305
|
+
const delay = Math.min(initialRetryDelay2 * (attempt + 1), 2e3);
|
|
2306
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (leafIndex === null) {
|
|
2313
|
+
const programId2 = this.config.programId || CLOAK_PROGRAM_ID;
|
|
2314
|
+
const mintForSOL = new import_web36.PublicKey(new Uint8Array(32));
|
|
2315
|
+
const { merkleTree } = getShieldPoolPDAs(programId2, mintForSOL);
|
|
2316
|
+
const merkleTreeAccount = await connection.getAccountInfo(merkleTree);
|
|
2317
|
+
if (!merkleTreeAccount) {
|
|
2318
|
+
throw new Error("Failed to fetch merkle tree account");
|
|
2319
|
+
}
|
|
2320
|
+
const nextIndex = Number(merkleTreeAccount.data.readBigUInt64LE(32));
|
|
2321
|
+
leafIndex = nextIndex - 1;
|
|
2322
|
+
const rootBytes = merkleTreeAccount.data.slice(1064, 1096);
|
|
2323
|
+
root = Buffer.from(rootBytes).toString("hex");
|
|
2324
|
+
}
|
|
2325
|
+
if (leafIndex === null || root === null) {
|
|
2326
|
+
throw new Error("Failed to get leaf index and root from transaction, indexer, or on-chain state");
|
|
2327
|
+
}
|
|
2328
|
+
let merkleProof = null;
|
|
2329
|
+
const maxRetries = 60;
|
|
2330
|
+
const initialRetryDelay = 500;
|
|
2331
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
2332
|
+
try {
|
|
2333
|
+
merkleProof = await this.indexer.getMerkleProof(leafIndex);
|
|
2334
|
+
break;
|
|
2335
|
+
} catch (error) {
|
|
2336
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2337
|
+
if (errorMessage.includes("404") && attempt < maxRetries - 1) {
|
|
2338
|
+
const delay = Math.min(initialRetryDelay * (attempt + 1), 2e3);
|
|
2339
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
throw error;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
if (!merkleProof) {
|
|
2346
|
+
throw new Error("Failed to get Merkle proof after retries");
|
|
2347
|
+
}
|
|
2348
|
+
const updatedNote = updateNoteWithDeposit(note, {
|
|
2349
|
+
signature,
|
|
2350
|
+
slot: depositSlot,
|
|
2351
|
+
leafIndex,
|
|
2352
|
+
root,
|
|
2353
|
+
merkleProof: {
|
|
2354
|
+
pathElements: merkleProof.pathElements,
|
|
2355
|
+
pathIndices: merkleProof.pathIndices
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
return {
|
|
2359
|
+
note: updatedNote,
|
|
2360
|
+
signature,
|
|
2361
|
+
leafIndex,
|
|
2362
|
+
root
|
|
2363
|
+
};
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
throw this.wrapError(error, "Deposit failed");
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Private transfer with up to 5 recipients
|
|
2370
|
+
*
|
|
2371
|
+
* Handles the complete private transfer flow:
|
|
2372
|
+
* 1. If note is not deposited, deposits it first and waits for confirmation
|
|
2373
|
+
* 2. Generates a zero-knowledge proof
|
|
2374
|
+
* 3. Submits the withdrawal via relay service to recipients
|
|
2375
|
+
*
|
|
2376
|
+
* This is the main method for performing private transfers - it handles everything!
|
|
2377
|
+
*
|
|
2378
|
+
* @param connection - Solana connection (required for deposit if not already deposited)
|
|
2379
|
+
* @param payer - Payer wallet (required for deposit if not already deposited)
|
|
2380
|
+
* @param note - Note to spend (can be deposited or not)
|
|
2381
|
+
* @param recipients - Array of 1-5 recipients with amounts
|
|
2382
|
+
* @param options - Optional configuration
|
|
2383
|
+
* @returns Transfer result with signature and outputs
|
|
2384
|
+
*
|
|
2385
|
+
* @example
|
|
2386
|
+
* ```typescript
|
|
2387
|
+
* // Create a note (not deposited yet)
|
|
2388
|
+
* const note = client.generateNote(1_000_000_000);
|
|
2389
|
+
*
|
|
2390
|
+
* // privateTransfer handles the full flow: deposit + withdraw
|
|
2391
|
+
* const result = await client.privateTransfer(
|
|
2392
|
+
* connection,
|
|
2393
|
+
* wallet,
|
|
2394
|
+
* note,
|
|
2395
|
+
* [
|
|
2396
|
+
* { recipient: new PublicKey("..."), amount: 500_000_000 },
|
|
2397
|
+
* { recipient: new PublicKey("..."), amount: 492_500_000 }
|
|
2398
|
+
* ],
|
|
2399
|
+
* {
|
|
2400
|
+
* relayFeeBps: 50, // 0.5%
|
|
2401
|
+
* onProgress: (status) => console.log(status),
|
|
2402
|
+
* onProofProgress: (pct) => console.log(`Proof: ${pct}%`)
|
|
2403
|
+
* }
|
|
2404
|
+
* );
|
|
2405
|
+
* console.log(`Success! TX: ${result.signature}`);
|
|
2406
|
+
* ```
|
|
2407
|
+
*/
|
|
2408
|
+
async privateTransfer(connection, note, recipients, options) {
|
|
2409
|
+
if (!isWithdrawable(note)) {
|
|
2410
|
+
const depositResult = await this.deposit(connection, note, {
|
|
2411
|
+
skipPreflight: false
|
|
2412
|
+
});
|
|
2413
|
+
note = depositResult.note;
|
|
2414
|
+
}
|
|
2415
|
+
const protocolFee = note.amount - getDistributableAmount2(note.amount);
|
|
2416
|
+
const feeBps = Math.ceil(protocolFee * 1e4 / note.amount);
|
|
2417
|
+
const distributableAmount = getDistributableAmount2(note.amount);
|
|
2418
|
+
validateTransfers(recipients, distributableAmount);
|
|
2419
|
+
let merkleProof;
|
|
2420
|
+
let merkleRoot;
|
|
2421
|
+
if (note.merkleProof && note.root) {
|
|
2422
|
+
merkleProof = {
|
|
2423
|
+
pathElements: note.merkleProof.pathElements,
|
|
2424
|
+
pathIndices: note.merkleProof.pathIndices
|
|
2425
|
+
};
|
|
2426
|
+
merkleRoot = note.root;
|
|
2427
|
+
} else {
|
|
2428
|
+
merkleProof = await this.indexer.getMerkleProof(note.leafIndex);
|
|
2429
|
+
merkleRoot = merkleProof.root || (await this.indexer.getMerkleRoot()).root;
|
|
2430
|
+
}
|
|
2431
|
+
const nullifier = await computeNullifierAsync(note.sk_spend, note.leafIndex);
|
|
2432
|
+
const nullifierHex = nullifier.toString(16).padStart(64, "0");
|
|
2433
|
+
const outputsHash = await computeOutputsHashAsync(recipients);
|
|
2434
|
+
const outputsHashHex = outputsHash.toString(16).padStart(64, "0");
|
|
2435
|
+
if (!note.leafIndex && note.leafIndex !== 0) {
|
|
2436
|
+
throw new Error("Note must have a leaf index (note must be deposited)");
|
|
2437
|
+
}
|
|
2438
|
+
if (!merkleProof.pathElements || merkleProof.pathElements.length === 0) {
|
|
2439
|
+
throw new Error("Merkle proof is invalid: missing path elements");
|
|
2440
|
+
}
|
|
2441
|
+
if (merkleProof.pathElements.length !== merkleProof.pathIndices.length) {
|
|
2442
|
+
throw new Error("Merkle proof is invalid: path elements and indices length mismatch");
|
|
2443
|
+
}
|
|
2444
|
+
for (let i = 0; i < merkleProof.pathIndices.length; i++) {
|
|
2445
|
+
const idx = merkleProof.pathIndices[i];
|
|
2446
|
+
if (idx !== 0 && idx !== 1) {
|
|
2447
|
+
throw new Error(`Merkle proof path index at position ${i} must be 0 or 1, got ${idx}`);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (!isValidHex(note.r, 32)) {
|
|
2451
|
+
throw new Error("Note r must be 64 hex characters (32 bytes)");
|
|
2452
|
+
}
|
|
2453
|
+
if (!isValidHex(note.sk_spend, 32)) {
|
|
2454
|
+
throw new Error("Note sk_spend must be 64 hex characters (32 bytes)");
|
|
2455
|
+
}
|
|
2456
|
+
if (!isValidHex(merkleRoot, 32)) {
|
|
2457
|
+
throw new Error("Merkle root must be 64 hex characters (32 bytes)");
|
|
2458
|
+
}
|
|
2459
|
+
for (let i = 0; i < merkleProof.pathElements.length; i++) {
|
|
2460
|
+
const element = merkleProof.pathElements[i];
|
|
2461
|
+
if (typeof element !== "string" || !isValidHex(element, 32)) {
|
|
2462
|
+
throw new Error(`Merkle proof path element at position ${i} must be 64 hex characters (32 bytes)`);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
const circuitsPath = process.env.CIRCUITS_PATH || getDefaultCircuitsPath();
|
|
2466
|
+
const useDirectProof = areCircuitsAvailable(circuitsPath);
|
|
2467
|
+
let proofHex;
|
|
2468
|
+
let finalPublicInputs;
|
|
2469
|
+
if (useDirectProof) {
|
|
2470
|
+
const sk_spend_bigint = BigInt("0x" + note.sk_spend);
|
|
2471
|
+
const r_bigint = BigInt("0x" + note.r);
|
|
2472
|
+
const root_bigint = BigInt("0x" + merkleRoot);
|
|
2473
|
+
const nullifier_bigint = BigInt("0x" + nullifierHex);
|
|
2474
|
+
const outputs_hash_bigint = BigInt("0x" + outputsHashHex);
|
|
2475
|
+
const amount_bigint = BigInt(note.amount);
|
|
2476
|
+
const pathElements = merkleProof.pathElements.map((p) => BigInt("0x" + p));
|
|
2477
|
+
const outAddr = [];
|
|
2478
|
+
for (let i = 0; i < 5; i++) {
|
|
2479
|
+
if (i < recipients.length) {
|
|
2480
|
+
const [lo, hi] = pubkeyToLimbs(recipients[i].recipient.toBytes());
|
|
2481
|
+
outAddr.push([lo, hi]);
|
|
2482
|
+
} else {
|
|
2483
|
+
outAddr.push([0n, 0n]);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
const t = amount_bigint * 5n;
|
|
2487
|
+
const varFee = t / 1000n;
|
|
2488
|
+
const rem = t % 1000n;
|
|
2489
|
+
const outAmount = [];
|
|
2490
|
+
const outFlags = [];
|
|
2491
|
+
for (let i = 0; i < 5; i++) {
|
|
2492
|
+
if (i < recipients.length) {
|
|
2493
|
+
outAmount.push(BigInt(recipients[i].amount));
|
|
2494
|
+
outFlags.push(1);
|
|
2495
|
+
} else {
|
|
2496
|
+
outAmount.push(0n);
|
|
2497
|
+
outFlags.push(0);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
const sk = splitTo2Limbs(sk_spend_bigint);
|
|
2501
|
+
const r = splitTo2Limbs(r_bigint);
|
|
2502
|
+
const proofInputs = {
|
|
2503
|
+
root: root_bigint,
|
|
2504
|
+
nullifier: nullifier_bigint,
|
|
2505
|
+
outputs_hash: outputs_hash_bigint,
|
|
2506
|
+
public_amount: amount_bigint,
|
|
2507
|
+
amount: amount_bigint,
|
|
2508
|
+
leaf_index: BigInt(note.leafIndex),
|
|
2509
|
+
sk,
|
|
2510
|
+
r,
|
|
2511
|
+
pathElements,
|
|
2512
|
+
pathIndices: merkleProof.pathIndices,
|
|
2513
|
+
num_outputs: recipients.length,
|
|
2514
|
+
out_addr: outAddr,
|
|
2515
|
+
out_amount: outAmount,
|
|
2516
|
+
out_flags: outFlags,
|
|
2517
|
+
var_fee: varFee,
|
|
2518
|
+
rem
|
|
2519
|
+
};
|
|
2520
|
+
const proofResult = await generateWithdrawRegularProof(proofInputs, circuitsPath);
|
|
2521
|
+
proofHex = Buffer.from(proofResult.proofBytes).toString("hex");
|
|
2522
|
+
finalPublicInputs = {
|
|
2523
|
+
root: merkleRoot,
|
|
2524
|
+
nf: nullifierHex,
|
|
2525
|
+
outputs_hash: outputsHashHex,
|
|
2526
|
+
amount: note.amount
|
|
2527
|
+
};
|
|
2528
|
+
} else {
|
|
2529
|
+
const proofInputs = {
|
|
2530
|
+
privateInputs: {
|
|
2531
|
+
amount: note.amount,
|
|
2532
|
+
r: note.r,
|
|
2533
|
+
sk_spend: note.sk_spend,
|
|
2534
|
+
leaf_index: note.leafIndex,
|
|
2535
|
+
merkle_path: {
|
|
2536
|
+
path_elements: merkleProof.pathElements,
|
|
2537
|
+
path_indices: merkleProof.pathIndices
|
|
2538
|
+
}
|
|
2539
|
+
},
|
|
2540
|
+
publicInputs: {
|
|
2541
|
+
root: merkleRoot,
|
|
2542
|
+
nf: nullifierHex,
|
|
2543
|
+
outputs_hash: outputsHashHex,
|
|
2544
|
+
amount: note.amount
|
|
2545
|
+
},
|
|
2546
|
+
outputs: recipients.map((r) => ({
|
|
2547
|
+
address: r.recipient.toBase58(),
|
|
2548
|
+
amount: r.amount
|
|
2549
|
+
}))
|
|
2550
|
+
};
|
|
2551
|
+
const proofResult = await this.artifactProver.generateProof(proofInputs, {
|
|
2552
|
+
onProgress: options?.onProofProgress,
|
|
2553
|
+
onError: options?.onProgress ? (error) => options.onProgress?.(`Proof generation error: ${error}`) : void 0
|
|
2554
|
+
});
|
|
2555
|
+
if (!proofResult.success || !proofResult.proof) {
|
|
2556
|
+
let errorMessage = proofResult.error || "Proof generation failed";
|
|
2557
|
+
if (errorMessage.startsWith("Proof generation failed: ")) {
|
|
2558
|
+
errorMessage = errorMessage.substring("Proof generation failed: ".length);
|
|
2559
|
+
}
|
|
2560
|
+
errorMessage += `
|
|
2561
|
+
Note details: leafIndex=${note.leafIndex}, root=${merkleRoot.slice(0, 16)}..., nullifier=${nullifierHex.slice(0, 16)}...`;
|
|
2562
|
+
throw new Error(errorMessage);
|
|
2563
|
+
}
|
|
2564
|
+
proofHex = proofResult.proof;
|
|
2565
|
+
finalPublicInputs = proofInputs.publicInputs;
|
|
2566
|
+
}
|
|
2567
|
+
const signature = await this.relay.submitWithdraw(
|
|
2568
|
+
{
|
|
2569
|
+
proof: proofHex,
|
|
2570
|
+
publicInputs: finalPublicInputs,
|
|
2571
|
+
outputs: recipients.map((r) => ({
|
|
2572
|
+
recipient: r.recipient.toBase58(),
|
|
2573
|
+
amount: r.amount
|
|
2574
|
+
})),
|
|
2575
|
+
feeBps
|
|
2576
|
+
// Use calculated protocol fee BPS
|
|
2577
|
+
},
|
|
2578
|
+
options?.onProgress
|
|
2579
|
+
);
|
|
2580
|
+
return {
|
|
2581
|
+
signature,
|
|
2582
|
+
outputs: recipients.map((r) => ({
|
|
2583
|
+
recipient: r.recipient.toBase58(),
|
|
2584
|
+
amount: r.amount
|
|
2585
|
+
})),
|
|
2586
|
+
nullifier: nullifierHex,
|
|
2587
|
+
root: merkleRoot
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Withdraw to a single recipient
|
|
2592
|
+
*
|
|
2593
|
+
* Convenience method for withdrawing to one address.
|
|
2594
|
+
* Handles the complete flow: deposits if needed, then withdraws.
|
|
2595
|
+
*
|
|
2596
|
+
* @param connection - Solana connection
|
|
2597
|
+
* @param payer - Payer wallet
|
|
2598
|
+
* @param note - Note to spend
|
|
2599
|
+
* @param recipient - Recipient address
|
|
2600
|
+
* @param options - Optional configuration
|
|
2601
|
+
* @returns Transfer result
|
|
2602
|
+
*
|
|
2603
|
+
* @example
|
|
2604
|
+
* ```typescript
|
|
2605
|
+
* const note = client.generateNote(1_000_000_000);
|
|
2606
|
+
* const result = await client.withdraw(
|
|
2607
|
+
* connection,
|
|
2608
|
+
* wallet,
|
|
2609
|
+
* note,
|
|
2610
|
+
* new PublicKey("..."),
|
|
2611
|
+
* { withdrawAll: true }
|
|
2612
|
+
* );
|
|
2613
|
+
* ```
|
|
2614
|
+
*/
|
|
2615
|
+
async withdraw(connection, note, recipient, options) {
|
|
2616
|
+
const withdrawAll = options?.withdrawAll ?? true;
|
|
2617
|
+
const amount = withdrawAll ? getDistributableAmount2(note.amount) : options?.amount || note.amount;
|
|
2618
|
+
if (!withdrawAll && !options?.amount) {
|
|
2619
|
+
throw new Error("Must specify amount or set withdrawAll: true");
|
|
2620
|
+
}
|
|
2621
|
+
return this.privateTransfer(
|
|
2622
|
+
connection,
|
|
2623
|
+
note,
|
|
2624
|
+
[{ recipient, amount }],
|
|
2625
|
+
options
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Send SOL privately to multiple recipients
|
|
2630
|
+
*
|
|
2631
|
+
* Convenience method that wraps privateTransfer with a simpler API.
|
|
2632
|
+
* Handles the complete flow: deposits if needed, then sends to recipients.
|
|
2633
|
+
*
|
|
2634
|
+
* @param connection - Solana connection
|
|
2635
|
+
* @param note - Note to spend
|
|
2636
|
+
* @param recipients - Array of 1-5 recipients with amounts
|
|
2637
|
+
* @param options - Optional configuration
|
|
2638
|
+
* @returns Transfer result
|
|
2639
|
+
*
|
|
2640
|
+
* @example
|
|
2641
|
+
* ```typescript
|
|
2642
|
+
* const note = client.generateNote(1_000_000_000);
|
|
2643
|
+
* const result = await client.send(
|
|
2644
|
+
* connection,
|
|
2645
|
+
* note,
|
|
2646
|
+
* [
|
|
2647
|
+
* { recipient: new PublicKey("..."), amount: 500_000_000 },
|
|
2648
|
+
* { recipient: new PublicKey("..."), amount: 492_500_000 }
|
|
2649
|
+
* ]
|
|
2650
|
+
* );
|
|
2651
|
+
* ```
|
|
2652
|
+
*/
|
|
2653
|
+
async send(connection, note, recipients, options) {
|
|
2654
|
+
return this.privateTransfer(connection, note, recipients, options);
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Swap SOL for tokens privately
|
|
2658
|
+
*
|
|
2659
|
+
* Withdraws SOL from a note and swaps it for tokens via the relay service.
|
|
2660
|
+
* Handles the complete flow: deposits if needed, generates proof, and submits swap.
|
|
2661
|
+
*
|
|
2662
|
+
* @param connection - Solana connection
|
|
2663
|
+
* @param note - Note to spend
|
|
2664
|
+
* @param recipient - Recipient address (will receive tokens)
|
|
2665
|
+
* @param options - Swap configuration
|
|
2666
|
+
* @returns Swap result with transaction signature
|
|
2667
|
+
*
|
|
2668
|
+
* @example
|
|
2669
|
+
* ```typescript
|
|
2670
|
+
* const note = client.generateNote(1_000_000_000);
|
|
2671
|
+
* const result = await client.swap(
|
|
2672
|
+
* connection,
|
|
2673
|
+
* note,
|
|
2674
|
+
* new PublicKey("..."), // recipient
|
|
2675
|
+
* {
|
|
2676
|
+
* outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
|
|
2677
|
+
* slippageBps: 100, // 1%
|
|
2678
|
+
* getQuote: async (amount, mint, slippage) => {
|
|
2679
|
+
* // Fetch quote from your swap API
|
|
2680
|
+
* const quote = await fetchSwapQuote(amount, mint, slippage);
|
|
2681
|
+
* return {
|
|
2682
|
+
* outAmount: quote.outAmount,
|
|
2683
|
+
* minOutputAmount: quote.minOutputAmount
|
|
2684
|
+
* };
|
|
2685
|
+
* }
|
|
2686
|
+
* }
|
|
2687
|
+
* );
|
|
2688
|
+
* ```
|
|
2689
|
+
*/
|
|
2690
|
+
async swap(connection, note, recipient, options) {
|
|
2691
|
+
try {
|
|
2692
|
+
if (!isWithdrawable(note)) {
|
|
2693
|
+
const depositResult = await this.deposit(connection, note, {
|
|
2694
|
+
skipPreflight: false
|
|
2695
|
+
});
|
|
2696
|
+
note = depositResult.note;
|
|
2697
|
+
}
|
|
2698
|
+
const variableFee = Math.floor(note.amount * 5 / 1e3);
|
|
2699
|
+
const feeBps = note.amount === 0 ? 0 : Math.min(Math.floor((variableFee * 1e4 + note.amount - 1) / note.amount), 65535);
|
|
2700
|
+
const withdrawAmountLamports = getDistributableAmount2(note.amount);
|
|
2701
|
+
if (withdrawAmountLamports <= 0) {
|
|
2702
|
+
throw new Error("Amount too small after fees");
|
|
2703
|
+
}
|
|
2704
|
+
let minOutputAmount;
|
|
2705
|
+
if (options.minOutputAmount !== void 0) {
|
|
2706
|
+
minOutputAmount = options.minOutputAmount;
|
|
2707
|
+
} else if (options.getQuote) {
|
|
2708
|
+
const quote = await options.getQuote(
|
|
2709
|
+
withdrawAmountLamports,
|
|
2710
|
+
options.outputMint,
|
|
2711
|
+
options.slippageBps || 100
|
|
2712
|
+
);
|
|
2713
|
+
minOutputAmount = quote.minOutputAmount;
|
|
2714
|
+
} else {
|
|
2715
|
+
throw new Error(
|
|
2716
|
+
"Must provide either minOutputAmount or getQuote function"
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
let recipientAta;
|
|
2720
|
+
try {
|
|
2721
|
+
const splTokenModule = await import("@solana/spl-token");
|
|
2722
|
+
const getAssociatedTokenAddress = splTokenModule.getAssociatedTokenAddress;
|
|
2723
|
+
if (!getAssociatedTokenAddress) {
|
|
2724
|
+
throw new Error("getAssociatedTokenAddress not found");
|
|
2725
|
+
}
|
|
2726
|
+
const outputMint2 = new import_web36.PublicKey(options.outputMint);
|
|
2727
|
+
recipientAta = await getAssociatedTokenAddress(outputMint2, recipient);
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
`Failed to get associated token account: ${error instanceof Error ? error.message : String(error)}. Please install @solana/spl-token or provide recipientAta in options.`
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
let merkleProof;
|
|
2734
|
+
let merkleRoot;
|
|
2735
|
+
if (note.merkleProof && note.root) {
|
|
2736
|
+
merkleProof = {
|
|
2737
|
+
pathElements: note.merkleProof.pathElements,
|
|
2738
|
+
pathIndices: note.merkleProof.pathIndices
|
|
2739
|
+
};
|
|
2740
|
+
merkleRoot = note.root;
|
|
2741
|
+
} else {
|
|
2742
|
+
merkleProof = await this.indexer.getMerkleProof(note.leafIndex);
|
|
2743
|
+
merkleRoot = merkleProof.root || (await this.indexer.getMerkleRoot()).root;
|
|
2744
|
+
}
|
|
2745
|
+
const nullifier = await computeNullifierAsync(note.sk_spend, note.leafIndex);
|
|
2746
|
+
const nullifierHex = nullifier.toString(16).padStart(64, "0");
|
|
2747
|
+
const inputMint = new import_web36.PublicKey("11111111111111111111111111111111");
|
|
2748
|
+
const outputMint = new import_web36.PublicKey(options.outputMint);
|
|
2749
|
+
const outputsHash = await computeSwapOutputsHashAsync(
|
|
2750
|
+
inputMint,
|
|
2751
|
+
outputMint,
|
|
2752
|
+
recipientAta,
|
|
2753
|
+
minOutputAmount,
|
|
2754
|
+
note.amount
|
|
2755
|
+
);
|
|
2756
|
+
const outputsHashHex = outputsHash.toString(16).padStart(64, "0");
|
|
2757
|
+
if (!note.leafIndex && note.leafIndex !== 0) {
|
|
2758
|
+
throw new Error("Note must have a leaf index (note must be deposited)");
|
|
2759
|
+
}
|
|
2760
|
+
if (!merkleProof.pathElements || merkleProof.pathElements.length === 0) {
|
|
2761
|
+
throw new Error("Merkle proof is invalid: missing path elements");
|
|
2762
|
+
}
|
|
2763
|
+
const circuitsPath = process.env.CIRCUITS_PATH || getDefaultCircuitsPath();
|
|
2764
|
+
const useDirectProof = areCircuitsAvailable(circuitsPath);
|
|
2765
|
+
let proofHex;
|
|
2766
|
+
let finalPublicInputs;
|
|
2767
|
+
if (useDirectProof) {
|
|
2768
|
+
const sk_spend_bigint = BigInt("0x" + note.sk_spend);
|
|
2769
|
+
const r_bigint = BigInt("0x" + note.r);
|
|
2770
|
+
const root_bigint = BigInt("0x" + merkleRoot);
|
|
2771
|
+
const nullifier_bigint = BigInt("0x" + nullifierHex);
|
|
2772
|
+
const outputs_hash_bigint = BigInt("0x" + outputsHashHex);
|
|
2773
|
+
const amount_bigint = BigInt(note.amount);
|
|
2774
|
+
const pathElements = merkleProof.pathElements.map((p) => BigInt("0x" + p));
|
|
2775
|
+
const inputMintLimbs = pubkeyToLimbs(inputMint.toBytes());
|
|
2776
|
+
const outputMintLimbs = pubkeyToLimbs(outputMint.toBytes());
|
|
2777
|
+
const recipientAtaLimbs = pubkeyToLimbs(recipientAta.toBytes());
|
|
2778
|
+
const t = amount_bigint * 5n;
|
|
2779
|
+
const varFee = t / 1000n;
|
|
2780
|
+
const rem = t % 1000n;
|
|
2781
|
+
const proofInputs = {
|
|
2782
|
+
sk_spend: sk_spend_bigint,
|
|
2783
|
+
r: r_bigint,
|
|
2784
|
+
amount: amount_bigint,
|
|
2785
|
+
leaf_index: BigInt(note.leafIndex),
|
|
2786
|
+
path_elements: pathElements,
|
|
2787
|
+
path_indices: merkleProof.pathIndices,
|
|
2788
|
+
root: root_bigint,
|
|
2789
|
+
nullifier: nullifier_bigint,
|
|
2790
|
+
outputs_hash: outputs_hash_bigint,
|
|
2791
|
+
public_amount: amount_bigint,
|
|
2792
|
+
input_mint: inputMintLimbs,
|
|
2793
|
+
output_mint: outputMintLimbs,
|
|
2794
|
+
recipient_ata: recipientAtaLimbs,
|
|
2795
|
+
min_output_amount: BigInt(minOutputAmount),
|
|
2796
|
+
var_fee: varFee,
|
|
2797
|
+
rem
|
|
2798
|
+
};
|
|
2799
|
+
const proofResult = await generateWithdrawSwapProof(proofInputs, circuitsPath);
|
|
2800
|
+
proofHex = Buffer.from(proofResult.proofBytes).toString("hex");
|
|
2801
|
+
finalPublicInputs = {
|
|
2802
|
+
root: merkleRoot,
|
|
2803
|
+
nf: nullifierHex,
|
|
2804
|
+
outputs_hash: outputsHashHex,
|
|
2805
|
+
amount: note.amount
|
|
2806
|
+
};
|
|
2807
|
+
} else {
|
|
2808
|
+
const proofInputs = {
|
|
2809
|
+
privateInputs: {
|
|
2810
|
+
amount: note.amount,
|
|
2811
|
+
r: note.r,
|
|
2812
|
+
sk_spend: note.sk_spend,
|
|
2813
|
+
leaf_index: note.leafIndex,
|
|
2814
|
+
merkle_path: {
|
|
2815
|
+
path_elements: merkleProof.pathElements,
|
|
2816
|
+
path_indices: merkleProof.pathIndices
|
|
2817
|
+
}
|
|
2818
|
+
},
|
|
2819
|
+
publicInputs: {
|
|
2820
|
+
root: merkleRoot,
|
|
2821
|
+
nf: nullifierHex,
|
|
2822
|
+
outputs_hash: outputsHashHex,
|
|
2823
|
+
amount: note.amount
|
|
2824
|
+
},
|
|
2825
|
+
outputs: [],
|
|
2826
|
+
// Empty for swaps
|
|
2827
|
+
swapParams: {
|
|
2828
|
+
output_mint: options.outputMint,
|
|
2829
|
+
recipient_ata: recipientAta.toBase58(),
|
|
2830
|
+
min_output_amount: minOutputAmount
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
const proofResult = await this.artifactProver.generateProof(proofInputs, {
|
|
2834
|
+
onProgress: options.onProofProgress,
|
|
2835
|
+
onError: options.onProgress ? (error) => options.onProgress?.(`Proof generation error: ${error}`) : void 0
|
|
2836
|
+
});
|
|
2837
|
+
if (!proofResult.success || !proofResult.proof) {
|
|
2838
|
+
let errorMessage = proofResult.error || "Proof generation failed";
|
|
2839
|
+
if (errorMessage.startsWith("Proof generation failed: ")) {
|
|
2840
|
+
errorMessage = errorMessage.substring("Proof generation failed: ".length);
|
|
2841
|
+
}
|
|
2842
|
+
errorMessage += `
|
|
2843
|
+
Note details: leafIndex=${note.leafIndex}, root=${merkleRoot.slice(0, 16)}..., nullifier=${nullifierHex.slice(0, 16)}...`;
|
|
2844
|
+
throw new Error(errorMessage);
|
|
2845
|
+
}
|
|
2846
|
+
proofHex = proofResult.proof;
|
|
2847
|
+
finalPublicInputs = proofInputs.publicInputs;
|
|
2848
|
+
}
|
|
2849
|
+
const signature = await this.relay.submitSwap(
|
|
2850
|
+
{
|
|
2851
|
+
proof: proofHex,
|
|
2852
|
+
publicInputs: finalPublicInputs,
|
|
2853
|
+
outputs: [
|
|
2854
|
+
{
|
|
2855
|
+
recipient: recipient.toBase58(),
|
|
2856
|
+
amount: withdrawAmountLamports
|
|
2857
|
+
}
|
|
2858
|
+
],
|
|
2859
|
+
feeBps,
|
|
2860
|
+
swap: {
|
|
2861
|
+
output_mint: options.outputMint,
|
|
2862
|
+
slippage_bps: options.slippageBps || 100,
|
|
2863
|
+
min_output_amount: minOutputAmount
|
|
2864
|
+
}
|
|
2865
|
+
},
|
|
2866
|
+
options.onProgress
|
|
2867
|
+
);
|
|
2868
|
+
return {
|
|
2869
|
+
signature,
|
|
2870
|
+
outputs: [
|
|
2871
|
+
{
|
|
2872
|
+
recipient: recipient.toBase58(),
|
|
2873
|
+
amount: withdrawAmountLamports
|
|
2874
|
+
}
|
|
2875
|
+
],
|
|
2876
|
+
nullifier: nullifierHex,
|
|
2877
|
+
root: merkleRoot,
|
|
2878
|
+
outputMint: options.outputMint,
|
|
2879
|
+
minOutputAmount
|
|
2880
|
+
};
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
throw this.wrapError(error, "Swap failed");
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Generate a new note without depositing
|
|
2887
|
+
*
|
|
2888
|
+
* @param amountLamports - Amount for the note
|
|
2889
|
+
* @param useWalletKeys - Whether to use wallet keys (v2.0 recommended)
|
|
2890
|
+
* @returns New note (not yet deposited)
|
|
2891
|
+
*/
|
|
2892
|
+
async generateNote(amountLamports, useWalletKeys = false) {
|
|
2893
|
+
if (useWalletKeys && this.cloakKeys) {
|
|
2894
|
+
return await generateNoteFromWallet(amountLamports, this.cloakKeys, this.config.network);
|
|
2895
|
+
} else if (useWalletKeys) {
|
|
2896
|
+
const keys = generateCloakKeys();
|
|
2897
|
+
this.cloakKeys = keys;
|
|
2898
|
+
const result = this.storage.saveKeys(keys);
|
|
2899
|
+
if (result instanceof Promise) {
|
|
2900
|
+
result.catch(() => {
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
return await generateNoteFromWallet(amountLamports, keys, this.config.network);
|
|
2904
|
+
}
|
|
2905
|
+
return await generateNote(amountLamports, this.config.network);
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Parse a note from JSON string
|
|
2909
|
+
*
|
|
2910
|
+
* @param jsonString - JSON representation
|
|
2911
|
+
* @returns Parsed note
|
|
2912
|
+
*/
|
|
2913
|
+
parseNote(jsonString) {
|
|
2914
|
+
return parseNote2(jsonString);
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Export a note to JSON string
|
|
2918
|
+
*
|
|
2919
|
+
* @param note - Note to export
|
|
2920
|
+
* @param pretty - Format with indentation
|
|
2921
|
+
* @returns JSON string
|
|
2922
|
+
*/
|
|
2923
|
+
exportNote(note, pretty = false) {
|
|
2924
|
+
return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Check if a note is ready for withdrawal
|
|
2928
|
+
*
|
|
2929
|
+
* @param note - Note to check
|
|
2930
|
+
* @returns True if withdrawable
|
|
2931
|
+
*/
|
|
2932
|
+
isWithdrawable(note) {
|
|
2933
|
+
return isWithdrawable(note);
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Get Merkle proof for a leaf index
|
|
2937
|
+
*
|
|
2938
|
+
* @param leafIndex - Leaf index in tree
|
|
2939
|
+
* @returns Merkle proof
|
|
2940
|
+
*/
|
|
2941
|
+
async getMerkleProof(leafIndex) {
|
|
2942
|
+
return this.indexer.getMerkleProof(leafIndex);
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Get current Merkle root
|
|
2946
|
+
*
|
|
2947
|
+
* @returns Current root hash
|
|
2948
|
+
*/
|
|
2949
|
+
async getCurrentRoot() {
|
|
2950
|
+
const response = await this.indexer.getMerkleRoot();
|
|
2951
|
+
return response.root;
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Get transaction status from relay service
|
|
2955
|
+
*
|
|
2956
|
+
* @param requestId - Request ID from previous submission
|
|
2957
|
+
* @returns Current status
|
|
2958
|
+
*/
|
|
2959
|
+
async getTransactionStatus(requestId) {
|
|
2960
|
+
return this.relay.getStatus(requestId);
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Recover a deposit that completed on-chain but failed to register
|
|
2964
|
+
*
|
|
2965
|
+
* Use this when a deposit transaction succeeded but the browser crashed
|
|
2966
|
+
* or lost connection before the indexer registration completed.
|
|
2967
|
+
*
|
|
2968
|
+
* @param signature - Transaction signature
|
|
2969
|
+
* @param commitment - Note commitment hash
|
|
2970
|
+
* @param note - Optional: The full note if available
|
|
2971
|
+
* @returns Recovery result with updated note
|
|
2972
|
+
*
|
|
2973
|
+
* @example
|
|
2974
|
+
* ```typescript
|
|
2975
|
+
* const result = await sdk.recoverDeposit({
|
|
2976
|
+
* signature: "5Kn4...",
|
|
2977
|
+
* commitment: "abc123...",
|
|
2978
|
+
* note: myNote // optional if you have it
|
|
2979
|
+
* });
|
|
2980
|
+
*
|
|
2981
|
+
* if (result.success) {
|
|
2982
|
+
* console.log(`Recovered! Leaf index: ${result.leafIndex}`);
|
|
2983
|
+
* }
|
|
2984
|
+
* ```
|
|
2985
|
+
*/
|
|
2986
|
+
async recoverDeposit(options) {
|
|
2987
|
+
return this.depositRecovery.recoverDeposit(options);
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Load all notes from storage
|
|
2991
|
+
*
|
|
2992
|
+
* @returns Array of saved notes
|
|
2993
|
+
*/
|
|
2994
|
+
async loadNotes() {
|
|
2995
|
+
const notes = this.storage.loadAllNotes();
|
|
2996
|
+
return Array.isArray(notes) ? notes : await notes;
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Save a note to storage
|
|
3000
|
+
*
|
|
3001
|
+
* @param note - Note to save
|
|
3002
|
+
*/
|
|
3003
|
+
async saveNote(note) {
|
|
3004
|
+
const result = this.storage.saveNote(note);
|
|
3005
|
+
if (result instanceof Promise) {
|
|
3006
|
+
await result;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Find a note by its commitment
|
|
3011
|
+
*
|
|
3012
|
+
* @param commitment - Commitment hash
|
|
3013
|
+
* @returns Note if found
|
|
3014
|
+
*/
|
|
3015
|
+
async findNote(commitment) {
|
|
3016
|
+
const notes = await this.loadNotes();
|
|
3017
|
+
return findNoteByCommitment(notes, commitment);
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Import wallet keys from JSON
|
|
3021
|
+
*
|
|
3022
|
+
* @param keysJson - JSON string containing keys
|
|
3023
|
+
*/
|
|
3024
|
+
async importWalletKeys(keysJson) {
|
|
3025
|
+
const keys = importWalletKeys(keysJson);
|
|
3026
|
+
this.cloakKeys = keys;
|
|
3027
|
+
const result = this.storage.saveKeys(keys);
|
|
3028
|
+
if (result instanceof Promise) {
|
|
3029
|
+
await result;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Export wallet keys to JSON
|
|
3034
|
+
*
|
|
3035
|
+
* WARNING: This exports secret keys! Store securely.
|
|
3036
|
+
*
|
|
3037
|
+
* @returns JSON string with keys
|
|
3038
|
+
*/
|
|
3039
|
+
exportWalletKeys() {
|
|
3040
|
+
if (!this.cloakKeys) {
|
|
3041
|
+
throw new CloakError("No wallet keys available", "wallet", false);
|
|
3042
|
+
}
|
|
3043
|
+
return exportWalletKeys(this.cloakKeys);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Get the configuration
|
|
3047
|
+
*/
|
|
3048
|
+
getConfig() {
|
|
3049
|
+
return { ...this.config };
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Scan blockchain for notes belonging to this wallet (v2.0 feature)
|
|
3053
|
+
*
|
|
3054
|
+
* Requires Cloak keys to be configured in the SDK.
|
|
3055
|
+
* Fetches encrypted outputs from the indexer and decrypts notes
|
|
3056
|
+
* that belong to this wallet.
|
|
3057
|
+
*
|
|
3058
|
+
* @param options - Scanning options
|
|
3059
|
+
* @returns Array of discovered notes with metadata
|
|
3060
|
+
*
|
|
3061
|
+
* @example
|
|
3062
|
+
* ```typescript
|
|
3063
|
+
* const notes = await sdk.scanNotes({
|
|
3064
|
+
* onProgress: (current, total) => {
|
|
3065
|
+
* console.log(`Scanning: ${current}/${total}`);
|
|
3066
|
+
* }
|
|
3067
|
+
* });
|
|
3068
|
+
*
|
|
3069
|
+
* console.log(`Found ${notes.length} notes!`);
|
|
3070
|
+
* const totalBalance = notes.reduce((sum, n) => sum + n.amount, 0);
|
|
3071
|
+
* ```
|
|
3072
|
+
*/
|
|
3073
|
+
async scanNotes(options) {
|
|
3074
|
+
if (!this.cloakKeys) {
|
|
3075
|
+
throw new CloakError(
|
|
3076
|
+
"Note scanning requires Cloak keys. Initialize SDK with: cloakKeys: generateCloakKeys()",
|
|
3077
|
+
"validation",
|
|
3078
|
+
false
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
const startIndex = options?.startIndex ?? 0;
|
|
3082
|
+
const batchSize = options?.batchSize ?? 100;
|
|
3083
|
+
const { next_index: totalNotes } = await this.indexer.getMerkleRoot();
|
|
3084
|
+
const endIndex = options?.endIndex ?? (totalNotes > 0 ? totalNotes - 1 : 0);
|
|
3085
|
+
if (totalNotes === 0 || endIndex < startIndex) {
|
|
3086
|
+
return [];
|
|
3087
|
+
}
|
|
3088
|
+
const allEncryptedOutputs = [];
|
|
3089
|
+
for (let start = startIndex; start <= endIndex; start += batchSize) {
|
|
3090
|
+
const end = Math.min(start + batchSize - 1, endIndex);
|
|
3091
|
+
options?.onProgress?.(start, totalNotes);
|
|
3092
|
+
const { notes } = await this.indexer.getNotesRange(start, end, batchSize);
|
|
3093
|
+
allEncryptedOutputs.push(...notes);
|
|
3094
|
+
}
|
|
3095
|
+
options?.onProgress?.(totalNotes, totalNotes);
|
|
3096
|
+
const foundNoteData = scanNotesForWallet(
|
|
3097
|
+
allEncryptedOutputs,
|
|
3098
|
+
this.cloakKeys.view
|
|
3099
|
+
);
|
|
3100
|
+
const scannedNotes = foundNoteData.map((noteData) => ({
|
|
3101
|
+
version: "2.0",
|
|
3102
|
+
amount: noteData.amount,
|
|
3103
|
+
commitment: noteData.commitment,
|
|
3104
|
+
sk_spend: noteData.sk_spend,
|
|
3105
|
+
r: noteData.r,
|
|
3106
|
+
timestamp: Date.now(),
|
|
3107
|
+
network: this.config.network || "devnet",
|
|
3108
|
+
scannedAt: Date.now()
|
|
3109
|
+
}));
|
|
3110
|
+
return scannedNotes;
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Wrap errors with better categorization and user-friendly messages
|
|
3114
|
+
*
|
|
3115
|
+
* @private
|
|
3116
|
+
*/
|
|
3117
|
+
wrapError(error, context) {
|
|
3118
|
+
if (error instanceof CloakError) {
|
|
3119
|
+
return error;
|
|
3120
|
+
}
|
|
3121
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3122
|
+
if (errorMessage.includes("duplicate key") || errorMessage.includes("already deposited")) {
|
|
3123
|
+
return new CloakError(
|
|
3124
|
+
"This note was already deposited. The transaction succeeded but the indexer has it recorded. Generate a new note or scan for existing notes.",
|
|
3125
|
+
"indexer",
|
|
3126
|
+
false,
|
|
3127
|
+
error instanceof Error ? error : void 0
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
if (errorMessage.includes("insufficient funds") || errorMessage.includes("insufficient lamports")) {
|
|
3131
|
+
return new CloakError(
|
|
3132
|
+
"Insufficient funds for this transaction. Please check your wallet balance.",
|
|
3133
|
+
"wallet",
|
|
3134
|
+
false,
|
|
3135
|
+
error instanceof Error ? error : void 0
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
if (errorMessage.includes("Merkle tree") && errorMessage.includes("inconsistent")) {
|
|
3139
|
+
return new CloakError(
|
|
3140
|
+
"Indexer is temporarily unavailable. Please try again in a moment.",
|
|
3141
|
+
"indexer",
|
|
3142
|
+
true,
|
|
3143
|
+
error instanceof Error ? error : void 0
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
if (errorMessage.includes("timeout") || errorMessage.includes("timed out")) {
|
|
3147
|
+
return new CloakError(
|
|
3148
|
+
"Network timeout. Please check your connection and try again.",
|
|
3149
|
+
"network",
|
|
3150
|
+
true,
|
|
3151
|
+
error instanceof Error ? error : void 0
|
|
3152
|
+
);
|
|
3153
|
+
}
|
|
3154
|
+
if (errorMessage.includes("not connected") || errorMessage.includes("wallet")) {
|
|
3155
|
+
return new CloakError(
|
|
3156
|
+
"Wallet not connected. Please connect your wallet first.",
|
|
3157
|
+
"wallet",
|
|
3158
|
+
false,
|
|
3159
|
+
error instanceof Error ? error : void 0
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
if (errorMessage.includes("proof") && (errorMessage.includes("failed") || errorMessage.includes("error"))) {
|
|
3163
|
+
return new CloakError(
|
|
3164
|
+
"Zero-knowledge proof generation failed. This is usually temporary - please try again.",
|
|
3165
|
+
"prover",
|
|
3166
|
+
true,
|
|
3167
|
+
error instanceof Error ? error : void 0
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
if (errorMessage.includes("relay") || errorMessage.includes("withdraw")) {
|
|
3171
|
+
return new CloakError(
|
|
3172
|
+
"Relay service error. Please try again later.",
|
|
3173
|
+
"relay",
|
|
3174
|
+
true,
|
|
3175
|
+
error instanceof Error ? error : void 0
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
return new CloakError(
|
|
3179
|
+
`${context}: ${errorMessage}`,
|
|
3180
|
+
"network",
|
|
3181
|
+
false,
|
|
3182
|
+
error instanceof Error ? error : void 0
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// src/core/note.ts
|
|
3188
|
+
function serializeNote(note, pretty = false) {
|
|
3189
|
+
validateNote(note);
|
|
3190
|
+
return pretty ? JSON.stringify(note, null, 2) : JSON.stringify(note);
|
|
3191
|
+
}
|
|
3192
|
+
function downloadNote(note, filename) {
|
|
3193
|
+
const g = globalThis;
|
|
3194
|
+
const doc = g?.document;
|
|
3195
|
+
const URL_ = g?.URL;
|
|
3196
|
+
const Blob_ = g?.Blob;
|
|
3197
|
+
if (!doc || !URL_ || !Blob_) {
|
|
3198
|
+
throw new Error("downloadNote is only available in browser environments");
|
|
3199
|
+
}
|
|
3200
|
+
const json = serializeNote(note, true);
|
|
3201
|
+
const blob = new Blob_([json], { type: "application/json" });
|
|
3202
|
+
const url = URL_.createObjectURL(blob);
|
|
3203
|
+
const defaultFilename = `cloak-note-${note.commitment.slice(0, 8)}.json`;
|
|
3204
|
+
const link = doc.createElement("a");
|
|
3205
|
+
link.href = url;
|
|
3206
|
+
link.download = filename || defaultFilename;
|
|
3207
|
+
link.click();
|
|
3208
|
+
URL_.revokeObjectURL(url);
|
|
3209
|
+
}
|
|
3210
|
+
async function copyNoteToClipboard(note) {
|
|
3211
|
+
const g = globalThis;
|
|
3212
|
+
const nav = g?.navigator;
|
|
3213
|
+
if (!nav || !nav.clipboard) {
|
|
3214
|
+
throw new Error("Clipboard API not available");
|
|
3215
|
+
}
|
|
3216
|
+
const json = serializeNote(note, true);
|
|
3217
|
+
await nav.clipboard.writeText(json);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// src/utils/errors.ts
|
|
3221
|
+
var PROGRAM_ERRORS = {
|
|
3222
|
+
// Nullifier errors
|
|
3223
|
+
"NullifierAlreadyUsed": "This note has already been withdrawn. Each note can only be spent once.",
|
|
3224
|
+
"0x1770": "This note has already been withdrawn.",
|
|
3225
|
+
// Proof verification errors
|
|
3226
|
+
"ProofVerificationFailed": "Zero-knowledge proof verification failed. Please try again.",
|
|
3227
|
+
"0x1771": "Invalid proof. Please regenerate and try again.",
|
|
3228
|
+
"InvalidProof": "The provided proof is invalid.",
|
|
3229
|
+
// Root errors
|
|
3230
|
+
"RootNotFound": "The Merkle root is outdated or invalid. Please refresh and try again.",
|
|
3231
|
+
"InvalidRoot": "Invalid Merkle root. The tree may have been updated.",
|
|
3232
|
+
"0x1772": "Merkle root not found in history.",
|
|
3233
|
+
// Amount errors
|
|
3234
|
+
"InvalidAmount": "Invalid amount. Please check your input.",
|
|
3235
|
+
"InsufficientFunds": "Insufficient funds for this transaction.",
|
|
3236
|
+
"AmountMismatch": "Output amounts don't match the note amount.",
|
|
3237
|
+
// Fee errors
|
|
3238
|
+
"InvalidFee": "Invalid fee calculation. Please try again.",
|
|
3239
|
+
"FeeTooHigh": "Fee exceeds maximum allowed.",
|
|
3240
|
+
// Output errors
|
|
3241
|
+
"InvalidOutputs": "Invalid recipient configuration.",
|
|
3242
|
+
"TooManyOutputs": "Maximum 5 recipients allowed per transaction.",
|
|
3243
|
+
"OutputHashMismatch": "Output hash doesn't match proof.",
|
|
3244
|
+
// General errors
|
|
3245
|
+
"Unauthorized": "You don't have permission to perform this action.",
|
|
3246
|
+
"AccountNotFound": "Required account not found.",
|
|
3247
|
+
"InvalidInstruction": "Invalid instruction data."
|
|
3248
|
+
};
|
|
3249
|
+
function parseTransactionError(error) {
|
|
3250
|
+
if (!error) return "An unknown error occurred";
|
|
3251
|
+
const errorStr = typeof error === "string" ? error : error.message || error.toString();
|
|
3252
|
+
const hexMatch = errorStr.match(/0x[0-9a-f]{4}/i);
|
|
3253
|
+
if (hexMatch) {
|
|
3254
|
+
const errorCode = hexMatch[0];
|
|
3255
|
+
if (PROGRAM_ERRORS[errorCode]) {
|
|
3256
|
+
return PROGRAM_ERRORS[errorCode];
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
for (const [key, message] of Object.entries(PROGRAM_ERRORS)) {
|
|
3260
|
+
if (errorStr.includes(key)) {
|
|
3261
|
+
return message;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
if (errorStr.includes("insufficient funds") || errorStr.includes("insufficient lamports")) {
|
|
3265
|
+
return "Insufficient SOL balance. Please add funds to your wallet.";
|
|
3266
|
+
}
|
|
3267
|
+
if (errorStr.includes("blockhash not found")) {
|
|
3268
|
+
return "Transaction expired. Please try again.";
|
|
3269
|
+
}
|
|
3270
|
+
if (errorStr.includes("already been processed")) {
|
|
3271
|
+
return "This transaction has already been processed.";
|
|
3272
|
+
}
|
|
3273
|
+
if (errorStr.includes("signature verification failed")) {
|
|
3274
|
+
return "Transaction signature verification failed. Please try again.";
|
|
3275
|
+
}
|
|
3276
|
+
if (errorStr.includes("account does not exist")) {
|
|
3277
|
+
return "Required account not found. The program may need to be initialized.";
|
|
3278
|
+
}
|
|
3279
|
+
if (errorStr.includes("fetch") || errorStr.includes("network")) {
|
|
3280
|
+
return "Network error. Please check your connection and try again.";
|
|
3281
|
+
}
|
|
3282
|
+
if (errorStr.includes("timeout")) {
|
|
3283
|
+
return "Request timed out. Please try again.";
|
|
3284
|
+
}
|
|
3285
|
+
if (errorStr.includes("relay") || errorStr.includes("withdraw")) {
|
|
3286
|
+
if (errorStr.includes("in progress")) {
|
|
3287
|
+
return "A withdrawal is already in progress. Please wait for it to complete.";
|
|
3288
|
+
}
|
|
3289
|
+
if (errorStr.includes("rate limit")) {
|
|
3290
|
+
return "Too many requests. Please wait a moment and try again.";
|
|
3291
|
+
}
|
|
3292
|
+
return "Relay service error. Please try again later.";
|
|
3293
|
+
}
|
|
3294
|
+
if (errorStr.includes("proof") && errorStr.includes("generation")) {
|
|
3295
|
+
return "Failed to generate zero-knowledge proof. Please try again.";
|
|
3296
|
+
}
|
|
3297
|
+
if (errorStr.includes("indexer") || errorStr.includes("merkle")) {
|
|
3298
|
+
if (errorStr.includes("inconsistent")) {
|
|
3299
|
+
return "The indexer is temporarily unavailable. Please try again in a moment.";
|
|
3300
|
+
}
|
|
3301
|
+
if (errorStr.includes("not found")) {
|
|
3302
|
+
return "Note not found in the indexer. It may not be confirmed yet.";
|
|
3303
|
+
}
|
|
3304
|
+
return "Indexer service error. Please try again later.";
|
|
3305
|
+
}
|
|
3306
|
+
let cleanError = errorStr.replace(/Error:\s*/gi, "").replace(/\s+at\s+.*$/g, "").replace(/\[.*?\]/g, "").trim();
|
|
3307
|
+
if (cleanError.length > 200) {
|
|
3308
|
+
cleanError = cleanError.substring(0, 197) + "...";
|
|
3309
|
+
}
|
|
3310
|
+
return cleanError || "Transaction failed. Please try again.";
|
|
3311
|
+
}
|
|
3312
|
+
function createCloakError(error, _context) {
|
|
3313
|
+
if (error instanceof CloakError) {
|
|
3314
|
+
return error;
|
|
3315
|
+
}
|
|
3316
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3317
|
+
const userMessage = parseTransactionError(error);
|
|
3318
|
+
let category = "network";
|
|
3319
|
+
let retryable = false;
|
|
3320
|
+
if (errorMessage.includes("insufficient") || errorMessage.includes("balance")) {
|
|
3321
|
+
category = "wallet";
|
|
3322
|
+
retryable = false;
|
|
3323
|
+
} else if (errorMessage.includes("proof")) {
|
|
3324
|
+
category = "prover";
|
|
3325
|
+
retryable = true;
|
|
3326
|
+
} else if (errorMessage.includes("indexer") || errorMessage.includes("merkle")) {
|
|
3327
|
+
category = "indexer";
|
|
3328
|
+
retryable = errorMessage.includes("inconsistent") || errorMessage.includes("temporary");
|
|
3329
|
+
} else if (errorMessage.includes("relay")) {
|
|
3330
|
+
category = "relay";
|
|
3331
|
+
retryable = true;
|
|
3332
|
+
} else if (errorMessage.includes("timeout") || errorMessage.includes("network")) {
|
|
3333
|
+
category = "network";
|
|
3334
|
+
retryable = true;
|
|
3335
|
+
} else if (errorMessage.includes("validation") || errorMessage.includes("invalid")) {
|
|
3336
|
+
category = "validation";
|
|
3337
|
+
retryable = false;
|
|
3338
|
+
}
|
|
3339
|
+
return new CloakError(
|
|
3340
|
+
userMessage,
|
|
3341
|
+
category,
|
|
3342
|
+
retryable,
|
|
3343
|
+
error instanceof Error ? error : void 0
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
function formatErrorForLogging(error) {
|
|
3347
|
+
if (error instanceof CloakError) {
|
|
3348
|
+
return `[${error.category}] ${error.message}${error.retryable ? " (retryable)" : ""}`;
|
|
3349
|
+
}
|
|
3350
|
+
if (error instanceof Error) {
|
|
3351
|
+
return `${error.name}: ${error.message}`;
|
|
3352
|
+
}
|
|
3353
|
+
return String(error);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
// src/services/ProverService.ts
|
|
3357
|
+
var ProverService = class {
|
|
3358
|
+
/**
|
|
3359
|
+
* Create a new Prover Service client
|
|
3360
|
+
*
|
|
3361
|
+
* @param indexerUrl - Indexer/Prover service base URL
|
|
3362
|
+
* @param timeout - Proof generation timeout in ms (default: 5 minutes)
|
|
3363
|
+
*/
|
|
3364
|
+
constructor(indexerUrl, timeout = 5 * 60 * 1e3) {
|
|
3365
|
+
this.indexerUrl = indexerUrl.replace(/\/$/, "");
|
|
3366
|
+
this.timeout = timeout;
|
|
3367
|
+
}
|
|
3368
|
+
/**
|
|
3369
|
+
* Generate a zero-knowledge proof for withdrawal
|
|
3370
|
+
*
|
|
3371
|
+
* This process typically takes 30-180 seconds depending on the backend.
|
|
3372
|
+
*
|
|
3373
|
+
* @param inputs - Circuit inputs (private + public + outputs)
|
|
3374
|
+
* @param options - Optional progress tracking and callbacks
|
|
3375
|
+
* @returns Proof result with hex-encoded proof and public inputs
|
|
3376
|
+
*
|
|
3377
|
+
* @example
|
|
3378
|
+
* ```typescript
|
|
3379
|
+
* const result = await prover.generateProof(inputs);
|
|
3380
|
+
* if (result.success) {
|
|
3381
|
+
* console.log(`Proof: ${result.proof}`);
|
|
3382
|
+
* }
|
|
3383
|
+
* ```
|
|
3384
|
+
*
|
|
3385
|
+
* @example
|
|
3386
|
+
* ```typescript
|
|
3387
|
+
* // With progress tracking
|
|
3388
|
+
* const result = await prover.generateProof(inputs, {
|
|
3389
|
+
* onProgress: (progress) => console.log(`Progress: ${progress}%`),
|
|
3390
|
+
* onStart: () => console.log("Starting proof generation..."),
|
|
3391
|
+
* onSuccess: (result) => console.log("Proof generated!"),
|
|
3392
|
+
* onError: (error) => console.error("Failed:", error)
|
|
3393
|
+
* });
|
|
3394
|
+
* ```
|
|
3395
|
+
*/
|
|
3396
|
+
async generateProof(inputs, options) {
|
|
3397
|
+
const startTime = Date.now();
|
|
3398
|
+
const actualTimeout = options?.timeout || this.timeout;
|
|
3399
|
+
options?.onStart?.();
|
|
3400
|
+
let progressInterval;
|
|
3401
|
+
try {
|
|
3402
|
+
const requestBody = {
|
|
3403
|
+
private_inputs: JSON.stringify(inputs.privateInputs),
|
|
3404
|
+
public_inputs: JSON.stringify(inputs.publicInputs),
|
|
3405
|
+
outputs: JSON.stringify(inputs.outputs)
|
|
3406
|
+
};
|
|
3407
|
+
if (inputs.swapParams) {
|
|
3408
|
+
requestBody.swap_params = inputs.swapParams;
|
|
3409
|
+
}
|
|
3410
|
+
const controller = new AbortController();
|
|
3411
|
+
const timeoutId = setTimeout(() => controller.abort(), actualTimeout);
|
|
3412
|
+
if (options?.onProgress) {
|
|
3413
|
+
let progress = 0;
|
|
3414
|
+
progressInterval = setInterval(() => {
|
|
3415
|
+
progress = Math.min(90, progress + Math.random() * 10);
|
|
3416
|
+
options.onProgress(Math.floor(progress));
|
|
3417
|
+
}, 2e3);
|
|
3418
|
+
}
|
|
3419
|
+
const response = await fetch(`${this.indexerUrl}/api/v1/prove`, {
|
|
3420
|
+
method: "POST",
|
|
3421
|
+
headers: {
|
|
3422
|
+
"Content-Type": "application/json"
|
|
3423
|
+
},
|
|
3424
|
+
body: JSON.stringify(requestBody),
|
|
3425
|
+
signal: controller.signal
|
|
3426
|
+
});
|
|
3427
|
+
clearTimeout(timeoutId);
|
|
3428
|
+
if (progressInterval) clearInterval(progressInterval);
|
|
3429
|
+
if (!response.ok) {
|
|
3430
|
+
let errorMessage = `${response.status} ${response.statusText}`;
|
|
3431
|
+
try {
|
|
3432
|
+
const errorText = await response.text();
|
|
3433
|
+
try {
|
|
3434
|
+
const errorJson = JSON.parse(errorText);
|
|
3435
|
+
errorMessage = errorJson.error || errorJson.message || errorText;
|
|
3436
|
+
} catch {
|
|
3437
|
+
errorMessage = errorText || errorMessage;
|
|
3438
|
+
}
|
|
3439
|
+
} catch {
|
|
3440
|
+
}
|
|
3441
|
+
options?.onError?.(errorMessage);
|
|
3442
|
+
return {
|
|
3443
|
+
success: false,
|
|
3444
|
+
generationTimeMs: Date.now() - startTime,
|
|
3445
|
+
error: errorMessage
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
options?.onProgress?.(100);
|
|
3449
|
+
const rawData = await response.json();
|
|
3450
|
+
const result = {
|
|
3451
|
+
success: rawData.success,
|
|
3452
|
+
proof: rawData.proof,
|
|
3453
|
+
publicInputs: rawData.public_inputs,
|
|
3454
|
+
// Map snake_case
|
|
3455
|
+
generationTimeMs: rawData.generation_time_ms || Date.now() - startTime,
|
|
3456
|
+
error: rawData.error
|
|
3457
|
+
};
|
|
3458
|
+
if (!result.success && rawData.execution_report) {
|
|
3459
|
+
}
|
|
3460
|
+
if (!result.success && result.error) {
|
|
3461
|
+
try {
|
|
3462
|
+
const errorObj = typeof result.error === "string" ? JSON.parse(result.error) : result.error;
|
|
3463
|
+
if (errorObj?.error && typeof errorObj.error === "string") {
|
|
3464
|
+
result.error = errorObj.error;
|
|
3465
|
+
} else if (typeof errorObj === "string") {
|
|
3466
|
+
result.error = errorObj;
|
|
3467
|
+
}
|
|
3468
|
+
if (errorObj?.execution_report && typeof errorObj.execution_report === "string") {
|
|
3469
|
+
result.error += `
|
|
3470
|
+
Execution report: ${errorObj.execution_report}`;
|
|
3471
|
+
}
|
|
3472
|
+
if (errorObj?.total_cycles !== void 0) {
|
|
3473
|
+
result.error += `
|
|
3474
|
+
Total cycles: ${errorObj.total_cycles}`;
|
|
3475
|
+
}
|
|
3476
|
+
if (errorObj?.total_syscalls !== void 0) {
|
|
3477
|
+
result.error += `
|
|
3478
|
+
Total syscalls: ${errorObj.total_syscalls}`;
|
|
3479
|
+
}
|
|
3480
|
+
} catch {
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
if (result.success) {
|
|
3484
|
+
options?.onSuccess?.(result);
|
|
3485
|
+
} else if (result.error) {
|
|
3486
|
+
options?.onError?.(result.error);
|
|
3487
|
+
}
|
|
3488
|
+
return result;
|
|
3489
|
+
} catch (error) {
|
|
3490
|
+
const totalTime = Date.now() - startTime;
|
|
3491
|
+
if (progressInterval) clearInterval(progressInterval);
|
|
3492
|
+
let errorMessage;
|
|
3493
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
3494
|
+
errorMessage = `Proof generation timed out after ${actualTimeout}ms`;
|
|
3495
|
+
} else {
|
|
3496
|
+
errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
3497
|
+
}
|
|
3498
|
+
options?.onError?.(errorMessage);
|
|
3499
|
+
return {
|
|
3500
|
+
success: false,
|
|
3501
|
+
generationTimeMs: totalTime,
|
|
3502
|
+
error: errorMessage
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Check if the prover service is available
|
|
3508
|
+
*
|
|
3509
|
+
* @returns True if service is healthy
|
|
3510
|
+
*/
|
|
3511
|
+
async healthCheck() {
|
|
3512
|
+
try {
|
|
3513
|
+
const response = await fetch(`${this.indexerUrl}/health`, {
|
|
3514
|
+
method: "GET"
|
|
3515
|
+
});
|
|
3516
|
+
return response.ok;
|
|
3517
|
+
} catch {
|
|
3518
|
+
return false;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Get the configured timeout
|
|
3523
|
+
*/
|
|
3524
|
+
getTimeout() {
|
|
3525
|
+
return this.timeout;
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Set a new timeout
|
|
3529
|
+
*/
|
|
3530
|
+
setTimeout(timeout) {
|
|
3531
|
+
if (timeout <= 0) {
|
|
3532
|
+
throw new Error("Timeout must be positive");
|
|
3533
|
+
}
|
|
3534
|
+
this.timeout = timeout;
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
|
|
3538
|
+
// src/helpers/wallet-integration.ts
|
|
3539
|
+
var import_web37 = require("@solana/web3.js");
|
|
3540
|
+
function validateWalletConnected(wallet) {
|
|
3541
|
+
if (wallet instanceof import_web37.Keypair) {
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
if (!wallet.publicKey) {
|
|
3545
|
+
throw new CloakError(
|
|
3546
|
+
"Wallet not connected. Please connect your wallet first.",
|
|
3547
|
+
"wallet",
|
|
3548
|
+
false
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
function getPublicKey(wallet) {
|
|
3553
|
+
if (wallet instanceof import_web37.Keypair) {
|
|
3554
|
+
return wallet.publicKey;
|
|
3555
|
+
}
|
|
3556
|
+
if (!wallet.publicKey) {
|
|
3557
|
+
throw new CloakError(
|
|
3558
|
+
"Wallet not connected",
|
|
3559
|
+
"wallet",
|
|
3560
|
+
false
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
return wallet.publicKey;
|
|
3564
|
+
}
|
|
3565
|
+
async function sendTransaction(transaction, wallet, connection, options) {
|
|
3566
|
+
if (wallet instanceof import_web37.Keypair) {
|
|
3567
|
+
return await connection.sendTransaction(transaction, [wallet], options);
|
|
3568
|
+
}
|
|
3569
|
+
if (wallet.sendTransaction) {
|
|
3570
|
+
return await wallet.sendTransaction(transaction, connection, options);
|
|
3571
|
+
} else if (wallet.signTransaction) {
|
|
3572
|
+
const signed = await wallet.signTransaction(transaction);
|
|
3573
|
+
return await connection.sendRawTransaction(signed.serialize(), options);
|
|
3574
|
+
} else {
|
|
3575
|
+
throw new CloakError(
|
|
3576
|
+
"Wallet does not support transaction signing",
|
|
3577
|
+
"wallet",
|
|
3578
|
+
false
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
async function signTransaction(transaction, wallet) {
|
|
3583
|
+
if (wallet instanceof import_web37.Keypair) {
|
|
3584
|
+
transaction.sign(wallet);
|
|
3585
|
+
return transaction;
|
|
3586
|
+
}
|
|
3587
|
+
if (!wallet.signTransaction) {
|
|
3588
|
+
throw new CloakError(
|
|
3589
|
+
"Wallet does not support transaction signing",
|
|
3590
|
+
"wallet",
|
|
3591
|
+
false
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
return await wallet.signTransaction(transaction);
|
|
3595
|
+
}
|
|
3596
|
+
function keypairToAdapter(keypair) {
|
|
3597
|
+
return {
|
|
3598
|
+
publicKey: keypair.publicKey,
|
|
3599
|
+
signTransaction: async (tx) => {
|
|
3600
|
+
tx.sign(keypair);
|
|
3601
|
+
return tx;
|
|
3602
|
+
},
|
|
3603
|
+
signAllTransactions: async (txs) => {
|
|
3604
|
+
txs.forEach((tx) => tx.sign(keypair));
|
|
3605
|
+
return txs;
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// src/index.ts
|
|
3611
|
+
var VERSION = "1.0.0";
|
|
3612
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3613
|
+
0 && (module.exports = {
|
|
3614
|
+
ArtifactProverService,
|
|
3615
|
+
CLOAK_PROGRAM_ID,
|
|
3616
|
+
CloakError,
|
|
3617
|
+
CloakSDK,
|
|
3618
|
+
DepositRecoveryService,
|
|
3619
|
+
FIXED_FEE_LAMPORTS,
|
|
3620
|
+
IndexerService,
|
|
3621
|
+
LAMPORTS_PER_SOL,
|
|
3622
|
+
LocalStorageAdapter,
|
|
3623
|
+
MemoryStorageAdapter,
|
|
3624
|
+
ProverService,
|
|
3625
|
+
RelayService,
|
|
3626
|
+
VARIABLE_FEE_RATE,
|
|
3627
|
+
VERSION,
|
|
3628
|
+
bigintToBytes32,
|
|
3629
|
+
buildPublicInputsBytes,
|
|
3630
|
+
bytesToHex,
|
|
3631
|
+
calculateFee,
|
|
3632
|
+
calculateRelayFee,
|
|
3633
|
+
computeCommitment,
|
|
3634
|
+
computeMerkleRoot,
|
|
3635
|
+
computeNullifier,
|
|
3636
|
+
computeNullifierAsync,
|
|
3637
|
+
computeNullifierSync,
|
|
3638
|
+
computeOutputsHash,
|
|
3639
|
+
computeOutputsHashAsync,
|
|
3640
|
+
computeOutputsHashSync,
|
|
3641
|
+
computeSwapOutputsHash,
|
|
3642
|
+
computeSwapOutputsHashAsync,
|
|
3643
|
+
computeSwapOutputsHashSync,
|
|
3644
|
+
copyNoteToClipboard,
|
|
3645
|
+
createCloakError,
|
|
3646
|
+
createDepositInstruction,
|
|
3647
|
+
deriveSpendKey,
|
|
3648
|
+
deriveViewKey,
|
|
3649
|
+
detectNetworkFromRpcUrl,
|
|
3650
|
+
downloadNote,
|
|
3651
|
+
encodeNoteSimple,
|
|
3652
|
+
encryptNoteForRecipient,
|
|
3653
|
+
exportKeys,
|
|
3654
|
+
exportNote,
|
|
3655
|
+
exportWalletKeys,
|
|
3656
|
+
filterNotesByNetwork,
|
|
3657
|
+
filterWithdrawableNotes,
|
|
3658
|
+
findNoteByCommitment,
|
|
3659
|
+
formatAmount,
|
|
3660
|
+
formatErrorForLogging,
|
|
3661
|
+
generateCloakKeys,
|
|
3662
|
+
generateCommitment,
|
|
3663
|
+
generateCommitmentAsync,
|
|
3664
|
+
generateMasterSeed,
|
|
3665
|
+
generateNote,
|
|
3666
|
+
generateNoteFromWallet,
|
|
3667
|
+
getAddressExplorerUrl,
|
|
3668
|
+
getDistributableAmount,
|
|
3669
|
+
getExplorerUrl,
|
|
3670
|
+
getPublicKey,
|
|
3671
|
+
getPublicViewKey,
|
|
3672
|
+
getRecipientAmount,
|
|
3673
|
+
getRpcUrlForNetwork,
|
|
3674
|
+
getShieldPoolPDAs,
|
|
3675
|
+
getViewKey,
|
|
3676
|
+
hexToBigint,
|
|
3677
|
+
hexToBytes,
|
|
3678
|
+
importKeys,
|
|
3679
|
+
importWalletKeys,
|
|
3680
|
+
isValidHex,
|
|
3681
|
+
isValidRpcUrl,
|
|
3682
|
+
isValidSolanaAddress,
|
|
3683
|
+
isWithdrawable,
|
|
3684
|
+
keypairToAdapter,
|
|
3685
|
+
parseAmount,
|
|
3686
|
+
parseNote,
|
|
3687
|
+
parseTransactionError,
|
|
3688
|
+
poseidonHash,
|
|
3689
|
+
prepareEncryptedOutput,
|
|
3690
|
+
prepareEncryptedOutputForRecipient,
|
|
3691
|
+
proofToBytes,
|
|
3692
|
+
pubkeyToLimbs,
|
|
3693
|
+
randomBytes,
|
|
3694
|
+
scanNotesForWallet,
|
|
3695
|
+
sendTransaction,
|
|
3696
|
+
serializeNote,
|
|
3697
|
+
signTransaction,
|
|
3698
|
+
splitTo2Limbs,
|
|
3699
|
+
tryDecryptNote,
|
|
3700
|
+
updateNoteWithDeposit,
|
|
3701
|
+
validateDepositParams,
|
|
3702
|
+
validateNote,
|
|
3703
|
+
validateOutputsSum,
|
|
3704
|
+
validateTransfers,
|
|
3705
|
+
validateWalletConnected,
|
|
3706
|
+
validateWithdrawableNote
|
|
3707
|
+
});
|