@veilo/sdk-core 0.3.3 → 0.4.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 (195) hide show
  1. package/README.md +42 -3
  2. package/SHIELD_INTEGRATION.md +143 -0
  3. package/config.d.ts +2 -0
  4. package/config.js +4 -0
  5. package/dist/cjs/client.d.ts +5 -5
  6. package/dist/cjs/client.js +79 -42
  7. package/dist/cjs/compactNote.d.ts +107 -0
  8. package/dist/cjs/compactNote.js +167 -0
  9. package/dist/cjs/events.d.ts +1 -1
  10. package/dist/cjs/events.js +2 -2
  11. package/dist/cjs/idl/privacy_pool.d.ts +5 -0
  12. package/dist/cjs/idl/privacy_pool.js +15218 -0
  13. package/dist/cjs/index.d.ts +16 -13
  14. package/dist/cjs/index.js +40 -30
  15. package/dist/cjs/merkle.d.ts +13 -0
  16. package/dist/cjs/merkle.js +31 -8
  17. package/dist/cjs/program.d.ts +11 -0
  18. package/dist/cjs/program.js +26 -3
  19. package/dist/cjs/proof.d.ts +1 -1
  20. package/dist/cjs/proof.js +37 -37
  21. package/dist/cjs/prover.d.ts +1 -1
  22. package/dist/cjs/prover.js +2 -2
  23. package/dist/cjs/random.d.ts +16 -0
  24. package/dist/cjs/random.js +28 -0
  25. package/dist/cjs/relayer.d.ts +27 -4
  26. package/dist/cjs/relayer.js +16 -5
  27. package/dist/cjs/shield/alt.d.ts +87 -0
  28. package/dist/cjs/shield/alt.js +194 -0
  29. package/dist/cjs/shield/computeBudget.d.ts +61 -0
  30. package/dist/cjs/shield/computeBudget.js +64 -0
  31. package/dist/cjs/shield/errors.d.ts +58 -0
  32. package/dist/cjs/shield/errors.js +121 -0
  33. package/dist/cjs/shield/finalize.d.ts +45 -0
  34. package/dist/cjs/shield/finalize.js +119 -0
  35. package/dist/cjs/shield/index.d.ts +35 -0
  36. package/dist/cjs/shield/index.js +68 -0
  37. package/dist/cjs/shield/ix.d.ts +54 -0
  38. package/dist/cjs/shield/ix.js +119 -0
  39. package/dist/cjs/shield/owner.d.ts +36 -0
  40. package/dist/cjs/shield/owner.js +126 -0
  41. package/dist/cjs/shield/ports.d.ts +43 -0
  42. package/dist/cjs/shield/ports.js +153 -0
  43. package/dist/cjs/shield/preflight.d.ts +30 -0
  44. package/dist/cjs/shield/preflight.js +154 -0
  45. package/dist/cjs/shield/shield.d.ts +68 -0
  46. package/dist/cjs/shield/shield.js +499 -0
  47. package/dist/cjs/shield/types.d.ts +202 -0
  48. package/dist/cjs/shield/types.js +2 -0
  49. package/dist/cjs/utxo.d.ts +20 -0
  50. package/dist/cjs/utxo.js +41 -28
  51. package/dist/esm/client.d.ts +407 -0
  52. package/dist/esm/client.js +9 -5
  53. package/dist/esm/compactNote.d.ts +107 -0
  54. package/dist/esm/compactNote.js +156 -0
  55. package/dist/esm/config.d.ts +82 -0
  56. package/dist/esm/events.d.ts +77 -0
  57. package/dist/esm/events.js +1 -1
  58. package/dist/esm/idl/privacy_pool.d.ts +5 -0
  59. package/dist/esm/idl/privacy_pool.js +15216 -0
  60. package/dist/esm/index.d.ts +16 -0
  61. package/dist/esm/index.js +18 -11
  62. package/dist/esm/merkle.d.ts +77 -0
  63. package/dist/esm/merkle.js +23 -1
  64. package/dist/esm/package.json +1 -0
  65. package/dist/esm/poseidon.d.ts +29 -0
  66. package/dist/esm/program.d.ts +37 -0
  67. package/dist/esm/program.js +23 -1
  68. package/dist/esm/proof.d.ts +183 -0
  69. package/dist/esm/proof.js +2 -2
  70. package/dist/esm/prover.d.ts +54 -0
  71. package/dist/esm/prover.js +1 -1
  72. package/dist/esm/random.d.ts +16 -0
  73. package/dist/esm/random.js +21 -0
  74. package/dist/esm/relayer.d.ts +318 -0
  75. package/dist/esm/relayer.js +16 -5
  76. package/dist/esm/retry.d.ts +32 -0
  77. package/dist/esm/shield/alt.d.ts +87 -0
  78. package/dist/esm/shield/alt.js +186 -0
  79. package/dist/esm/shield/computeBudget.d.ts +61 -0
  80. package/dist/esm/shield/computeBudget.js +61 -0
  81. package/dist/esm/shield/errors.d.ts +58 -0
  82. package/dist/esm/shield/errors.js +115 -0
  83. package/dist/esm/shield/finalize.d.ts +45 -0
  84. package/dist/esm/shield/finalize.js +83 -0
  85. package/dist/esm/shield/index.d.ts +35 -0
  86. package/dist/esm/shield/index.js +32 -0
  87. package/dist/esm/shield/ix.d.ts +54 -0
  88. package/dist/esm/shield/ix.js +82 -0
  89. package/dist/esm/shield/owner.d.ts +36 -0
  90. package/dist/esm/shield/owner.js +122 -0
  91. package/dist/esm/shield/ports.d.ts +43 -0
  92. package/dist/esm/shield/ports.js +147 -0
  93. package/dist/esm/shield/preflight.d.ts +30 -0
  94. package/dist/esm/shield/preflight.js +151 -0
  95. package/dist/esm/shield/shield.d.ts +68 -0
  96. package/dist/esm/shield/shield.js +492 -0
  97. package/dist/esm/shield/types.d.ts +202 -0
  98. package/dist/esm/shield/types.js +1 -0
  99. package/dist/esm/utxo.d.ts +235 -0
  100. package/dist/esm/utxo.js +18 -8
  101. package/dist/src/client.d.ts +407 -0
  102. package/dist/src/client.js +951 -0
  103. package/dist/src/compactNote.d.ts +107 -0
  104. package/dist/src/compactNote.js +167 -0
  105. package/dist/src/config.d.ts +82 -0
  106. package/dist/src/config.js +57 -0
  107. package/dist/src/events.d.ts +77 -0
  108. package/dist/src/events.js +167 -0
  109. package/dist/src/idl/privacy_pool.d.ts +5 -0
  110. package/dist/src/idl/privacy_pool.js +15218 -0
  111. package/dist/src/index.d.ts +16 -0
  112. package/dist/src/index.js +67 -0
  113. package/dist/src/merkle.d.ts +77 -0
  114. package/dist/src/merkle.js +156 -0
  115. package/dist/src/poseidon.d.ts +29 -0
  116. package/dist/src/poseidon.js +100 -0
  117. package/dist/src/program.d.ts +37 -0
  118. package/dist/src/program.js +61 -0
  119. package/dist/src/proof.d.ts +183 -0
  120. package/dist/src/proof.js +292 -0
  121. package/dist/src/prover.d.ts +54 -0
  122. package/dist/src/prover.js +112 -0
  123. package/dist/src/random.d.ts +16 -0
  124. package/dist/src/random.js +28 -0
  125. package/dist/src/relayer.d.ts +318 -0
  126. package/dist/src/relayer.js +257 -0
  127. package/dist/src/retry.d.ts +32 -0
  128. package/dist/src/retry.js +75 -0
  129. package/dist/src/shield/alt.d.ts +87 -0
  130. package/dist/src/shield/alt.js +194 -0
  131. package/dist/src/shield/computeBudget.d.ts +61 -0
  132. package/dist/src/shield/computeBudget.js +64 -0
  133. package/dist/src/shield/errors.d.ts +58 -0
  134. package/dist/src/shield/errors.js +121 -0
  135. package/dist/src/shield/finalize.d.ts +45 -0
  136. package/dist/src/shield/finalize.js +119 -0
  137. package/dist/src/shield/index.d.ts +35 -0
  138. package/dist/src/shield/index.js +68 -0
  139. package/dist/src/shield/ix.d.ts +54 -0
  140. package/dist/src/shield/ix.js +119 -0
  141. package/dist/src/shield/owner.d.ts +36 -0
  142. package/dist/src/shield/owner.js +126 -0
  143. package/dist/src/shield/ports.d.ts +43 -0
  144. package/dist/src/shield/ports.js +153 -0
  145. package/dist/src/shield/preflight.d.ts +30 -0
  146. package/dist/src/shield/preflight.js +154 -0
  147. package/dist/src/shield/shield.d.ts +68 -0
  148. package/dist/src/shield/shield.js +499 -0
  149. package/dist/src/shield/types.d.ts +202 -0
  150. package/dist/src/shield/types.js +2 -0
  151. package/dist/src/utxo.d.ts +235 -0
  152. package/dist/src/utxo.js +407 -0
  153. package/dist/tests/compact-note.test.d.ts +1 -0
  154. package/dist/tests/compact-note.test.js +173 -0
  155. package/dist/tests/config.test.d.ts +1 -0
  156. package/dist/tests/config.test.js +102 -0
  157. package/dist/tests/edge-cases.test.d.ts +1 -0
  158. package/dist/tests/edge-cases.test.js +220 -0
  159. package/dist/tests/encryption.test.d.ts +1 -0
  160. package/dist/tests/encryption.test.js +215 -0
  161. package/dist/tests/events.test.d.ts +1 -0
  162. package/dist/tests/events.test.js +78 -0
  163. package/dist/tests/multi-tree.test.d.ts +1 -0
  164. package/dist/tests/multi-tree.test.js +405 -0
  165. package/dist/tests/pda.test.d.ts +1 -0
  166. package/dist/tests/pda.test.js +229 -0
  167. package/dist/tests/poseidon-builder-parity.test.d.ts +1 -0
  168. package/dist/tests/poseidon-builder-parity.test.js +72 -0
  169. package/dist/tests/poseidon.test.d.ts +1 -0
  170. package/dist/tests/poseidon.test.js +142 -0
  171. package/dist/tests/proof.test.d.ts +1 -0
  172. package/dist/tests/proof.test.js +296 -0
  173. package/dist/tests/relayer.test.d.ts +1 -0
  174. package/dist/tests/relayer.test.js +271 -0
  175. package/dist/tests/sdk.integration.test.d.ts +1 -0
  176. package/dist/tests/sdk.integration.test.js +330 -0
  177. package/dist/tests/shield-owner.test.d.ts +1 -0
  178. package/dist/tests/shield-owner.test.js +89 -0
  179. package/dist/tests/shield-preflight.test.d.ts +1 -0
  180. package/dist/tests/shield-preflight.test.js +87 -0
  181. package/dist/tests/shield-realproof.test.d.ts +1 -0
  182. package/dist/tests/shield-realproof.test.js +272 -0
  183. package/dist/tests/shield.test.d.ts +1 -0
  184. package/dist/tests/shield.test.js +403 -0
  185. package/dist/tests/utxo.test.d.ts +1 -0
  186. package/dist/tests/utxo.test.js +140 -0
  187. package/package.json +69 -11
  188. package/poseidon.d.ts +2 -0
  189. package/poseidon.js +4 -0
  190. package/proof.d.ts +2 -0
  191. package/proof.js +4 -0
  192. package/prover.d.ts +2 -0
  193. package/prover.js +4 -0
  194. package/shield.d.ts +2 -0
  195. package/shield.js +4 -0
@@ -0,0 +1,492 @@
1
+ import { ComputeBudgetProgram, PublicKey, SystemProgram, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
2
+ import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from "@solana/spl-token";
3
+ import { PRIVACY_POOL_PROGRAM_ID } from "../program.js";
4
+ import { getGlobalConfigPda, getNoteTreePda, getNullifierMarkerPda, getPoolPdas, } from "../client.js";
5
+ import { MERKLE_TREE_DEPTH, NATIVE_SOL_MINT } from "../config.js";
6
+ import { bigIntToBytesBE, bytesToBigIntBE, BN254_FR_MODULUS, } from "../poseidon.js";
7
+ import { poseidonZeroChain } from "../merkle.js";
8
+ import { createUTXO, createOwnedZeroUTXO, generateKeypair, } from "../utxo.js";
9
+ import { computeExtDataHash, encodeSnarkjsProofToTransactionProof, prepareTransactionInputs, } from "../proof.js";
10
+ import { createCompactNoteCipher, emptyNoteCipher } from "../compactNote.js";
11
+ import { buildTransactInstruction } from "./ix.js";
12
+ import { sharedDepositAlt } from "./alt.js";
13
+ import { memoryCache, onChainTreeSource } from "./ports.js";
14
+ import { preflightShield } from "./preflight.js";
15
+ import { ShieldError, mapShieldError } from "./errors.js";
16
+ import { DEFAULT_DEADLINE_SECONDS, MAX_TRANSACTION_BYTES, NULLIFIER_MARKERS_PER_TRANSACT, NULLIFIER_MARKER_ACCOUNT_BYTES, SHIELD_COMPUTE_UNIT_LIMIT, SHIELD_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS, SIGNATURE_BYTES, } from "./computeBudget.js";
17
+ /**
18
+ * Build an unsigned transaction that shields public funds into the privacy pool.
19
+ *
20
+ * ## The contract
21
+ *
22
+ * This function builds. It does not sign, submit, confirm, or retry — those
23
+ * belong to whoever holds the key, exactly as with Jupiter's `/swap`. The
24
+ * returned `transaction` is unsigned; the caller signs it with whatever wallet
25
+ * they have and broadcasts it themselves.
26
+ *
27
+ * ## Why this can be handed to a third party
28
+ *
29
+ * Three properties make it safe to build a shield on behalf of a signer you do
30
+ * not control, and to shield to an owner who is not the signer:
31
+ *
32
+ * 1. **Deposits are permissionless.** The program gates its relayer allowlist
33
+ * to `public_amount <= 0`, so any wallet can sign a deposit.
34
+ * 2. **No secrets are involved in building.** The proof for a deposit has no
35
+ * real inputs, and the output commitment binds only the recipient's *public*
36
+ * keys.
37
+ * 3. **The note rides on-chain.** `note_ciphers` travels inside the instruction,
38
+ * so the recipient can find the note by scanning even if the platform that
39
+ * submitted it never says a word.
40
+ *
41
+ * ## What the signer unavoidably pays
42
+ *
43
+ * The signer is the fee payer, the funding source, and the payer of two
44
+ * `NullifierMarker` rents (~0.00192 SOL, permanently burned — the program
45
+ * hardcodes `payer = relayer` on both). A wallet with zero SOL cannot shield
46
+ * through this path no matter who submits the transaction. Fee sponsorship is
47
+ * not achievable here: a second signature costs ~96 bytes against a budget with
48
+ * ~69 to spare, and would not cover the rent anyway.
49
+ */
50
+ export async function shield(params) {
51
+ const { connection, amount, owner, signer, prover, mint = NATIVE_SOL_MINT, programId = PRIVACY_POOL_PROGRAM_ID, computeUnitLimit = SHIELD_COMPUTE_UNIT_LIMIT, computeUnitPriceMicroLamports = SHIELD_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS, deadlineSeconds = DEFAULT_DEADLINE_SECONDS, } = params;
52
+ if (amount <= 0n) {
53
+ throw new ShieldError("UNKNOWN", `shield amount must be positive, got ${amount}`);
54
+ }
55
+ // The owner invariant cannot be verified by anything on-chain or in the type
56
+ // system: if `veiloPublicKey` and `noteViewingKey` belong to different people
57
+ // the note is spendable-but-invisible and the funds are gone from the owner's
58
+ // view with no error anywhere. `resolveShieldOwner()` sets this flag because
59
+ // it fetches both halves from one response and can therefore guarantee they
60
+ // match. Anyone hand-building an owner is asserting they did the same.
61
+ if (!owner.acknowledgeOwnerInvariant) {
62
+ throw new ShieldError("OWNER_INVARIANT_UNACKNOWLEDGED", "ShieldOwner was constructed by hand. `veiloPublicKey` (spend authority) and " +
63
+ "`noteViewingKey` (detection authority) MUST belong to the same account — if " +
64
+ "they do not, the note is spendable but permanently invisible to its owner. " +
65
+ "Use resolveShieldOwner() to obtain both from a single source, or set " +
66
+ "`acknowledgeOwnerInvariant: true` if you have verified them yourself.");
67
+ }
68
+ const cache = params.cache ?? memoryCache();
69
+ const treeSource = params.treeSource ?? onChainTreeSource(connection, programId);
70
+ const altSource = params.alt ?? sharedDepositAlt(undefined, { cache });
71
+ const isToken = !mint.equals(NATIVE_SOL_MINT);
72
+ const userTokenAccount = isToken
73
+ ? (signer.tokenAccount ?? (await getAssociatedTokenAddress(mint, signer.publicKey)))
74
+ : signer.publicKey;
75
+ // --- 1. Pre-flight ---------------------------------------------------------
76
+ // Before the prover, which costs 10-30 seconds. Every check here is one RPC
77
+ // call, and each one pre-empts an on-chain failure whose error message would
78
+ // tell the user nothing actionable.
79
+ if (params.preflight !== false) {
80
+ await preflightShield({
81
+ connection,
82
+ amount,
83
+ mint,
84
+ signer: { publicKey: signer.publicKey, tokenAccount: userTokenAccount },
85
+ programId,
86
+ options: typeof params.preflight === "object" ? params.preflight : {},
87
+ cache,
88
+ computeUnitLimit,
89
+ computeUnitPriceMicroLamports,
90
+ });
91
+ }
92
+ // --- 2. Target tree and root ----------------------------------------------
93
+ const target = await treeSource.getDepositTarget(mint);
94
+ const treeCapacity = 2 ** MERKLE_TREE_DEPTH;
95
+ if (target.outputNextIndex >= treeCapacity) {
96
+ throw new ShieldError("TREE_FULL", `Merkle tree ${target.outputTreeId} is full (${target.outputNextIndex}/${treeCapacity} leaves).`, {
97
+ retryable: "reshield",
98
+ context: {
99
+ treeId: target.outputTreeId,
100
+ nextIndex: target.outputNextIndex,
101
+ capacity: treeCapacity,
102
+ },
103
+ });
104
+ }
105
+ // --- 3. Outputs ------------------------------------------------------------
106
+ // Output 0 carries the value and is owned by the recipient. Output 1 is a
107
+ // zero-value dummy the 2-in-2-out circuit requires.
108
+ const outputNote = createUTXO({
109
+ amount,
110
+ pubkey: owner.veiloPublicKey,
111
+ mintAddress: mint,
112
+ });
113
+ const dummyOutput = createUTXO({
114
+ amount: 0n,
115
+ pubkey: generateKeypair().publicKey,
116
+ mintAddress: mint,
117
+ });
118
+ // --- 4. Dummy inputs -------------------------------------------------------
119
+ // Two INDEPENDENT keypairs. Reusing one (as the stale client.ts `deposit()`
120
+ // does) only avoids the program's DuplicateNullifiers check by accident of
121
+ // blinding randomness; two keys make it structural.
122
+ const zeroChain = poseidonZeroChain(22).slice(0, 22);
123
+ const dummyInputs = [
124
+ toDummyInput(createOwnedZeroUTXO(generateKeypair().privateKey, mint), zeroChain),
125
+ toDummyInput(createOwnedZeroUTXO(generateKeypair().privateKey, mint), zeroChain),
126
+ ];
127
+ // --- 5. ExtData ------------------------------------------------------------
128
+ // For a deposit these slots carry no payment semantics — no fee is taken and
129
+ // nothing is withdrawn — but they are bound into the proof via ext_data_hash,
130
+ // so they must match exactly what goes on-chain.
131
+ const extData = {
132
+ recipient: signer.publicKey,
133
+ relayer: signer.publicKey,
134
+ fee: 0n,
135
+ refund: 0n,
136
+ claimant: SystemProgram.programId,
137
+ };
138
+ const extDataHash = computeExtDataHash(extData);
139
+ // --- 6. Prove --------------------------------------------------------------
140
+ const circuitInputs = prepareTransactionInputs({
141
+ inputUTXOs: dummyInputs,
142
+ outputUTXOs: [outputNote, dummyOutput],
143
+ root: target.inputRoot,
144
+ publicAmount: amount,
145
+ extData,
146
+ mintAddress: mint,
147
+ });
148
+ let proof;
149
+ try {
150
+ proof = encodeSnarkjsProofToTransactionProof(await prover(circuitInputs));
151
+ }
152
+ catch (e) {
153
+ throw mapShieldError(e);
154
+ }
155
+ // --- 7. Encrypt the note to its owner --------------------------------------
156
+ // Encrypted to `noteViewingKey`, which may be a completely different party
157
+ // from the signer. This is what makes shield-to-someone-else work.
158
+ const compact = createCompactNoteCipher(owner.noteViewingKey.toBytes(), bigIntToBytesBE(outputNote.blinding), amount);
159
+ const empty = emptyNoteCipher();
160
+ // --- 8. Resolve accounts ---------------------------------------------------
161
+ const accounts = resolveTransactAccounts({
162
+ programId,
163
+ mint,
164
+ signer: signer.publicKey,
165
+ userTokenAccount,
166
+ inputTreeId: target.inputTreeId,
167
+ outputTreeId: target.outputTreeId,
168
+ inputNullifiers: circuitInputs.inputNullifiers,
169
+ vaultTokenAccount: isToken
170
+ ? await getAssociatedTokenAddress(mint, getPoolPdas(programId, mint).vault, true)
171
+ : getPoolPdas(programId, mint).vault,
172
+ isToken,
173
+ });
174
+ // --- 9. Cache everything needed to rebuild ---------------------------------
175
+ const alt = await altSource.resolve({
176
+ connection,
177
+ mint,
178
+ outputTreeId: target.outputTreeId,
179
+ programId,
180
+ });
181
+ const proofCache = {
182
+ version: 1,
183
+ programId: programId.toBase58(),
184
+ mint: mint.toBase58(),
185
+ signer: signer.publicKey.toBase58(),
186
+ altAddress: alt.key.toBase58(),
187
+ inputTreeId: target.inputTreeId,
188
+ outputTreeId: target.outputTreeId,
189
+ root: hex(target.inputRoot),
190
+ publicAmount: amount.toString(),
191
+ extData: {
192
+ recipient: extData.recipient.toBase58(),
193
+ relayer: extData.relayer.toBase58(),
194
+ fee: extData.fee.toString(),
195
+ refund: extData.refund.toString(),
196
+ claimant: extData.claimant.toBase58(),
197
+ },
198
+ extDataHash: hex(extDataHash),
199
+ inputNullifiers: [
200
+ hex(circuitInputs.inputNullifiers[0]),
201
+ hex(circuitInputs.inputNullifiers[1]),
202
+ ],
203
+ outputCommitments: [hex(outputNote.commitment), hex(dummyOutput.commitment)],
204
+ proof,
205
+ noteCiphers: {
206
+ note0EphemeralKey: hex(compact.ephemeralPublicKey),
207
+ note0Encrypted: hex(compact.compactBlob),
208
+ note0ViewTag: compact.viewTag,
209
+ note1EphemeralKey: hex(empty.ephemeralPublicKey),
210
+ note1Encrypted: hex(empty.compactBlob),
211
+ note1ViewTag: empty.viewTag,
212
+ },
213
+ accounts: Object.fromEntries(Object.entries(accounts).map(([k, v]) => [k, v.toBase58()])),
214
+ computeUnitLimit,
215
+ };
216
+ const note = {
217
+ commitment: outputNote.commitment,
218
+ blinding: outputNote.blinding,
219
+ amount,
220
+ ownerVeiloPublicKey: owner.veiloPublicKey,
221
+ mint,
222
+ treeId: target.outputTreeId,
223
+ compact,
224
+ };
225
+ // --- 10. Assemble ----------------------------------------------------------
226
+ const blockhash = params.blockhash ?? (await connection.getLatestBlockhash("confirmed"));
227
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + deadlineSeconds);
228
+ const built = buildFromCache({
229
+ cache: proofCache,
230
+ alt,
231
+ blockhash,
232
+ deadline,
233
+ computeUnitPriceMicroLamports,
234
+ });
235
+ const estimatedRentLamports = await rentForNullifierMarkers(connection, cache);
236
+ return {
237
+ transaction: built.transaction,
238
+ blockhash: blockhash.blockhash,
239
+ lastValidBlockHeight: blockhash.lastValidBlockHeight,
240
+ deadline,
241
+ note,
242
+ predictedLeafIndex: target.outputNextIndex,
243
+ quote: {
244
+ serializedSize: built.serializedSize,
245
+ computeUnitLimit,
246
+ computeUnitPriceMicroLamports,
247
+ estimatedFeeLamports: 5000n +
248
+ (BigInt(computeUnitLimit) * BigInt(computeUnitPriceMicroLamports)) / 1000000n,
249
+ estimatedRentLamports,
250
+ },
251
+ proofCache,
252
+ };
253
+ }
254
+ /**
255
+ * Re-assemble a shield with a fresh blockhash and deadline, reusing the proof.
256
+ *
257
+ * The Groth16 proof commits to the root, the public amount, `ext_data_hash`, the
258
+ * mint, the nullifiers and the commitments — none of which is time-dependent.
259
+ * `deadline` is a positional instruction argument, outside `ext_data_hash` and
260
+ * therefore outside the proof, so it can be refreshed too. That last part
261
+ * matters: a shield built at T and signed at T+59min needs a new deadline, not
262
+ * just a new blockhash.
263
+ *
264
+ * Costs one `getLatestBlockhash` and a message compile — no proving, no
265
+ * Poseidon, no account resolution.
266
+ *
267
+ * Does NOT recover from `ROOT_STALE`: a stale root invalidates the proof, so
268
+ * that case needs a fresh `shield()` call.
269
+ */
270
+ export async function rebuild(result, opts = {}) {
271
+ const cache = result.proofCache;
272
+ const blockhash = opts.blockhash ??
273
+ (opts.connection
274
+ ? await opts.connection.getLatestBlockhash("confirmed")
275
+ : (() => {
276
+ throw new ShieldError("UNKNOWN", "rebuild() needs either a `connection` or an explicit `blockhash`");
277
+ })());
278
+ const alt = opts.alt ??
279
+ (opts.connection
280
+ ? await sharedDepositAlt(new PublicKey(cache.altAddress), {
281
+ verifyCoverage: false,
282
+ }).resolve({
283
+ connection: opts.connection,
284
+ mint: new PublicKey(cache.mint),
285
+ outputTreeId: cache.outputTreeId,
286
+ programId: new PublicKey(cache.programId),
287
+ })
288
+ : (() => {
289
+ throw new ShieldError("UNKNOWN", "rebuild() needs either a `connection` or an explicit `alt`");
290
+ })());
291
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + (opts.deadlineSeconds ?? DEFAULT_DEADLINE_SECONDS));
292
+ const priceMicroLamports = opts.computeUnitPriceMicroLamports ?? result.quote.computeUnitPriceMicroLamports;
293
+ const built = buildFromCache({
294
+ cache,
295
+ alt,
296
+ blockhash,
297
+ deadline,
298
+ computeUnitPriceMicroLamports: priceMicroLamports,
299
+ });
300
+ return {
301
+ ...result,
302
+ transaction: built.transaction,
303
+ blockhash: blockhash.blockhash,
304
+ lastValidBlockHeight: blockhash.lastValidBlockHeight,
305
+ deadline,
306
+ quote: {
307
+ ...result.quote,
308
+ serializedSize: built.serializedSize,
309
+ computeUnitPriceMicroLamports: priceMicroLamports,
310
+ },
311
+ };
312
+ }
313
+ /**
314
+ * The single place a shield transaction is assembled and size-checked.
315
+ *
316
+ * Both `shield()` and `rebuild()` route through here so there is exactly one
317
+ * message layout and exactly one size assertion — the failure mode this guards
318
+ * against (a transaction that is 40 bytes too large) is otherwise diagnosed as
319
+ * an opaque wallet error with no indication of which account caused it.
320
+ */
321
+ function buildFromCache(params) {
322
+ const { cache, alt, blockhash, deadline, computeUnitPriceMicroLamports } = params;
323
+ const programId = new PublicKey(cache.programId);
324
+ const signer = new PublicKey(cache.signer);
325
+ const accounts = Object.fromEntries(Object.entries(cache.accounts).map(([k, v]) => [k, new PublicKey(v)]));
326
+ const transactIx = buildTransactInstruction(accounts, {
327
+ root: unhex(cache.root),
328
+ inputTreeId: cache.inputTreeId,
329
+ outputTreeId: cache.outputTreeId,
330
+ publicAmount: BigInt(cache.publicAmount),
331
+ extDataHash: unhex(cache.extDataHash),
332
+ mintAddress: new PublicKey(cache.mint),
333
+ inputNullifiers: [unhex(cache.inputNullifiers[0]), unhex(cache.inputNullifiers[1])],
334
+ outputCommitments: [
335
+ unhex(cache.outputCommitments[0]),
336
+ unhex(cache.outputCommitments[1]),
337
+ ],
338
+ deadline,
339
+ extData: {
340
+ recipient: new PublicKey(cache.extData.recipient),
341
+ relayer: new PublicKey(cache.extData.relayer),
342
+ fee: BigInt(cache.extData.fee),
343
+ refund: BigInt(cache.extData.refund),
344
+ claimant: new PublicKey(cache.extData.claimant),
345
+ },
346
+ proof: cache.proof,
347
+ noteCiphers: {
348
+ note0EphemeralKey: unhex(cache.noteCiphers.note0EphemeralKey),
349
+ note0Encrypted: unhex(cache.noteCiphers.note0Encrypted),
350
+ note0ViewTag: cache.noteCiphers.note0ViewTag,
351
+ note1EphemeralKey: unhex(cache.noteCiphers.note1EphemeralKey),
352
+ note1Encrypted: unhex(cache.noteCiphers.note1Encrypted),
353
+ note1ViewTag: cache.noteCiphers.note1ViewTag,
354
+ },
355
+ }, programId);
356
+ const instructions = [
357
+ ComputeBudgetProgram.setComputeUnitLimit({ units: cache.computeUnitLimit }),
358
+ ];
359
+ // A zero price omits the instruction entirely — worth ~40 bytes when the
360
+ // budget is tight (the gasless path relies on exactly this).
361
+ if (computeUnitPriceMicroLamports > 0) {
362
+ instructions.push(ComputeBudgetProgram.setComputeUnitPrice({
363
+ microLamports: computeUnitPriceMicroLamports,
364
+ }));
365
+ }
366
+ instructions.push(transactIx);
367
+ const message = new TransactionMessage({
368
+ payerKey: signer,
369
+ recentBlockhash: blockhash.blockhash,
370
+ instructions,
371
+ }).compileToV0Message([alt]);
372
+ const transaction = new VersionedTransaction(message);
373
+ // Measure the message separately and add the one-signature wire section.
374
+ // VersionedTransaction.serialize() cannot be used for diagnostics here: it
375
+ // serializes through a fixed-size buffer and can throw `encoding overruns`
376
+ // before we get a chance to report which ALT keys were missing.
377
+ const serializedSize = serializedMessageSize(message) + SIGNATURE_BYTES;
378
+ if (serializedSize > MAX_TRANSACTION_BYTES) {
379
+ const staticKeys = message.staticAccountKeys.map((k) => k.toBase58());
380
+ const inAlt = new Set(alt.state.addresses.map((a) => a.toBase58()));
381
+ throw new ShieldError("TX_TOO_LARGE", `Shield transaction is ${serializedSize} bytes, over the ${MAX_TRANSACTION_BYTES} ` +
382
+ `limit by ${serializedSize - MAX_TRANSACTION_BYTES}. ` +
383
+ `${staticKeys.length} account(s) could not be compressed into the lookup table.`, {
384
+ context: {
385
+ serializedSize,
386
+ limit: MAX_TRANSACTION_BYTES,
387
+ alt: alt.key.toBase58(),
388
+ uncompressedKeys: staticKeys.filter((k) => !inAlt.has(k)),
389
+ },
390
+ });
391
+ }
392
+ return { transaction, serializedSize };
393
+ }
394
+ /** Exact v0 message wire size without serializing into web3.js' 1232-byte buffer. */
395
+ function serializedMessageSize(message) {
396
+ const staticKeys = message.staticAccountKeys.length;
397
+ const instructions = message.compiledInstructions;
398
+ const lookups = message.addressTableLookups;
399
+ return (1 + // v0 prefix
400
+ 3 + // message header
401
+ shortVecSize(staticKeys) +
402
+ staticKeys * 32 +
403
+ 32 + // recent blockhash
404
+ shortVecSize(instructions.length) +
405
+ instructions.reduce((sum, ix) => sum +
406
+ 1 + // program id index
407
+ shortVecSize(ix.accountKeyIndexes.length) +
408
+ ix.accountKeyIndexes.length +
409
+ shortVecSize(ix.data.length) +
410
+ ix.data.length, 0) +
411
+ shortVecSize(lookups.length) +
412
+ lookups.reduce((sum, lookup) => sum +
413
+ 32 +
414
+ shortVecSize(lookup.writableIndexes.length) +
415
+ lookup.writableIndexes.length +
416
+ shortVecSize(lookup.readonlyIndexes.length) +
417
+ lookup.readonlyIndexes.length, 0));
418
+ }
419
+ /** Number of bytes used by Solana's short-vector length prefix. */
420
+ function shortVecSize(value) {
421
+ let size = 1;
422
+ while (value >= 0x80) {
423
+ value >>>= 7;
424
+ size++;
425
+ }
426
+ return size;
427
+ }
428
+ // -- helpers ------------------------------------------------------------------
429
+ /**
430
+ * Wrap a zero-value UTXO as a circuit input.
431
+ *
432
+ * The path elements are the Poseidon zero chain and the index is 0. Neither is
433
+ * constrained — the circuit disables its Merkle check for zero-amount inputs —
434
+ * but the witness still has to be well-formed and the right length.
435
+ */
436
+ function toDummyInput(utxo, zeroChain) {
437
+ return {
438
+ ...utxo,
439
+ pathIndex: 0,
440
+ pathElements: zeroChain,
441
+ privateKey: utxo.privateKey,
442
+ };
443
+ }
444
+ function resolveTransactAccounts(params) {
445
+ const { programId, mint, signer } = params;
446
+ const { config, vault, nullifiers } = getPoolPdas(programId, mint);
447
+ return {
448
+ config,
449
+ global_config: getGlobalConfigPda(programId),
450
+ vault,
451
+ input_tree: getNoteTreePda(programId, mint, params.inputTreeId),
452
+ output_tree: getNoteTreePda(programId, mint, params.outputTreeId),
453
+ nullifiers,
454
+ nullifier_marker_0: getNullifierMarkerPda(programId, mint, params.inputNullifiers[0]),
455
+ nullifier_marker_1: getNullifierMarkerPda(programId, mint, params.inputNullifiers[1]),
456
+ relayer: signer,
457
+ // For a deposit nothing is withdrawn, so `recipient` is just the signer.
458
+ // Pointing the unused token slots at accounts already in the transaction
459
+ // keeps them from costing 32 static bytes each.
460
+ recipient: signer,
461
+ vault_token_account: params.vaultTokenAccount,
462
+ user_token_account: params.userTokenAccount,
463
+ recipient_token_account: params.userTokenAccount,
464
+ relayer_token_account: params.userTokenAccount,
465
+ token_program: params.isToken ? TOKEN_PROGRAM_ID : SystemProgram.programId,
466
+ system_program: SystemProgram.programId,
467
+ };
468
+ }
469
+ async function rentForNullifierMarkers(connection, cache) {
470
+ const key = `rent:${NULLIFIER_MARKER_ACCOUNT_BYTES}`;
471
+ let per = await cache.get(key);
472
+ if (per === undefined) {
473
+ per = await connection.getMinimumBalanceForRentExemption(NULLIFIER_MARKER_ACCOUNT_BYTES);
474
+ await cache.set(key, per, 60 * 60000);
475
+ }
476
+ return BigInt(per) * BigInt(NULLIFIER_MARKERS_PER_TRANSACT);
477
+ }
478
+ const hex = (u) => Buffer.from(u).toString("hex");
479
+ const unhex = (s) => new Uint8Array(Buffer.from(s, "hex"));
480
+ /** Serialize a proof cache for transport between processes. Contains no secrets. */
481
+ export function serializeProofCache(cache) {
482
+ return JSON.stringify(cache);
483
+ }
484
+ export function deserializeProofCache(json) {
485
+ const parsed = JSON.parse(json);
486
+ if (parsed?.version !== 1) {
487
+ throw new ShieldError("UNKNOWN", `unsupported ShieldProofCache version: ${parsed?.version}`);
488
+ }
489
+ return parsed;
490
+ }
491
+ /** Re-exported for callers that want the raw field arithmetic. */
492
+ export { bytesToBigIntBE, BN254_FR_MODULUS };
@@ -0,0 +1,202 @@
1
+ import { AddressLookupTableAccount, Connection, PublicKey, VersionedTransaction } from "@solana/web3.js";
2
+ import type { TransactionProofBuilder, TransactionProofStruct } from "../proof.js";
3
+ import type { CompactNoteCipher } from "../compactNote.js";
4
+ /**
5
+ * Who owns the resulting note. Both halves are required and neither is derived
6
+ * from the other:
7
+ *
8
+ * - `veiloPublicKey` is the Poseidon field element bound into the output
9
+ * commitment. It is the **spending** authority.
10
+ * - `noteViewingKey` is the Ed25519 wallet key the compact cipher is encrypted
11
+ * to. It is the **detection** authority.
12
+ *
13
+ * If these two belong to different people, the note is *spendable but
14
+ * invisible*: the owner can never find it, so the funds are effectively gone.
15
+ * Nothing on-chain and nothing in this type can detect that — which is why
16
+ * `resolveShieldOwner()` exists and why hand-constructing this requires an
17
+ * explicit acknowledgement.
18
+ *
19
+ * See veilo-browser-extension/.../privateStealthWallet.ts:403-415 for the
20
+ * production comment describing this failure mode.
21
+ */
22
+ export type ShieldOwner = {
23
+ veiloPublicKey: bigint;
24
+ noteViewingKey: PublicKey;
25
+ /**
26
+ * Set automatically by {@link resolveShieldOwner}, which fetches both halves
27
+ * from a single relayer response and can therefore guarantee they match.
28
+ *
29
+ * Hand-built owners must set this explicitly — it is the only mechanism
30
+ * standing between an integration mistake and silent fund loss.
31
+ */
32
+ acknowledgeOwnerInvariant?: true;
33
+ };
34
+ /**
35
+ * Who signs and pays. Public key only — the SDK never receives a secret key.
36
+ *
37
+ * The signer is unavoidably the fee payer, the funding source, and the payer of
38
+ * ~0.00192 SOL of nullifier-marker rent (the program hardcodes
39
+ * `payer = relayer` on both marker accounts). A wallet with no SOL cannot
40
+ * shield through this path regardless of who submits the transaction.
41
+ */
42
+ export type ShieldSigner = {
43
+ publicKey: PublicKey;
44
+ /**
45
+ * Source token account for SPL shields. Defaults to the signer's canonical
46
+ * ATA. Must be owned by `publicKey` with no delegate set.
47
+ */
48
+ tokenAccount?: PublicKey;
49
+ };
50
+ /** Where the target tree and its root come from. */
51
+ export interface TreeSource {
52
+ getDepositTarget(mint: PublicKey): Promise<DepositTarget>;
53
+ }
54
+ export type DepositTarget = {
55
+ /** Always 0 — the tree whose root the dummy inputs nominally prove against. */
56
+ inputTreeId: number;
57
+ /** 32-byte root, read from that tree's on-chain root history. */
58
+ inputRoot: Uint8Array;
59
+ /** Tree the new commitments are appended to. */
60
+ outputTreeId: number;
61
+ /** `nextIndex` at build time. A prediction — see `predictedLeafIndex`. */
62
+ outputNextIndex: number;
63
+ };
64
+ /** Where the address lookup table comes from. */
65
+ export interface AltSource {
66
+ resolve(ctx: {
67
+ connection: Connection;
68
+ mint: PublicKey;
69
+ outputTreeId: number;
70
+ programId: PublicKey;
71
+ }): Promise<AddressLookupTableAccount>;
72
+ }
73
+ /**
74
+ * Caching for values that are stable across builds (the ALT account, rent
75
+ * exemption minimums, pool config).
76
+ *
77
+ * Deliberately an interface: the three production deposit implementations
78
+ * differ on exactly this axis — `localStorage`, `chrome.storage`, and
79
+ * `AsyncStorage` — and none of them belongs in a published SDK.
80
+ */
81
+ export interface ShieldCachePort {
82
+ get<T>(key: string): Promise<T | undefined>;
83
+ set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
84
+ }
85
+ export type PreflightOptions = {
86
+ /** Skip the SPL token-account inspection (owner, delegate, balance). */
87
+ skipTokenAccount?: boolean;
88
+ /** Skip the signer balance check. */
89
+ skipBalance?: boolean;
90
+ /** Skip pool state checks (paused, min/max, tree capacity). */
91
+ skipPoolState?: boolean;
92
+ };
93
+ export type ShieldParams = {
94
+ connection: Connection;
95
+ /** Net amount to shield, in base units. No fee is taken on deposits. */
96
+ amount: bigint;
97
+ /** Defaults to native SOL. */
98
+ mint?: PublicKey;
99
+ owner: ShieldOwner;
100
+ signer: ShieldSigner;
101
+ /** Proof generation. `createTransactionProver(artifacts)` satisfies this. */
102
+ prover: TransactionProofBuilder;
103
+ treeSource?: TreeSource;
104
+ alt?: AltSource;
105
+ cache?: ShieldCachePort;
106
+ programId?: PublicKey;
107
+ computeUnitLimit?: number;
108
+ /** Set to 0 to omit the priority-fee instruction entirely. */
109
+ computeUnitPriceMicroLamports?: number;
110
+ deadlineSeconds?: number;
111
+ /** Supply to avoid an RPC round-trip; otherwise fetched just before compiling. */
112
+ blockhash?: {
113
+ blockhash: string;
114
+ lastValidBlockHeight: number;
115
+ };
116
+ preflight?: boolean | PreflightOptions;
117
+ };
118
+ /**
119
+ * The recoverable secret material for the note this shield creates.
120
+ *
121
+ * SECRET. Anyone holding this can locate the note. It must never be persisted
122
+ * server-side in the clear or transmitted alongside {@link ShieldProofCache},
123
+ * which is deliberately public.
124
+ */
125
+ export type ShieldNote = {
126
+ commitment: Uint8Array;
127
+ blinding: bigint;
128
+ amount: bigint;
129
+ ownerVeiloPublicKey: bigint;
130
+ mint: PublicKey;
131
+ treeId: number;
132
+ compact: CompactNoteCipher;
133
+ };
134
+ /**
135
+ * Everything needed to re-assemble the transaction with a fresh blockhash.
136
+ *
137
+ * PUBLIC and JSON-serializable by construction: it carries no blinding, no
138
+ * private key, and no note plaintext. A platform can build server-side, ship
139
+ * this to a client for signature, and rebuild in a different process without
140
+ * ever moving a secret.
141
+ */
142
+ export type ShieldProofCache = {
143
+ version: 1;
144
+ programId: string;
145
+ mint: string;
146
+ signer: string;
147
+ altAddress: string;
148
+ inputTreeId: number;
149
+ outputTreeId: number;
150
+ root: string;
151
+ publicAmount: string;
152
+ extData: {
153
+ recipient: string;
154
+ relayer: string;
155
+ fee: string;
156
+ refund: string;
157
+ claimant: string;
158
+ };
159
+ extDataHash: string;
160
+ inputNullifiers: [string, string];
161
+ outputCommitments: [string, string];
162
+ proof: TransactionProofStruct;
163
+ noteCiphers: {
164
+ note0EphemeralKey: string;
165
+ note0Encrypted: string;
166
+ note0ViewTag: number;
167
+ note1EphemeralKey: string;
168
+ note1Encrypted: string;
169
+ note1ViewTag: number;
170
+ };
171
+ /** Resolved instruction accounts, keyed by IDL account name. */
172
+ accounts: Record<string, string>;
173
+ computeUnitLimit: number;
174
+ };
175
+ export type ShieldQuote = {
176
+ /** Serialized size including the signature. Must be <= 1232. */
177
+ serializedSize: number;
178
+ computeUnitLimit: number;
179
+ computeUnitPriceMicroLamports: number;
180
+ estimatedFeeLamports: bigint;
181
+ /** Nullifier-marker rent, permanently burned by the signer. */
182
+ estimatedRentLamports: bigint;
183
+ };
184
+ export type ShieldResult = {
185
+ /** UNSIGNED. The caller signs and submits — this SDK never broadcasts. */
186
+ transaction: VersionedTransaction;
187
+ blockhash: string;
188
+ lastValidBlockHeight: number;
189
+ deadline: bigint;
190
+ note: ShieldNote;
191
+ /**
192
+ * Leaf index the output commitment is *expected* to land at.
193
+ *
194
+ * A prediction, read before submission. Under concurrent deposits into the
195
+ * same tree it will be wrong. Use `finalizeShield()` to read the authoritative
196
+ * index from the confirmed transaction's `CommitmentEvent` before persisting
197
+ * anything that depends on it.
198
+ */
199
+ predictedLeafIndex: number;
200
+ quote: ShieldQuote;
201
+ proofCache: ShieldProofCache;
202
+ };