@lethenymous/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,357 @@
1
+ import { createHash } from "node:crypto";
2
+ import { PublicKey, Transaction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
3
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAccount, getAssociatedTokenAddressSync, getMint, TOKEN_PROGRAM_ID, } from "@solana/spl-token";
4
+ import { accountDiscriminator, PROGRAM_ID } from "./encoding.js";
5
+ import { decodePool, decodeProtocolConfig, decodeShieldedState, decodeTreeState } from "./accounts.js";
6
+ import { pda } from "./pda.js";
7
+ import { addLiquidity as addLiquidityIx, initializePool, initializeShieldedState, removeLiquidity as removeLiquidityIx, swap as swapIx } from "./instructions.js";
8
+ import { swapOutputPreservingLpClaims } from "./math.js";
9
+ import { keyHierarchy, ownerCommitment } from "./crypto.js";
10
+ import { EncryptedFileNoteStore } from "./store.js";
11
+ import { ShieldedWallet } from "./wallet.js";
12
+ const FINALIZED = "finalized";
13
+ const MAX_U64 = 0xffffffffffffffffn;
14
+ const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
15
+ function encodeBase58(value) {
16
+ let number = BigInt(`0x${Buffer.from(value).toString("hex")}`);
17
+ let output = "";
18
+ while (number > 0n) {
19
+ const remainder = Number(number % 58n);
20
+ output = BASE58[remainder] + output;
21
+ number /= 58n;
22
+ }
23
+ let leading = 0;
24
+ while (leading < value.length && value[leading] === 0)
25
+ leading++;
26
+ return "1".repeat(leading) + output;
27
+ }
28
+ function signedSignature(transaction) {
29
+ const signature = transaction instanceof VersionedTransaction ? transaction.signatures[0] : transaction.signature;
30
+ if (!signature || signature.every(byte => byte === 0))
31
+ return undefined;
32
+ return encodeBase58(signature);
33
+ }
34
+ export function transactionSignatureFromBytes(serialized) {
35
+ try {
36
+ return signedSignature(VersionedTransaction.deserialize(serialized));
37
+ }
38
+ catch {
39
+ try {
40
+ return signedSignature(Transaction.from(Buffer.from(serialized)));
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ }
47
+ export class LookupTableRequiredError extends Error {
48
+ constructor() { super("A validated versioned lookup table is required for private proof transactions"); this.name = "LookupTableRequiredError"; }
49
+ }
50
+ export class TransactionFailedError extends Error {
51
+ outcome;
52
+ constructor(outcome) {
53
+ super(`Transaction finalized with an execution error: ${String(outcome.error)}`);
54
+ this.outcome = outcome;
55
+ this.name = "TransactionFailedError";
56
+ }
57
+ }
58
+ export class TransactionUnknownError extends Error {
59
+ outcome;
60
+ constructor(outcome) {
61
+ super(`Transaction outcome is unknown: ${outcome.reason}`);
62
+ this.outcome = outcome;
63
+ this.name = "TransactionUnknownError";
64
+ }
65
+ }
66
+ export class Lethenymous {
67
+ connection;
68
+ wallet;
69
+ programId;
70
+ witnessProvider;
71
+ lookupTables;
72
+ lookupTableCache = new Map();
73
+ lookupTableLoads = new Map();
74
+ constructor(config) {
75
+ this.connection = config.connection;
76
+ this.wallet = config.wallet;
77
+ this.programId = config.programId ?? PROGRAM_ID;
78
+ this.witnessProvider = config.witnessProvider;
79
+ this.lookupTables = (config.lookupTables ?? []).map(value => value instanceof PublicKey ? { address: value } : value);
80
+ }
81
+ async account(address, kind) {
82
+ const info = await this.connection.getAccountInfo(address, FINALIZED);
83
+ if (!info)
84
+ throw new Error(`Account not found: ${address.toBase58()}`);
85
+ if (!info.owner.equals(this.programId))
86
+ throw new Error(`Account is not owned by configured program: ${address.toBase58()}`);
87
+ if (info.data.length < 8 || !info.data.subarray(0, 8).equals(accountDiscriminator(kind)))
88
+ throw new Error(`Invalid ${kind} account discriminator: ${address.toBase58()}`);
89
+ return info.data.subarray(8);
90
+ }
91
+ async getPool(address) {
92
+ return decodePool(await this.account(address, "Pool"), address);
93
+ }
94
+ async getShieldedState(pool) {
95
+ const state = decodeShieldedState(await this.account(pda.shielded(pool, this.programId)[0], "ShieldedState"));
96
+ if (!state.pool.equals(pool) || !state.tree.equals(pda.tree(pool, this.programId)[0]))
97
+ throw new Error("Shielded state does not match pool");
98
+ return state;
99
+ }
100
+ async getTree(pool) {
101
+ const tree = decodeTreeState(await this.account(pda.tree(pool, this.programId)[0], "TreeState"));
102
+ if (!tree.pool.equals(pool))
103
+ throw new Error("Tree does not match pool");
104
+ return tree;
105
+ }
106
+ async getProtocolConfig() {
107
+ return decodeProtocolConfig(await this.account(pda.protocolConfig(this.programId)[0], "ProtocolConfig"));
108
+ }
109
+ async getReserves(pool) {
110
+ const [a, b] = await Promise.all([
111
+ getAccount(this.connection, pool.tokenAVault, FINALIZED, TOKEN_PROGRAM_ID),
112
+ getAccount(this.connection, pool.tokenBVault, FINALIZED, TOKEN_PROGRAM_ID),
113
+ ]);
114
+ if (!a.mint.equals(pool.tokenAMint) || !b.mint.equals(pool.tokenBMint))
115
+ throw new Error("Pool vault mint mismatch");
116
+ return { a: a.amount, b: b.amount };
117
+ }
118
+ async getLpSupply(pool) {
119
+ return (await getMint(this.connection, pool.lpMint, FINALIZED, TOKEN_PROGRAM_ID)).supply;
120
+ }
121
+ async validatedTables(instructions) {
122
+ if (!this.lookupTables.length)
123
+ return [];
124
+ const slot = await this.connection.getSlot(FINALIZED);
125
+ const tables = [];
126
+ for (const config of this.lookupTables) {
127
+ const key = config.address.toBase58();
128
+ let entry = this.lookupTableCache.get(key)?.slot === slot ? this.lookupTableCache.get(key) : undefined;
129
+ if (!entry) {
130
+ let load = this.lookupTableLoads.get(key);
131
+ if (!load) {
132
+ load = (async () => {
133
+ const response = await this.connection.getAddressLookupTable(config.address, { commitment: FINALIZED });
134
+ const value = response.value;
135
+ if (!value)
136
+ throw new Error(`Configured address lookup table is unavailable: ${config.address.toBase58()}`);
137
+ if (!value.key.equals(config.address))
138
+ throw new Error(`Configured address lookup table returned the wrong account: ${config.address.toBase58()}`);
139
+ const responseSlot = response.context?.slot ?? slot;
140
+ if (!Number.isSafeInteger(responseSlot) || responseSlot < 0)
141
+ throw new Error("Configured address lookup table returned an invalid context slot");
142
+ return { slot: responseSlot, table: value };
143
+ })();
144
+ this.lookupTableLoads.set(key, load);
145
+ }
146
+ try {
147
+ entry = await load;
148
+ this.lookupTableCache.set(key, entry);
149
+ }
150
+ finally {
151
+ if (this.lookupTableLoads.get(key) === load)
152
+ this.lookupTableLoads.delete(key);
153
+ }
154
+ }
155
+ const value = entry.table;
156
+ const validationSlot = entry.slot;
157
+ const state = value.state;
158
+ const lastExtendedSlotStartIndex = state.lastExtendedSlotStartIndex ?? state.addresses.length;
159
+ if (BigInt(state.lastExtendedSlot) > BigInt(validationSlot) || (BigInt(state.lastExtendedSlot) === BigInt(validationSlot) && lastExtendedSlotStartIndex < state.addresses.length))
160
+ throw new Error("Configured address lookup table is not active at finalized slot");
161
+ if (state.deactivationSlot !== MAX_U64 && state.deactivationSlot <= BigInt(validationSlot))
162
+ throw new Error("Configured address lookup table is deactivated");
163
+ if (config.expectedAuthority !== undefined) {
164
+ const actual = state.authority ?? null;
165
+ if ((actual === null) !== (config.expectedAuthority === null) || (actual && !actual.equals(config.expectedAuthority)))
166
+ throw new Error("Configured address lookup table authority mismatch");
167
+ }
168
+ if (config.expectedAddresses) {
169
+ const actual = new Set(state.addresses.map(address => address.toBase58()));
170
+ const expected = new Set(config.expectedAddresses.map(address => address.toBase58()));
171
+ if (expected.size !== config.expectedAddresses.length || actual.size !== expected.size)
172
+ throw new Error("Configured address lookup table contents differ from the expected deployment");
173
+ for (const address of expected)
174
+ if (!actual.has(address))
175
+ throw new Error("Configured address lookup table is missing an expected address");
176
+ }
177
+ tables.push(value);
178
+ }
179
+ return tables;
180
+ }
181
+ async buildAndSendOutcome(instructions, options = {}) {
182
+ if (options.requireVersioned && !this.lookupTables.length)
183
+ throw new LookupTableRequiredError();
184
+ const latest = await this.connection.getLatestBlockhash(FINALIZED);
185
+ const tables = await this.validatedTables(instructions);
186
+ let raw;
187
+ if (tables.length) {
188
+ if (!this.wallet.signVersionedTransaction)
189
+ throw new Error("Wallet adapter must implement signVersionedTransaction when lookup tables are configured");
190
+ const message = new TransactionMessage({ payerKey: this.wallet.publicKey, recentBlockhash: latest.blockhash, instructions }).compileToV0Message(tables);
191
+ raw = await this.wallet.signVersionedTransaction(new VersionedTransaction(message));
192
+ }
193
+ else {
194
+ if (options.requireVersioned)
195
+ throw new LookupTableRequiredError();
196
+ const transaction = new Transaction();
197
+ transaction.feePayer = this.wallet.publicKey;
198
+ transaction.recentBlockhash = latest.blockhash;
199
+ transaction.lastValidBlockHeight = latest.lastValidBlockHeight;
200
+ transaction.add(...instructions);
201
+ raw = await this.wallet.signTransaction(transaction);
202
+ }
203
+ const serialized = raw.serialize();
204
+ if (options.onPrepared)
205
+ await options.onPrepared(serialized, BigInt(latest.lastValidBlockHeight));
206
+ let signature;
207
+ try {
208
+ signature = await this.connection.sendRawTransaction(serialized);
209
+ }
210
+ catch (error) {
211
+ signature = signedSignature(raw);
212
+ if (!signature)
213
+ throw new Error(`Transaction submission failed before a signature was returned: ${String(error)}`);
214
+ if (options.onSubmitted) {
215
+ try {
216
+ await options.onSubmitted(signature);
217
+ }
218
+ catch { /* The transaction remains unknown and must not release local state. */ }
219
+ }
220
+ return { status: "unknown", signature, reason: "Transaction submission acknowledgement failed after signing" };
221
+ }
222
+ if (options.onSubmitted) {
223
+ try {
224
+ await options.onSubmitted(signature);
225
+ }
226
+ catch {
227
+ return { status: "unknown", signature, reason: "Transaction submitted; local submission state could not be journaled" };
228
+ }
229
+ }
230
+ try {
231
+ const confirmation = await this.connection.confirmTransaction({ signature, blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight }, FINALIZED);
232
+ if (confirmation.value.err !== null)
233
+ return { status: "finalized-failed", signature, error: confirmation.value.err };
234
+ return { status: "finalized-success", signature };
235
+ }
236
+ catch (error) {
237
+ return this.reconcileTransaction(signature, latest.lastValidBlockHeight, String(error));
238
+ }
239
+ }
240
+ async reconcileTransaction(signature, lastValidBlockHeight, reason = "confirmation did not complete") {
241
+ try {
242
+ const status = (await this.connection.getSignatureStatuses([signature], { searchTransactionHistory: true })).value[0];
243
+ if (status?.confirmationStatus === "finalized")
244
+ return { status: "finalized-success", signature };
245
+ const transaction = await this.connection.getTransaction(signature, { commitment: "finalized", maxSupportedTransactionVersion: 0 });
246
+ if (transaction?.meta?.err !== null && transaction?.meta?.err !== undefined)
247
+ return { status: "finalized-failed", signature, error: transaction.meta.err };
248
+ if (transaction?.meta && transaction.meta.err === null)
249
+ return { status: "finalized-success", signature };
250
+ if (status == null && lastValidBlockHeight !== undefined && await this.connection.getBlockHeight(FINALIZED) > lastValidBlockHeight)
251
+ return { status: "finalized-failed", signature, error: new Error("Blockhash expired before finalization") };
252
+ return { status: "unknown", signature, reason };
253
+ }
254
+ catch {
255
+ return { status: "unknown", signature, reason: "Transaction outcome could not be reconciled" };
256
+ }
257
+ }
258
+ async hasFinalizedProgramEvent(signature, name, needles = []) {
259
+ const transaction = await this.connection.getTransaction(signature, { commitment: "finalized", maxSupportedTransactionVersion: 0 });
260
+ if (!transaction || transaction.meta?.err !== null && transaction.meta?.err !== undefined)
261
+ return false;
262
+ const discriminator = createHash("sha256").update(`event:${name}`).digest().subarray(0, 8);
263
+ const stack = [];
264
+ for (const log of transaction.meta?.logMessages ?? []) {
265
+ const invoke = /^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[\d+\]$/.exec(log);
266
+ if (invoke) {
267
+ stack.push(invoke[1]);
268
+ continue;
269
+ }
270
+ const finished = /^Program ([1-9A-HJ-NP-z]+) (success|failed:.*)$/.exec(log);
271
+ if (finished) {
272
+ if (stack.length)
273
+ stack.pop();
274
+ continue;
275
+ }
276
+ if (!log.startsWith("Program data: ") || stack.at(-1) !== this.programId.toBase58())
277
+ continue;
278
+ const data = Buffer.from(log.slice("Program data: ".length), "base64");
279
+ if (!data.subarray(0, 8).equals(discriminator))
280
+ continue;
281
+ if (needles.every(needle => data.includes(Buffer.from(needle))))
282
+ return true;
283
+ }
284
+ return false;
285
+ }
286
+ async buildAndSend(instructions, options = {}) {
287
+ const outcome = await this.buildAndSendOutcome(instructions, options);
288
+ if (outcome.status === "finalized-success")
289
+ return outcome.signature;
290
+ if (outcome.status === "finalized-failed")
291
+ throw new TransactionFailedError(outcome);
292
+ throw new TransactionUnknownError(outcome);
293
+ }
294
+ async initializePool(a, b, feeBps, creator = this.wallet.publicKey) {
295
+ return this.buildAndSend([initializePool(this.wallet.publicKey, this.wallet.publicKey, creator, a, b, feeBps, this.programId)]);
296
+ }
297
+ async initializeShieldedState(pool) {
298
+ const state = await this.getPool(pool);
299
+ return this.buildAndSend([initializeShieldedState(this.wallet.publicKey, pool, { pool, tokenAMint: state.tokenAMint, tokenBMint: state.tokenBMint }, this.programId)]);
300
+ }
301
+ async ata(mint, owner, allowOwnerOffCurve = false) {
302
+ const address = getAssociatedTokenAddressSync(mint, owner, allowOwnerOffCurve, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID);
303
+ const info = await this.connection.getAccountInfo(address, FINALIZED);
304
+ if (!info)
305
+ return { address, create: createAssociatedTokenAccountIdempotentInstruction(this.wallet.publicKey, address, owner, mint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID) };
306
+ if (!info.owner.equals(TOKEN_PROGRAM_ID))
307
+ throw new Error("Existing ATA is not owned by the classic token program");
308
+ const account = await getAccount(this.connection, address, FINALIZED, TOKEN_PROGRAM_ID);
309
+ if (!account.mint.equals(mint) || !account.owner.equals(owner))
310
+ throw new Error("Existing ATA has the wrong mint or authority");
311
+ return { address };
312
+ }
313
+ async addLiquidity(poolAddress, amountA, amountB, minLp = 0n) {
314
+ const pool = await this.getPool(poolAddress);
315
+ const [a, b, lp] = await Promise.all([this.ata(pool.tokenAMint, this.wallet.publicKey), this.ata(pool.tokenBMint, this.wallet.publicKey), this.ata(pool.lpMint, this.wallet.publicKey)]);
316
+ return this.buildAndSend([a.create, b.create, lp.create, addLiquidityIx(this.wallet.publicKey, pool, a.address, b.address, lp.address, amountA, amountB, minLp, this.programId)].filter(Boolean));
317
+ }
318
+ async removeLiquidity(poolAddress, lpAmount, minA = 0n, minB = 0n) {
319
+ const pool = await this.getPool(poolAddress);
320
+ const [a, b, lp] = await Promise.all([this.ata(pool.tokenAMint, this.wallet.publicKey), this.ata(pool.tokenBMint, this.wallet.publicKey), this.ata(pool.lpMint, this.wallet.publicKey)]);
321
+ return this.buildAndSend([a.create, b.create, removeLiquidityIx(this.wallet.publicKey, pool, a.address, b.address, lp.address, lpAmount, minA, minB, this.programId)].filter(Boolean));
322
+ }
323
+ async quote(poolAddress, direction, amountIn) {
324
+ const pool = await this.getPool(poolAddress);
325
+ const [reserves, supply] = await Promise.all([this.getReserves(pool), this.getLpSupply(pool)]);
326
+ return swapOutputPreservingLpClaims(direction === "AToB" ? reserves.a : reserves.b, direction === "AToB" ? reserves.b : reserves.a, amountIn, pool.feeBps, supply);
327
+ }
328
+ async swap(poolAddress, direction, amountIn, minAmountOut) {
329
+ const pool = await this.getPool(poolAddress);
330
+ const [a, b] = await Promise.all([this.ata(pool.tokenAMint, this.wallet.publicKey), this.ata(pool.tokenBMint, this.wallet.publicKey)]);
331
+ return this.buildAndSend([a.create, b.create, swapIx(this.wallet.publicKey, pool, a.address, b.address, direction === "AToB" ? 0 : 1, amountIn, minAmountOut, this.programId)].filter(Boolean));
332
+ }
333
+ async ensureAta(mint, owner = this.wallet.publicKey, allowOwnerOffCurve = false) {
334
+ return (await this.ata(mint, owner, allowOwnerOffCurve)).address;
335
+ }
336
+ async ensureAtas(mintOwnerPairs) {
337
+ const entries = await Promise.all(mintOwnerPairs.map(entry => this.ata(entry.mint, entry.owner, entry.allowOwnerOffCurve ?? false)));
338
+ const missing = entries.flatMap(entry => entry.create ? [entry.create] : []);
339
+ if (missing.length)
340
+ await this.buildAndSend(missing);
341
+ return entries.map(entry => entry.address);
342
+ }
343
+ assertPrivateTransactionReady() {
344
+ if (!this.lookupTables.length || !this.wallet.signVersionedTransaction)
345
+ throw new LookupTableRequiredError();
346
+ }
347
+ async validatePrivateTransactionReady(instructions) {
348
+ this.assertPrivateTransactionReady();
349
+ await this.validatedTables(instructions);
350
+ }
351
+ shieldedWallet(seed, prover, options = {}) {
352
+ const noteStore = options.noteStore ?? (options.storagePath ? EncryptedFileNoteStore.fromSeed(options.storagePath, seed, ownerCommitment(keyHierarchy(seed).spendSecret)) : undefined);
353
+ if (!noteStore)
354
+ throw new Error("A persistent encrypted NoteStore is required; pass storagePath or noteStore");
355
+ return new ShieldedWallet(this, seed, prover, this.witnessProvider, noteStore);
356
+ }
357
+ }
@@ -0,0 +1,24 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ export declare const NOTE_PLAINTEXT_LENGTH = 145;
3
+ export declare const NOTE_PAYLOAD_LENGTH: number;
4
+ export declare function hash2(a: Uint8Array, b: Uint8Array): Uint8Array;
5
+ export declare function keyHierarchy(seed: Uint8Array): {
6
+ spendSecret: Uint8Array<ArrayBufferLike>;
7
+ viewKey: Uint8Array<ArrayBufferLike>;
8
+ storageKey: Uint8Array<ArrayBufferLike>;
9
+ };
10
+ export declare function noteStoreKey(seed: Uint8Array): Uint8Array;
11
+ export declare function ownerCommitment(spend: Uint8Array): Uint8Array;
12
+ export declare function noteCommitment(pool: PublicKey, asset: PublicKey, amount: bigint, owner: Uint8Array, randomness: Uint8Array): Uint8Array;
13
+ export declare function nullifier(pool: PublicKey, asset: PublicKey, spend: Uint8Array, randomness: Uint8Array): Uint8Array;
14
+ export declare function encodeNote(pool: PublicKey, asset: PublicKey, amount: bigint, owner: Uint8Array, randomness: Uint8Array): Buffer;
15
+ export interface DecryptedNote {
16
+ pool: PublicKey;
17
+ asset: PublicKey;
18
+ amount: bigint;
19
+ ownerCommitment: Uint8Array;
20
+ randomness: Uint8Array;
21
+ }
22
+ export declare function encryptNote(note: Buffer, viewKey: Uint8Array, pool: PublicKey, asset: PublicKey, commitment: Uint8Array): Buffer;
23
+ export declare function decryptNotePayload(payload: Uint8Array, viewKey: Uint8Array, pool: PublicKey, asset: PublicKey, commitment: Uint8Array): DecryptedNote;
24
+ export declare function cryptoRandom(length: number): Uint8Array;
package/dist/crypto.js ADDED
@@ -0,0 +1,111 @@
1
+ import { hkdf } from "@noble/hashes/hkdf";
2
+ import { sha256 } from "@noble/hashes/sha256";
3
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
4
+ import { poseidon2, poseidon3, poseidon9 } from "poseidon-lite";
5
+ import { PublicKey } from "@solana/web3.js";
6
+ const DOMAIN = 0x5a4b43504d4d0003n;
7
+ const OWNER_DOMAIN = 0x5a4b43504d4f0003n;
8
+ const NULLIFIER_DOMAIN = 0x5a4b43504d4e0003n;
9
+ const NOTE_VERSION = 2;
10
+ const PAYLOAD_VERSION = 1;
11
+ const AAD = Buffer.from("zkcpmm-v2/note-payload");
12
+ export const NOTE_PLAINTEXT_LENGTH = 145;
13
+ export const NOTE_PAYLOAD_LENGTH = 1 + 24 + NOTE_PLAINTEXT_LENGTH + 16;
14
+ const zero32 = () => new Uint8Array(32);
15
+ const be = (n) => { const b = Buffer.alloc(32); b.writeBigUInt64BE(n, 24); return b; };
16
+ const big = (b) => BigInt(`0x${Buffer.from(b).toString("hex")}`);
17
+ export function hash2(a, b) {
18
+ const n = poseidon2([big(a), big(b)]);
19
+ return Buffer.from(n.toString(16).padStart(64, "0"), "hex");
20
+ }
21
+ function limb(b) {
22
+ const out = zero32();
23
+ out.set(b.subarray(0, 16), 16);
24
+ return out;
25
+ }
26
+ export function keyHierarchy(seed) {
27
+ const spendSecret = hkdf(sha256, seed, Buffer.from("zkcpmm-v2/key-hierarchy"), Buffer.from("spend"), 32);
28
+ const viewKey = hkdf(sha256, seed, Buffer.from("zkcpmm-v2/key-hierarchy"), Buffer.from("view"), 32);
29
+ const storageKey = hkdf(sha256, seed, Buffer.from("zkcpmm-v2/key-hierarchy"), Buffer.from("note-store/v1"), 32);
30
+ return { spendSecret, viewKey, storageKey };
31
+ }
32
+ export function noteStoreKey(seed) {
33
+ return keyHierarchy(seed).storageKey;
34
+ }
35
+ function poseidonN(values, width) {
36
+ const n = (width === 3 ? poseidon3 : poseidon9)(values.map(big));
37
+ return Buffer.from(n.toString(16).padStart(64, "0"), "hex");
38
+ }
39
+ function fold(values) {
40
+ return values.reduce((a, b) => hash2(a, b));
41
+ }
42
+ export function ownerCommitment(spend) {
43
+ if (spend.length !== 32)
44
+ throw new Error("Spend secret must be 32 bytes");
45
+ return poseidonN([be(OWNER_DOMAIN), limb(spend), limb(spend.subarray(16))], 3);
46
+ }
47
+ export function noteCommitment(pool, asset, amount, owner, randomness) {
48
+ if (owner.length !== 32 || randomness.length !== 32)
49
+ throw new Error("Note fields must be 32 bytes");
50
+ const p = pool.toBytes();
51
+ const a = asset.toBytes();
52
+ return fold([be(DOMAIN), limb(p), limb(p.subarray(16)), limb(a), limb(a.subarray(16)), be(amount), owner, limb(randomness), limb(randomness.subarray(16))]);
53
+ }
54
+ export function nullifier(pool, asset, spend, randomness) {
55
+ if (spend.length !== 32 || randomness.length !== 32)
56
+ throw new Error("Nullifier fields must be 32 bytes");
57
+ const p = pool.toBytes();
58
+ const a = asset.toBytes();
59
+ return poseidonN([be(NULLIFIER_DOMAIN), limb(p), limb(p.subarray(16)), limb(a), limb(a.subarray(16)), limb(spend), limb(spend.subarray(16)), limb(randomness), limb(randomness.subarray(16))], 9);
60
+ }
61
+ export function encodeNote(pool, asset, amount, owner, randomness) {
62
+ if (owner.length !== 32 || randomness.length !== 32 || amount < 0n || amount > 0xffffffffffffffffn)
63
+ throw new Error("Invalid note");
64
+ const b = Buffer.alloc(NOTE_PLAINTEXT_LENGTH);
65
+ b[0] = NOTE_VERSION;
66
+ be(DOMAIN).subarray(24).copy(b, 1);
67
+ Buffer.from(pool.toBytes()).copy(b, 9);
68
+ Buffer.from(asset.toBytes()).copy(b, 41);
69
+ be(amount).subarray(24).copy(b, 73);
70
+ Buffer.from(owner).copy(b, 81);
71
+ Buffer.from(randomness).copy(b, 113);
72
+ return b;
73
+ }
74
+ export function encryptNote(note, viewKey, pool, asset, commitment) {
75
+ if (note.length !== NOTE_PLAINTEXT_LENGTH || viewKey.length !== 32 || commitment.length !== 32)
76
+ throw new Error("Invalid note payload");
77
+ const nonce = cryptoRandom(24);
78
+ const aad = Buffer.concat([AAD, pool.toBuffer(), asset.toBuffer(), Buffer.from(commitment)]);
79
+ const cipher = xchacha20poly1305(viewKey, nonce, aad).encrypt(note);
80
+ const payload = Buffer.concat([Buffer.from([PAYLOAD_VERSION]), nonce, Buffer.from(cipher)]);
81
+ if (payload.length !== NOTE_PAYLOAD_LENGTH)
82
+ throw new Error("Invalid encrypted note length");
83
+ return payload;
84
+ }
85
+ export function decryptNotePayload(payload, viewKey, pool, asset, commitment) {
86
+ if (payload.length !== NOTE_PAYLOAD_LENGTH || payload[0] !== PAYLOAD_VERSION)
87
+ throw new Error("Invalid encrypted note payload");
88
+ const aad = Buffer.concat([AAD, pool.toBuffer(), asset.toBuffer(), Buffer.from(commitment)]);
89
+ const plaintext = Buffer.from(xchacha20poly1305(viewKey, payload.subarray(1, 25), aad).decrypt(payload.subarray(25)));
90
+ if (plaintext.length !== NOTE_PLAINTEXT_LENGTH || plaintext[0] !== NOTE_VERSION)
91
+ throw new Error("Invalid note plaintext");
92
+ const decodedPool = new PublicKey(plaintext.subarray(9, 41));
93
+ const decodedAsset = new PublicKey(plaintext.subarray(41, 73));
94
+ const amount = plaintext.readBigUInt64BE(73);
95
+ const ownerCommitmentValue = plaintext.subarray(81, 113);
96
+ const randomness = plaintext.subarray(113, 145);
97
+ if (!decodedPool.equals(pool) || !decodedAsset.equals(asset))
98
+ throw new Error("Note payload identity mismatch");
99
+ if (!Buffer.from(noteCommitment(pool, asset, amount, ownerCommitmentValue, randomness)).equals(Buffer.from(commitment)))
100
+ throw new Error("Note commitment mismatch");
101
+ return { pool: decodedPool, asset: decodedAsset, amount, ownerCommitment: Uint8Array.from(ownerCommitmentValue), randomness: Uint8Array.from(randomness) };
102
+ }
103
+ export function cryptoRandom(length) {
104
+ if (!Number.isSafeInteger(length) || length <= 0)
105
+ throw new Error("Invalid randomness length");
106
+ if (!globalThis.crypto)
107
+ throw new Error("Secure randomness unavailable");
108
+ const out = new Uint8Array(length);
109
+ globalThis.crypto.getRandomValues(out);
110
+ return out;
111
+ }
@@ -0,0 +1,26 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ export declare const PROGRAM_ID: PublicKey;
3
+ export declare const SYSTEM_PROGRAM_ID: PublicKey;
4
+ export declare const TOKEN_PROGRAM_ID: PublicKey;
5
+ export declare function discriminator(name: string): Buffer;
6
+ export declare function accountDiscriminator(name: string): Buffer;
7
+ export declare function concat(...parts: Uint8Array[]): Buffer;
8
+ export declare function u16(value: number): Buffer;
9
+ export declare function u64(value: bigint): Buffer;
10
+ export declare function vec(bytes: Uint8Array): Buffer;
11
+ export declare function u32(value: number): Buffer;
12
+ export declare function pubkey(value: PublicKey): Buffer;
13
+ export declare function bytes32(value: Uint8Array): Buffer;
14
+ export declare function bool(value: boolean): Buffer;
15
+ export declare function enumByte(value: number): Buffer;
16
+ export declare class Reader {
17
+ private readonly data;
18
+ private offset;
19
+ constructor(data: Uint8Array);
20
+ bytes(length: number): Buffer;
21
+ pubkey(): PublicKey;
22
+ u8(): number;
23
+ u16(): number;
24
+ u64(): bigint;
25
+ rest(): number;
26
+ }
@@ -0,0 +1,41 @@
1
+ import { sha256 } from "@noble/hashes/sha256";
2
+ import { PublicKey } from "@solana/web3.js";
3
+ export const PROGRAM_ID = new PublicKey("ZkCP47fAJJREdXNKSBvTsgJAuLoTKepgk6opmqsHobm");
4
+ export const SYSTEM_PROGRAM_ID = new PublicKey("11111111111111111111111111111111");
5
+ export const TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
6
+ export function discriminator(name) {
7
+ return Buffer.from(sha256(new TextEncoder().encode(`global:${name}`)).subarray(0, 8));
8
+ }
9
+ export function accountDiscriminator(name) {
10
+ return Buffer.from(sha256(new TextEncoder().encode(`account:${name}`)).subarray(0, 8));
11
+ }
12
+ export function concat(...parts) { return Buffer.concat(parts.map(Buffer.from)); }
13
+ export function u16(value) { if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff)
14
+ throw new Error("Invalid u16"); const b = Buffer.alloc(2); b.writeUInt16LE(value); return b; }
15
+ export function u64(value) { if (typeof value !== "bigint" || value < 0n || value > 0xffffffffffffffffn)
16
+ throw new Error("u64 requires a bigint in range"); const b = Buffer.alloc(8); b.writeBigUInt64LE(value); return b; }
17
+ export function vec(bytes) { return concat(u32(bytes.length), bytes); }
18
+ export function u32(value) { if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff)
19
+ throw new Error("Invalid u32"); const b = Buffer.alloc(4); b.writeUInt32LE(value); return b; }
20
+ export function pubkey(value) { return Buffer.from(value.toBytes()); }
21
+ export function bytes32(value) {
22
+ if (value.length !== 32)
23
+ throw new Error("Expected exactly 32 bytes");
24
+ return Buffer.from(value);
25
+ }
26
+ export function bool(value) { return Buffer.from([value ? 1 : 0]); }
27
+ export function enumByte(value) { if (!Number.isSafeInteger(value) || value < 0 || value > 255)
28
+ throw new Error("Invalid enum"); return Buffer.from([value]); }
29
+ export class Reader {
30
+ data;
31
+ offset = 0;
32
+ constructor(data) {
33
+ this.data = data;
34
+ }
35
+ bytes(length) { const out = Buffer.from(this.data.subarray(this.offset, this.offset + length)); this.offset += length; return out; }
36
+ pubkey() { return new PublicKey(this.bytes(32)); }
37
+ u8() { return this.bytes(1)[0]; }
38
+ u16() { return this.bytes(2).readUInt16LE(); }
39
+ u64() { return this.bytes(8).readBigUInt64LE(); }
40
+ rest() { return this.data.length - this.offset; }
41
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./encoding.js";
2
+ export * from "./pda.js";
3
+ export * from "./accounts.js";
4
+ export * from "./math.js";
5
+ export * from "./crypto.js";
6
+ export * from "./merkle.js";
7
+ export * from "./types.js";
8
+ export * from "./instructions.js";
9
+ export * from "./client.js";
10
+ export * from "./wallet.js";
11
+ export * from "./prover.js";
12
+ export * from "./witness.js";
13
+ export * from "./store.js";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./encoding.js";
2
+ export * from "./pda.js";
3
+ export * from "./accounts.js";
4
+ export * from "./math.js";
5
+ export * from "./crypto.js";
6
+ export * from "./merkle.js";
7
+ export * from "./types.js";
8
+ export * from "./instructions.js";
9
+ export * from "./client.js";
10
+ export * from "./wallet.js";
11
+ export * from "./prover.js";
12
+ export * from "./witness.js";
13
+ export * from "./store.js";
@@ -0,0 +1,10 @@
1
+ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
+ import type { PoolState, ShieldedState } from "./types.js";
3
+ export declare function initializePool(payer: PublicKey, authority: PublicKey, creator: PublicKey, a: PublicKey, b: PublicKey, feeBps: number, programId?: PublicKey): TransactionInstruction;
4
+ export declare function initializeShieldedState(payer: PublicKey, pool: PublicKey, state?: ShieldedState, programId?: PublicKey): TransactionInstruction;
5
+ export declare function addLiquidity(provider: PublicKey, pool: PoolState, providerA: PublicKey, providerB: PublicKey, providerLp: PublicKey, a: bigint, b: bigint, minLp: bigint, programId?: PublicKey): TransactionInstruction;
6
+ export declare function shield(depositor: PublicKey, pool: PoolState, state: ShieldedState, asset: number, amount: bigint, owner: Uint8Array, randomness: Uint8Array, encrypted: Uint8Array, depositorA: PublicKey, depositorB: PublicKey, programId?: PublicKey): TransactionInstruction;
7
+ export declare function removeLiquidity(provider: PublicKey, pool: PoolState, providerA: PublicKey, providerB: PublicKey, providerLp: PublicKey, lpAmount: bigint, minA: bigint, minB: bigint, programId?: PublicKey): TransactionInstruction;
8
+ export declare function swap(trader: PublicKey, pool: PoolState, traderA: PublicKey, traderB: PublicKey, direction: number, amountIn: bigint, minAmountOut: bigint, programId?: PublicKey): TransactionInstruction;
9
+ export declare function unshield(payer: PublicKey, pool: PoolState, state: ShieldedState, asset: number, amount: bigint, root: Uint8Array, rootSequence: bigint, generation: bigint, nullifierValue: Uint8Array, recipient: PublicKey, recipientA: PublicKey, recipientB: PublicKey, proof: Uint8Array, publicInputs: Uint8Array, programId?: PublicKey): TransactionInstruction;
10
+ export declare function privateSwap(payer: PublicKey, pool: PoolState, state: ShieldedState, direction: number, root: Uint8Array, rootSequence: bigint, generation: bigint, nullifierValue: Uint8Array, amountIn: bigint, amountOut: bigint, changeAmount: bigint, changeCommitment: Uint8Array, outputCommitment: Uint8Array, proof: Uint8Array, programId?: PublicKey): TransactionInstruction;