@soteria1/sdk 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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/dist/confidential/confidential.d.ts +142 -0
  4. package/dist/confidential/confidential.js +266 -0
  5. package/dist/confidential/index.d.ts +1 -0
  6. package/dist/confidential/index.js +1 -0
  7. package/dist/index.d.ts +24 -0
  8. package/dist/index.js +24 -0
  9. package/dist/pool/crypto.d.ts +17 -0
  10. package/dist/pool/crypto.js +79 -0
  11. package/dist/pool/index.d.ts +4 -0
  12. package/dist/pool/index.js +4 -0
  13. package/dist/pool/note.d.ts +22 -0
  14. package/dist/pool/note.js +66 -0
  15. package/dist/pool/pdas.d.ts +54 -0
  16. package/dist/pool/pdas.js +109 -0
  17. package/dist/pool/prover.d.ts +44 -0
  18. package/dist/pool/prover.js +73 -0
  19. package/dist/shielded/index.d.ts +5 -0
  20. package/dist/shielded/index.js +5 -0
  21. package/dist/shielded/instruction.d.ts +20 -0
  22. package/dist/shielded/instruction.js +50 -0
  23. package/dist/shielded/keypair.d.ts +23 -0
  24. package/dist/shielded/keypair.js +72 -0
  25. package/dist/shielded/note.d.ts +22 -0
  26. package/dist/shielded/note.js +32 -0
  27. package/dist/shielded/prover.d.ts +46 -0
  28. package/dist/shielded/prover.js +90 -0
  29. package/dist/shielded/scan.d.ts +21 -0
  30. package/dist/shielded/scan.js +28 -0
  31. package/dist/stealth/index.d.ts +2 -0
  32. package/dist/stealth/index.js +2 -0
  33. package/dist/stealth/scanner.d.ts +20 -0
  34. package/dist/stealth/scanner.js +31 -0
  35. package/dist/stealth/stealth.d.ts +46 -0
  36. package/dist/stealth/stealth.js +119 -0
  37. package/dist/zk/index.d.ts +2 -0
  38. package/dist/zk/index.js +2 -0
  39. package/dist/zk/merkle.d.ts +36 -0
  40. package/dist/zk/merkle.js +92 -0
  41. package/dist/zk/prover.d.ts +50 -0
  42. package/dist/zk/prover.js +87 -0
  43. package/package.json +65 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soteria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @soteria1/sdk
2
+
3
+ Client SDK for **Soteria** — privacy primitives for Solana. Runs in the browser
4
+ (keys never leave the device) or Node.
5
+
6
+ ```bash
7
+ npm install @soteria1/sdk
8
+ ```
9
+
10
+ ## Modules
11
+
12
+ ```ts
13
+ import { shielded, pool, zk, stealth, confidential } from "@soteria1/sdk";
14
+ ```
15
+
16
+ | Module | What it does |
17
+ |--------|--------------|
18
+ | **`shielded`** | Hidden-amount UTXO payments: deposit any amount, pay anyone (amounts encrypted), change + multi-recipient, scan for incoming notes. |
19
+ | **`pool`** | Fixed-denomination compliant privacy pool (ZK deposit/withdraw, association set). |
20
+ | **`zk`** | ZK selective disclosure: prove set membership without revealing identity; Poseidon Merkle tree. |
21
+ | **`stealth`** | Dual-key stealth addresses (one-time receive addresses). |
22
+ | **`confidential`** | Token-2022 confidential-transfer helpers (hidden amounts with an auditor key). |
23
+
24
+ ## Example — a private payment (shielded)
25
+
26
+ ```ts
27
+ import { shielded } from "@soteria1/sdk";
28
+
29
+ // derive a recoverable shielded identity from a wallet signature
30
+ const me = await shielded.deriveShieldedKeypair(signature);
31
+ const address = shielded.encodeShieldedAddress(me); // share this to get paid
32
+
33
+ // build a transfer proof (amounts hidden, change handled)
34
+ const tx = await shielded.buildTransaction({
35
+ inputs, outputs, spendKeypair: me,
36
+ extAmount: 0n, fee: 5000n, recipient, relayer, root,
37
+ wasmPath: "/transaction.wasm", zkeyPath: "/transaction_final.zkey",
38
+ });
39
+
40
+ // find notes paid to you
41
+ const mine = await shielded.scanOutputs(records, me);
42
+ const total = shielded.balance(mine);
43
+ ```
44
+
45
+ Proof artifacts (`transaction.wasm`, `transaction_final.zkey`) come from the
46
+ project's trusted setup (`scripts/setup-transaction.sh`); serve them statically.
47
+
48
+ ## ⚠️ Status
49
+
50
+ Experimental. The ZK circuits are **unaudited** and the trusted setups are
51
+ single-contributor (dev). **Do not use with real funds** without a professional
52
+ audit and a real multi-party ceremony.
53
+
54
+ MIT licensed.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Confidential amounts via the Token-2022 Confidential Transfer extension.
3
+ *
4
+ * Hides transfer AMOUNTS and balances (not the transfer graph) using twisted
5
+ * ElGamal encryption + ZK proofs verified on-chain by Solana's ZK ElGamal Proof
6
+ * program. A mint-level `auditor` ElGamal key can decrypt every amount for
7
+ * compliance — wire it in via `createMint({ auditorElGamalPubkey })`.
8
+ *
9
+ * This module uses the @solana/kit (web3.js v2) stack internally because the
10
+ * confidential-transfer instruction + proof helpers ship for it. Callers work
11
+ * with plain values (base58 addresses, bigint amounts) and the zk-sdk key
12
+ * objects; no kit knowledge is required.
13
+ *
14
+ * Proofs that exceed transaction size (transfer, withdraw) are verified into
15
+ * dedicated context-state accounts, created and torn down automatically by the
16
+ * underlying instruction-plan helpers.
17
+ */
18
+ import { type Address, type KeyPairSigner, type TransactionSigner } from "@solana/kit";
19
+ import { AeKey, ElGamalKeypair } from "@solana/zk-sdk/bundler";
20
+ export { AeKey, ElGamalKeypair };
21
+ /** Encode a 32-byte ElGamal public key as a base58 Address. */
22
+ export declare function elGamalPubkeyToAddress(keypair: ElGamalKeypair): Address;
23
+ /** Signs an arbitrary message and returns a 64-byte ed25519 signature. */
24
+ export type SignMessage = (message: Uint8Array) => Promise<Uint8Array> | Uint8Array;
25
+ export interface AccountKeys {
26
+ elgamalKeypair: ElGamalKeypair;
27
+ aesKey: AeKey;
28
+ }
29
+ /**
30
+ * Deterministically derive an account's confidential keys from the owner's
31
+ * signature over a domain-separated message, bound to `(owner, mint)`.
32
+ *
33
+ * The same `(sign, owner, mint)` always yields the same keys, so an owner can
34
+ * recover them from their wallet alone — nothing secret is ever stored or
35
+ * transmitted, and the binding prevents key reuse across mints. In the browser,
36
+ * pass a wallet adapter's `signMessage`; on a server, sign with the owner's
37
+ * ed25519 secret key.
38
+ */
39
+ export declare function deriveAccountKeys(params: {
40
+ sign: SignMessage;
41
+ owner: Address;
42
+ mint: Address;
43
+ }): Promise<AccountKeys>;
44
+ export interface ConfidentialClientConfig {
45
+ /** Solana JSON-RPC HTTP endpoint, e.g. https://api.devnet.solana.com */
46
+ rpcUrl: string;
47
+ /** Fee payer; signs and pays for every transaction. */
48
+ payer: KeyPairSigner;
49
+ }
50
+ export interface CreateMintParams {
51
+ mint: KeyPairSigner;
52
+ decimals: number;
53
+ mintAuthority: TransactionSigner;
54
+ /** Auditor ElGamal pubkey (compliance). Pass null to run without an auditor. */
55
+ auditorElGamalPubkey?: Address | null;
56
+ /** When true, accounts may transact immediately without authority approval. */
57
+ autoApproveNewAccounts?: boolean;
58
+ }
59
+ export interface ConfigureAccountParams {
60
+ owner: TransactionSigner;
61
+ mint: Address;
62
+ elgamalKeypair: ElGamalKeypair;
63
+ aesKey: AeKey;
64
+ maximumPendingBalanceCreditCounter?: number | bigint;
65
+ }
66
+ export interface DepositParams {
67
+ token: Address;
68
+ mint: Address;
69
+ authority: TransactionSigner;
70
+ amount: bigint;
71
+ decimals: number;
72
+ }
73
+ export interface ApplyPendingParams {
74
+ token: Address;
75
+ authority: TransactionSigner;
76
+ elgamalKeypair: ElGamalKeypair;
77
+ aesKey: AeKey;
78
+ }
79
+ export interface TransferParams {
80
+ source: Address;
81
+ destination: Address;
82
+ mint: Address;
83
+ authority: TransactionSigner;
84
+ amount: bigint;
85
+ sourceElgamalKeypair: ElGamalKeypair;
86
+ sourceAesKey: AeKey;
87
+ auditorElGamalPubkey?: Address;
88
+ }
89
+ export interface WithdrawParams {
90
+ token: Address;
91
+ mint: Address;
92
+ authority: TransactionSigner;
93
+ amount: bigint;
94
+ decimals: number;
95
+ elgamalKeypair: ElGamalKeypair;
96
+ aesKey: AeKey;
97
+ }
98
+ /**
99
+ * A confidential-transfer client bound to one RPC endpoint and fee payer.
100
+ * Each method submits the on-chain transaction(s) for one lifecycle step.
101
+ */
102
+ export declare class ConfidentialClient {
103
+ private readonly rpc;
104
+ private readonly payer;
105
+ private readonly planner;
106
+ private readonly executor;
107
+ constructor(config: ConfidentialClientConfig);
108
+ /** Poll signature status until confirmed (WebSocket-free). */
109
+ private confirm;
110
+ private run;
111
+ /** Fetch a decoded token account, retrying transient RPC failures. */
112
+ private fetchTokenAccount;
113
+ /** Associated token address for an owner on a given mint. */
114
+ associatedTokenAddress(owner: Address, mint: Address): Promise<Address>;
115
+ /** Create a Token-2022 mint with the confidential-transfer extension + auditor. */
116
+ createMint(params: CreateMintParams): Promise<Address>;
117
+ /**
118
+ * Configure a token account for confidential transfers (creates the ATA,
119
+ * reallocates, configures, and verifies the pubkey-validity proof).
120
+ * Returns the associated token address.
121
+ */
122
+ configureAccount(params: ConfigureAccountParams): Promise<Address>;
123
+ /** Mint public (plaintext) tokens to a token account. */
124
+ mintTo(params: {
125
+ mint: Address;
126
+ token: Address;
127
+ mintAuthority: TransactionSigner;
128
+ amount: bigint;
129
+ }): Promise<void>;
130
+ /** Move a public balance into the encrypted pending balance. */
131
+ deposit(params: DepositParams): Promise<void>;
132
+ /** Move the encrypted pending balance into the spendable available balance. */
133
+ applyPending(params: ApplyPendingParams): Promise<void>;
134
+ /** Confidentially transfer an amount between two accounts (amount hidden). */
135
+ transfer(params: TransferParams): Promise<void>;
136
+ /** Move an encrypted available balance back to a public (plaintext) balance. */
137
+ withdraw(params: WithdrawParams): Promise<void>;
138
+ /** The plaintext (public) token amount visible on-chain. */
139
+ getPublicAmount(token: Address): Promise<bigint>;
140
+ /** Decrypt an account's available balance with its owner's AES key. */
141
+ decryptAvailableBalance(token: Address, aesKey: AeKey): Promise<bigint>;
142
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Confidential amounts via the Token-2022 Confidential Transfer extension.
3
+ *
4
+ * Hides transfer AMOUNTS and balances (not the transfer graph) using twisted
5
+ * ElGamal encryption + ZK proofs verified on-chain by Solana's ZK ElGamal Proof
6
+ * program. A mint-level `auditor` ElGamal key can decrypt every amount for
7
+ * compliance — wire it in via `createMint({ auditorElGamalPubkey })`.
8
+ *
9
+ * This module uses the @solana/kit (web3.js v2) stack internally because the
10
+ * confidential-transfer instruction + proof helpers ship for it. Callers work
11
+ * with plain values (base58 addresses, bigint amounts) and the zk-sdk key
12
+ * objects; no kit knowledge is required.
13
+ *
14
+ * Proofs that exceed transaction size (transfer, withdraw) are verified into
15
+ * dedicated context-state accounts, created and torn down automatically by the
16
+ * underlying instruction-plan helpers.
17
+ */
18
+ import { createSolanaRpc, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, getAddressDecoder, getAddressEncoder, getBase64EncodedWireTransaction, getSignatureFromTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, singleInstructionPlan, some, } from "@solana/kit";
19
+ import { TOKEN_2022_PROGRAM_ADDRESS, extension, fetchToken, findAssociatedTokenPda, getConfidentialDepositInstruction, getCreateMintInstructionPlan, getMintToInstruction, } from "@solana-program/token-2022";
20
+ import { getApplyConfidentialPendingBalanceInstructionFromToken, getConfidentialTransferInstructionPlan, getConfidentialWithdrawInstructionPlan, getCreateConfidentialTransferAccountInstructionPlan, } from "@solana-program/token-2022/confidential";
21
+ import { AeCiphertext, AeKey, ElGamalKeypair } from "@solana/zk-sdk/bundler";
22
+ export { AeKey, ElGamalKeypair };
23
+ /** Encode a 32-byte ElGamal public key as a base58 Address. */
24
+ export function elGamalPubkeyToAddress(keypair) {
25
+ return getAddressDecoder().decode(keypair.pubkey().toBytes());
26
+ }
27
+ /**
28
+ * Deterministically derive an account's confidential keys from the owner's
29
+ * signature over a domain-separated message, bound to `(owner, mint)`.
30
+ *
31
+ * The same `(sign, owner, mint)` always yields the same keys, so an owner can
32
+ * recover them from their wallet alone — nothing secret is ever stored or
33
+ * transmitted, and the binding prevents key reuse across mints. In the browser,
34
+ * pass a wallet adapter's `signMessage`; on a server, sign with the owner's
35
+ * ed25519 secret key.
36
+ */
37
+ export async function deriveAccountKeys(params) {
38
+ const encode = getAddressEncoder();
39
+ const seed = new Uint8Array([
40
+ ...encode.encode(params.owner),
41
+ ...encode.encode(params.mint),
42
+ ]);
43
+ const elgamalKeypair = ElGamalKeypair.fromSignature(await params.sign(Uint8Array.from(ElGamalKeypair.signerMessage(seed))));
44
+ const aesKey = AeKey.fromSignature(await params.sign(Uint8Array.from(AeKey.signerMessage(seed))));
45
+ return { elgamalKeypair, aesKey };
46
+ }
47
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
48
+ /** Retry transient RPC failures (rate limits, network blips) with backoff. */
49
+ async function withRetry(fn, attempts = 8) {
50
+ let delay = 500;
51
+ let lastErr;
52
+ for (let i = 0; i < attempts; i++) {
53
+ try {
54
+ return await fn();
55
+ }
56
+ catch (err) {
57
+ lastErr = err;
58
+ const msg = String(err?.message ?? err);
59
+ const retriable = /429|Too Many Requests|fetch failed|ENOTFOUND|ETIMEDOUT|50[023]|socket|network/i.test(msg);
60
+ if (!retriable || i === attempts - 1)
61
+ throw err;
62
+ await sleep(delay);
63
+ delay = Math.min(delay * 2, 8000);
64
+ }
65
+ }
66
+ throw lastErr;
67
+ }
68
+ /**
69
+ * A confidential-transfer client bound to one RPC endpoint and fee payer.
70
+ * Each method submits the on-chain transaction(s) for one lifecycle step.
71
+ */
72
+ export class ConfidentialClient {
73
+ constructor(config) {
74
+ const { rpcUrl, payer } = config;
75
+ this.payer = payer;
76
+ this.rpc = createSolanaRpc(rpcUrl);
77
+ this.planner = createTransactionPlanner({
78
+ createTransactionMessage: () => pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(payer, m)),
79
+ });
80
+ // Confirm via RPC polling rather than a WebSocket subscription: devnet WS
81
+ // is flaky under the transfer's parallel proof sends, and polling works
82
+ // identically in Node and the browser.
83
+ this.executor = createTransactionPlanExecutor({
84
+ executeTransactionMessage: async (_ctx, message) => {
85
+ const { value: blockhash } = await withRetry(() => this.rpc.getLatestBlockhash().send());
86
+ const signed = await signTransactionMessageWithSigners(setTransactionMessageLifetimeUsingBlockhash(blockhash, message));
87
+ const signature = getSignatureFromTransaction(signed);
88
+ const wire = getBase64EncodedWireTransaction(signed);
89
+ await withRetry(() => this.rpc
90
+ .sendTransaction(wire, {
91
+ encoding: "base64",
92
+ preflightCommitment: "confirmed",
93
+ })
94
+ .send());
95
+ await this.confirm(signature);
96
+ return signature;
97
+ },
98
+ });
99
+ }
100
+ /** Poll signature status until confirmed (WebSocket-free). */
101
+ async confirm(signature, timeoutMs = 60000) {
102
+ const deadline = Date.now() + timeoutMs;
103
+ while (Date.now() < deadline) {
104
+ const { value } = await withRetry(() => this.rpc
105
+ .getSignatureStatuses([
106
+ signature,
107
+ ])
108
+ .send());
109
+ const status = value[0];
110
+ if (status) {
111
+ if (status.err) {
112
+ throw new Error(`transaction ${signature} failed: ${JSON.stringify(status.err)}`);
113
+ }
114
+ if (status.confirmationStatus === "confirmed" ||
115
+ status.confirmationStatus === "finalized") {
116
+ return;
117
+ }
118
+ }
119
+ await sleep(1500);
120
+ }
121
+ throw new Error(`timed out confirming transaction ${signature}`);
122
+ }
123
+ async run(plan) {
124
+ await this.executor(await this.planner(plan));
125
+ }
126
+ /** Fetch a decoded token account, retrying transient RPC failures. */
127
+ async fetchTokenAccount(token) {
128
+ return (await withRetry(() => fetchToken(this.rpc, token))).data;
129
+ }
130
+ /** Associated token address for an owner on a given mint. */
131
+ async associatedTokenAddress(owner, mint) {
132
+ const [pda] = await findAssociatedTokenPda({
133
+ owner,
134
+ tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
135
+ mint,
136
+ });
137
+ return pda;
138
+ }
139
+ /** Create a Token-2022 mint with the confidential-transfer extension + auditor. */
140
+ async createMint(params) {
141
+ const { mint, decimals, mintAuthority, auditorElGamalPubkey = null, autoApproveNewAccounts = true, } = params;
142
+ await this.run(getCreateMintInstructionPlan({
143
+ payer: this.payer,
144
+ newMint: mint,
145
+ decimals,
146
+ mintAuthority,
147
+ extensions: [
148
+ extension("ConfidentialTransferMint", {
149
+ authority: some(mintAuthority.address),
150
+ autoApproveNewAccounts,
151
+ auditorElgamalPubkey: auditorElGamalPubkey
152
+ ? some(auditorElGamalPubkey)
153
+ : null,
154
+ }),
155
+ ],
156
+ }));
157
+ return mint.address;
158
+ }
159
+ /**
160
+ * Configure a token account for confidential transfers (creates the ATA,
161
+ * reallocates, configures, and verifies the pubkey-validity proof).
162
+ * Returns the associated token address.
163
+ */
164
+ async configureAccount(params) {
165
+ const { owner, mint, elgamalKeypair, aesKey } = params;
166
+ await this.run(await getCreateConfidentialTransferAccountInstructionPlan({
167
+ payer: this.payer,
168
+ owner,
169
+ mint,
170
+ rpc: this.rpc,
171
+ elgamalKeypair,
172
+ aesKey,
173
+ maximumPendingBalanceCreditCounter: params.maximumPendingBalanceCreditCounter,
174
+ }));
175
+ return this.associatedTokenAddress(owner.address, mint);
176
+ }
177
+ /** Mint public (plaintext) tokens to a token account. */
178
+ async mintTo(params) {
179
+ await this.run(singleInstructionPlan(getMintToInstruction({
180
+ mint: params.mint,
181
+ token: params.token,
182
+ mintAuthority: params.mintAuthority,
183
+ amount: params.amount,
184
+ })));
185
+ }
186
+ /** Move a public balance into the encrypted pending balance. */
187
+ async deposit(params) {
188
+ await this.run(singleInstructionPlan(getConfidentialDepositInstruction({
189
+ token: params.token,
190
+ mint: params.mint,
191
+ authority: params.authority,
192
+ amount: params.amount,
193
+ decimals: params.decimals,
194
+ })));
195
+ }
196
+ /** Move the encrypted pending balance into the spendable available balance. */
197
+ async applyPending(params) {
198
+ const account = await this.fetchTokenAccount(params.token);
199
+ await this.run(singleInstructionPlan(getApplyConfidentialPendingBalanceInstructionFromToken({
200
+ token: params.token,
201
+ tokenAccount: account,
202
+ authority: params.authority,
203
+ elgamalSecretKey: params.elgamalKeypair.secret(),
204
+ aesKey: params.aesKey,
205
+ })));
206
+ }
207
+ /** Confidentially transfer an amount between two accounts (amount hidden). */
208
+ async transfer(params) {
209
+ const sourceAccount = await this.fetchTokenAccount(params.source);
210
+ const destinationAccount = await this.fetchTokenAccount(params.destination);
211
+ await this.run(await getConfidentialTransferInstructionPlan({
212
+ sourceToken: params.source,
213
+ mint: params.mint,
214
+ destinationToken: params.destination,
215
+ sourceTokenAccount: sourceAccount,
216
+ destinationTokenAccount: destinationAccount,
217
+ auditorElgamalPubkey: params.auditorElGamalPubkey,
218
+ authority: params.authority,
219
+ amount: params.amount,
220
+ sourceElgamalKeypair: params.sourceElgamalKeypair,
221
+ aesKey: params.sourceAesKey,
222
+ payer: this.payer,
223
+ rpc: this.rpc,
224
+ }));
225
+ }
226
+ /** Move an encrypted available balance back to a public (plaintext) balance. */
227
+ async withdraw(params) {
228
+ const account = await this.fetchTokenAccount(params.token);
229
+ await this.run(await getConfidentialWithdrawInstructionPlan({
230
+ token: params.token,
231
+ mint: params.mint,
232
+ tokenAccount: account,
233
+ authority: params.authority,
234
+ amount: params.amount,
235
+ decimals: params.decimals,
236
+ elgamalKeypair: params.elgamalKeypair,
237
+ aesKey: params.aesKey,
238
+ payer: this.payer,
239
+ rpc: this.rpc,
240
+ }));
241
+ }
242
+ /** The plaintext (public) token amount visible on-chain. */
243
+ async getPublicAmount(token) {
244
+ return (await this.fetchTokenAccount(token)).amount;
245
+ }
246
+ /** Decrypt an account's available balance with its owner's AES key. */
247
+ async decryptAvailableBalance(token, aesKey) {
248
+ const account = await this.fetchTokenAccount(token);
249
+ // `extensions` is a kit Option<Extension[]>; unwrap defensively (shape is
250
+ // dynamic on-chain data, so we read it untyped).
251
+ const raw = account.extensions;
252
+ const list = Array.isArray(raw)
253
+ ? raw
254
+ : raw && raw.__option === "Some"
255
+ ? raw.value
256
+ : [];
257
+ const ct = list.find((e) => e.__kind === "ConfidentialTransferAccount");
258
+ if (!ct) {
259
+ throw new Error("account is not configured for confidential transfers");
260
+ }
261
+ const cipher = AeCiphertext.fromBytes(Uint8Array.from(ct.decryptableAvailableBalance));
262
+ if (!cipher)
263
+ throw new Error("failed to decode available-balance ciphertext");
264
+ return aesKey.decrypt(cipher);
265
+ }
266
+ }
@@ -0,0 +1 @@
1
+ export * from "./confidential.js";
@@ -0,0 +1 @@
1
+ export * from "./confidential.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Soteria SDK — privacy primitives for Solana.
3
+ *
4
+ * zk Selective disclosure: prove membership/eligibility without
5
+ * revealing which identity. (mainnet-ready)
6
+ * stealth One-time receive addresses so a main wallet isn't exposed.
7
+ * (mainnet-ready)
8
+ * confidential Token-2022 confidential amounts with an auditor key.
9
+ * (verified end-to-end on devnet via ConfidentialClient)
10
+ * pool Compliant fixed-denomination privacy pool: ZK deposit/withdraw
11
+ * that severs the on-chain link, gated by an association set and
12
+ * auditable via a curated root. (path C — needs setup-pool.sh)
13
+ * shielded Hidden-amount UTXO pool (Option B): arbitrary amounts, partial
14
+ * spends, change, multi-recipient — values stay encrypted.
15
+ * (needs setup-transaction.sh; verified on devnet)
16
+ *
17
+ * Every privacy feature keeps a disclosure/audit path: the pool is gated by an
18
+ * association set rather than being an unconditional tumbler.
19
+ */
20
+ export * as zk from "./zk/index.js";
21
+ export * as stealth from "./stealth/index.js";
22
+ export * as confidential from "./confidential/index.js";
23
+ export * as pool from "./pool/index.js";
24
+ export * as shielded from "./shielded/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Soteria SDK — privacy primitives for Solana.
3
+ *
4
+ * zk Selective disclosure: prove membership/eligibility without
5
+ * revealing which identity. (mainnet-ready)
6
+ * stealth One-time receive addresses so a main wallet isn't exposed.
7
+ * (mainnet-ready)
8
+ * confidential Token-2022 confidential amounts with an auditor key.
9
+ * (verified end-to-end on devnet via ConfidentialClient)
10
+ * pool Compliant fixed-denomination privacy pool: ZK deposit/withdraw
11
+ * that severs the on-chain link, gated by an association set and
12
+ * auditable via a curated root. (path C — needs setup-pool.sh)
13
+ * shielded Hidden-amount UTXO pool (Option B): arbitrary amounts, partial
14
+ * spends, change, multi-recipient — values stay encrypted.
15
+ * (needs setup-transaction.sh; verified on devnet)
16
+ *
17
+ * Every privacy feature keeps a disclosure/audit path: the pool is gated by an
18
+ * association set rather than being an unconditional tumbler.
19
+ */
20
+ export * as zk from "./zk/index.js";
21
+ export * as stealth from "./stealth/index.js";
22
+ export * as confidential from "./confidential/index.js";
23
+ export * as pool from "./pool/index.js";
24
+ export * as shielded from "./shielded/index.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Derive a recipient's X25519 receiving keypair from a 32+ byte seed — e.g. the
3
+ * bytes of a wallet signature over a fixed message, so the identity is
4
+ * recoverable from the wallet alone and nothing is ever stored.
5
+ */
6
+ export declare function receiveKeypairFromSeed(seed: Uint8Array): {
7
+ priv: Uint8Array;
8
+ pub: Uint8Array;
9
+ };
10
+ /** Shareable encoding of a receiving public key (a "private-payment address"). */
11
+ export declare function encodeReceiveAddress(pub: Uint8Array): string;
12
+ export declare function decodeReceiveAddress(s: string): Uint8Array;
13
+ /** Encrypt a note string to a recipient's receiving public key. */
14
+ export declare function encryptNote(noteStr: string, recipientPub: Uint8Array): Promise<string>;
15
+ export declare function isEncryptedNote(s: string): boolean;
16
+ /** Decrypt an encrypted note with the recipient's receiving private key. */
17
+ export declare function decryptNote(blobStr: string, recipientPriv: Uint8Array): Promise<string>;
@@ -0,0 +1,79 @@
1
+ import { x25519 } from "@noble/curves/ed25519";
2
+ import { hkdf } from "@noble/hashes/hkdf";
3
+ import { sha256 } from "@noble/hashes/sha256";
4
+ // Encrypt a pool note to a recipient so a claim link can travel over a public
5
+ // channel: only the holder of the matching private key can decrypt it. ECIES =
6
+ // ephemeral X25519 ECDH -> HKDF-SHA256 -> AES-256-GCM. No extra deps (WebCrypto
7
+ // is present in the browser and in Node 20+).
8
+ const ENC_PREFIX = "soteria-enc-v1";
9
+ function b64url(bytes) {
10
+ let s = "";
11
+ for (const b of bytes)
12
+ s += String.fromCharCode(b);
13
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
+ }
15
+ function unb64url(s) {
16
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
17
+ const pad = "=".repeat((4 - (b64.length % 4)) % 4);
18
+ const bin = atob(b64 + pad);
19
+ const out = new Uint8Array(bin.length);
20
+ for (let i = 0; i < bin.length; i++)
21
+ out[i] = bin.charCodeAt(i);
22
+ return out;
23
+ }
24
+ // A concrete ArrayBuffer copy, so the WebCrypto BufferSource types don't trip on
25
+ // the ArrayBufferLike/SharedArrayBuffer generic.
26
+ function buf(u) {
27
+ return u.slice().buffer;
28
+ }
29
+ /**
30
+ * Derive a recipient's X25519 receiving keypair from a 32+ byte seed — e.g. the
31
+ * bytes of a wallet signature over a fixed message, so the identity is
32
+ * recoverable from the wallet alone and nothing is ever stored.
33
+ */
34
+ export function receiveKeypairFromSeed(seed) {
35
+ const priv = sha256(seed); // 32 bytes; X25519 clamps internally
36
+ return { priv, pub: x25519.getPublicKey(priv) };
37
+ }
38
+ /** Shareable encoding of a receiving public key (a "private-payment address"). */
39
+ export function encodeReceiveAddress(pub) {
40
+ return b64url(pub);
41
+ }
42
+ export function decodeReceiveAddress(s) {
43
+ const pub = unb64url(s.trim());
44
+ if (pub.length !== 32)
45
+ throw new Error("invalid private-payment address");
46
+ return pub;
47
+ }
48
+ async function aesKey(shared) {
49
+ const raw = hkdf(sha256, shared, undefined, "soteria-pool-note", 32);
50
+ return crypto.subtle.importKey("raw", buf(raw), "AES-GCM", false, ["encrypt", "decrypt"]);
51
+ }
52
+ /** Encrypt a note string to a recipient's receiving public key. */
53
+ export async function encryptNote(noteStr, recipientPub) {
54
+ const eph = x25519.utils.randomPrivateKey();
55
+ const ephPub = x25519.getPublicKey(eph);
56
+ const shared = x25519.getSharedSecret(eph, recipientPub);
57
+ const key = await aesKey(shared);
58
+ const iv = crypto.getRandomValues(new Uint8Array(12));
59
+ const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, buf(new TextEncoder().encode(noteStr))));
60
+ const blob = new Uint8Array(32 + 12 + ct.length);
61
+ blob.set(ephPub, 0);
62
+ blob.set(iv, 32);
63
+ blob.set(ct, 44);
64
+ return `${ENC_PREFIX}:${b64url(blob)}`;
65
+ }
66
+ export function isEncryptedNote(s) {
67
+ return s.trim().startsWith(ENC_PREFIX + ":");
68
+ }
69
+ /** Decrypt an encrypted note with the recipient's receiving private key. */
70
+ export async function decryptNote(blobStr, recipientPriv) {
71
+ const blob = unb64url(blobStr.trim().slice(ENC_PREFIX.length + 1));
72
+ const ephPub = blob.slice(0, 32);
73
+ const iv = blob.slice(32, 44);
74
+ const ct = blob.slice(44);
75
+ const shared = x25519.getSharedSecret(recipientPriv, ephPub);
76
+ const key = await aesKey(shared);
77
+ const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf(iv) }, key, buf(ct));
78
+ return new TextDecoder().decode(pt);
79
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./note.js";
2
+ export * from "./prover.js";
3
+ export * from "./pdas.js";
4
+ export * from "./crypto.js";
@@ -0,0 +1,4 @@
1
+ export * from "./note.js";
2
+ export * from "./prover.js";
3
+ export * from "./pdas.js";
4
+ export * from "./crypto.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A pool note. Whoever holds (nullifier, secret) can withdraw the deposit whose
3
+ * commitment = Poseidon(nullifier, secret). Losing it loses the funds — there is
4
+ * no server-side copy.
5
+ */
6
+ export interface Note {
7
+ poolId: bigint;
8
+ nullifier: bigint;
9
+ secret: bigint;
10
+ }
11
+ /** Generate a fresh note for a pool. */
12
+ export declare function randomNote(poolId: bigint | number): Note;
13
+ /** Note commitment = Poseidon(nullifier, secret) — the tree leaf. */
14
+ export declare function commitment(note: Note): Promise<bigint>;
15
+ /** nullifierHash = Poseidon(nullifier) — revealed at withdraw, public. */
16
+ export declare function nullifierHash(note: Note): Promise<bigint>;
17
+ /** Serialize a note to the backup string the user must save to claim funds. */
18
+ export declare function encodeNote(note: Note): string;
19
+ /** Parse a backup string back into a note. */
20
+ export declare function decodeNote(s: string): Note;
21
+ /** 32-byte big-endian encoding of a field element (tree leaf / PDA seed). */
22
+ export declare function toBytes32(v: bigint): Uint8Array;