@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  11. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  12. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  13. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  14. package/package.json +86 -63
  15. package/src/announcement-client.ts +457 -0
  16. package/src/auditor-ciphertext.ts +181 -0
  17. package/src/auditor.ts +409 -0
  18. package/src/bitcoin/ika.ts +103 -0
  19. package/src/bitcoin/index.ts +5 -0
  20. package/src/bound-params.ts +322 -0
  21. package/src/chadbuffer.ts +603 -0
  22. package/src/circomlibjs.d.ts +51 -0
  23. package/src/claim-link.ts +53 -0
  24. package/src/client.ts +638 -0
  25. package/src/commitment-tree.ts +736 -0
  26. package/src/config.ts +772 -0
  27. package/src/core/esplora.ts +332 -0
  28. package/src/core/mempool.ts +159 -0
  29. package/src/crypto-babyjub.ts +385 -0
  30. package/src/crypto-ed25519.ts +297 -0
  31. package/src/crypto.ts +199 -0
  32. package/src/event-client.ts +231 -0
  33. package/src/events.ts +384 -0
  34. package/src/explorer.ts +300 -0
  35. package/src/index.ts +902 -0
  36. package/src/instructions.ts +2820 -0
  37. package/src/keys.ts +1228 -0
  38. package/src/logger.ts +41 -0
  39. package/src/magicblock.ts +278 -0
  40. package/src/merkle.ts +197 -0
  41. package/src/note.ts +754 -0
  42. package/src/pda.ts +516 -0
  43. package/src/pool-state.ts +176 -0
  44. package/src/poseidon.ts +175 -0
  45. package/src/prover/index.ts +19 -0
  46. package/src/prover/mobile.ts +303 -0
  47. package/src/prover/web.ts +771 -0
  48. package/src/psbt.ts +333 -0
  49. package/src/selective-disclosure.ts +284 -0
  50. package/src/sender-memo.ts +343 -0
  51. package/src/snarkjs.d.ts +19 -0
  52. package/src/sns-resolver.ts +333 -0
  53. package/src/solana/connection.ts +189 -0
  54. package/src/solana/priority-fee.ts +201 -0
  55. package/src/spend-doc.ts +163 -0
  56. package/src/stealth.ts +1477 -0
  57. package/src/taproot.ts +707 -0
  58. package/src/token-registry.ts +207 -0
  59. package/src/utils/encoding.ts +33 -0
  60. package/src/vk-registry.ts +295 -0
  61. package/LICENSE +0 -21
  62. package/packages/btc-client/src/esplora-client.ts +0 -153
  63. package/packages/btc-client/src/index.ts +0 -3
  64. package/packages/btc-client/src/op-return.ts +0 -93
  65. package/packages/btc-client/src/types.ts +0 -112
  66. package/packages/sdk/README.md +0 -277
  67. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  68. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  69. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  73. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  79. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  81. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  83. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  85. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/config.js +0 -0
  87. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  91. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  95. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  97. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/events.js +0 -0
  99. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  101. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  103. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  105. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  107. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  109. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/note.js +0 -0
  111. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  115. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  117. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  123. /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/psbt.js +0 -0
  125. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
package/src/pda.ts ADDED
@@ -0,0 +1,516 @@
1
+ /**
2
+ * PDA (Program Derived Address) Derivation Utilities
3
+ *
4
+ * Centralized module for all UTXOpia PDA derivations.
5
+ * Prevents code duplication across api.ts, pda.ts, etc.
6
+ *
7
+ * @module pda
8
+ */
9
+
10
+ import {
11
+ getAddressEncoder,
12
+ getProgramDerivedAddress,
13
+ type Address,
14
+ } from "@solana/kit";
15
+
16
+ import {
17
+ UTXOPIA_PROGRAM_ID,
18
+ UTXOPIA_POLICY_PROGRAM_ID,
19
+ BTC_LIGHT_CLIENT_PROGRAM_ID,
20
+ } from "./config";
21
+
22
+ function seedBytes(value: Address | Uint8Array, label: string): Uint8Array {
23
+ const bytes =
24
+ typeof value === "string"
25
+ ? new Uint8Array(getAddressEncoder().encode(value))
26
+ : value;
27
+ if (bytes.length !== 32) throw new Error(`${label} must be 32 bytes`);
28
+ return bytes;
29
+ }
30
+
31
+ // =============================================================================
32
+ // PDA Seeds
33
+ // =============================================================================
34
+
35
+ export const PDA_SEEDS = {
36
+ POOL_STATE: "pool_state",
37
+ COMMITMENT_TREE: "commitment_tree",
38
+ LIGHT_CLIENT: "btc_light_client",
39
+ BLOCK_HEADER: "block",
40
+ HEIGHT_INDEX: "height_index",
41
+ VERIFIED_TX: "verified_tx",
42
+ DEPOSIT: "deposit",
43
+ NULLIFIER: "nullifier",
44
+ POLICY_APPROVAL: "policy_approval",
45
+ EXIT_DESTINATION: "exit_destination",
46
+ VK_REGISTRY: "vk_registry",
47
+ TOKEN_CONFIG: "token_config",
48
+ POOL_CONFIG: "pool_config",
49
+ } as const;
50
+
51
+ // =============================================================================
52
+ // Seed builders — the single definition of every program-derived address
53
+ //
54
+ // These are the authority. Each mirrors the seed array the on-chain program
55
+ // derives with, and every derivation in this file is built on them, so there is
56
+ // one place to change when the program changes.
57
+ //
58
+ // They are exported and synchronous on purpose. `getProgramDerivedAddress` is
59
+ // async, but consumers on @solana/web3.js need `findProgramAddressSync` and
60
+ // `PublicKey`. Without a sync seam those consumers end up re-declaring the seeds
61
+ // themselves — which is exactly how the web app's copy drifted out of sync with
62
+ // the program twice (the nullifier and the redemption request both lost their
63
+ // pool scope, and both failed only on chain, after a proof had been paid for).
64
+ // Take the seeds from here and do your own address math with your own types.
65
+ // =============================================================================
66
+
67
+ const enc = (s: string): Uint8Array => new TextEncoder().encode(s);
68
+
69
+ const u32le = (n: number, label: string): Uint8Array => {
70
+ if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) {
71
+ throw new Error(`${label} must be a u32, got ${n}`);
72
+ }
73
+ const b = new Uint8Array(4);
74
+ new DataView(b.buffer).setUint32(0, n, true);
75
+ return b;
76
+ };
77
+
78
+ const u64le = (n: bigint): Uint8Array => {
79
+ const b = new Uint8Array(8);
80
+ new DataView(b.buffer).setBigUint64(0, n, true);
81
+ return b;
82
+ };
83
+
84
+ /** `["pool_state", pool_id]` — pool_id is the pool's zkBTC mint. */
85
+ export function poolStateSeeds(poolId: Address | Uint8Array): Uint8Array[] {
86
+ return [enc(PDA_SEEDS.POOL_STATE), seedBytes(poolId, "poolId")];
87
+ }
88
+
89
+ /** `["commitment_tree", pool_state, tree_index_le]`. */
90
+ export function commitmentTreeSeeds(
91
+ poolState: Address | Uint8Array,
92
+ treeIndex = 0,
93
+ ): Uint8Array[] {
94
+ return [
95
+ enc(PDA_SEEDS.COMMITMENT_TREE),
96
+ seedBytes(poolState, "poolState"),
97
+ u32le(treeIndex, "treeIndex"),
98
+ ];
99
+ }
100
+
101
+ /** `["token_config", pool_state, mint]`. */
102
+ export function tokenConfigSeeds(
103
+ poolState: Address | Uint8Array,
104
+ mint: Address | Uint8Array,
105
+ ): Uint8Array[] {
106
+ return [
107
+ enc(PDA_SEEDS.TOKEN_CONFIG),
108
+ seedBytes(poolState, "poolState"),
109
+ seedBytes(mint, "mint"),
110
+ ];
111
+ }
112
+
113
+ /** `["pool_config", pool_state]`. */
114
+ export function poolConfigSeeds(poolState: Address | Uint8Array): Uint8Array[] {
115
+ return [enc(PDA_SEEDS.POOL_CONFIG), seedBytes(poolState, "poolState")];
116
+ }
117
+
118
+ /** `["nullifier", pool_state, nullifier]` on tree 0, and
119
+ * `["nullifier", pool_state, tree_index_le, nullifier]` after a rotation.
120
+ *
121
+ * A nullifier is Poseidon(nullifyingKey, leafIndex), so it names a note only
122
+ * within one pool and one tree — leaf indices restart at 0 in each new tree.
123
+ * Drop either scope and two distinct notes collapse onto one PDA, where
124
+ * spending either strands the other. Tree 0 keeps the shorter seeds so records
125
+ * already on chain stay reachable (`joinsplit_common.rs`). */
126
+ export function nullifierRecordSeeds(
127
+ nullifierHash: Uint8Array,
128
+ poolState: Address | Uint8Array,
129
+ treeIndex = 0,
130
+ ): Uint8Array[] {
131
+ const seeds: Uint8Array[] = [
132
+ enc(PDA_SEEDS.NULLIFIER),
133
+ seedBytes(poolState, "poolState"),
134
+ ];
135
+ if (treeIndex !== 0) seeds.push(u32le(treeIndex, "treeIndex"));
136
+ seeds.push(nullifierHash);
137
+ return seeds;
138
+ }
139
+
140
+ /** `["redemption", pool_state, user, nonce_le]` (`redeem.rs`). */
141
+ export function redemptionRequestSeeds(
142
+ poolState: Address | Uint8Array,
143
+ userPubkey: Address | Uint8Array,
144
+ nonce: bigint,
145
+ ): Uint8Array[] {
146
+ return [
147
+ enc("redemption"),
148
+ seedBytes(poolState, "poolState"),
149
+ seedBytes(userPubkey, "userPubkey"),
150
+ u64le(nonce),
151
+ ];
152
+ }
153
+
154
+ /** `["vk_registry", [n_inputs], [n_outputs]]`. */
155
+ export function vkRegistrySeeds(nInputs: number, nOutputs: number): Uint8Array[] {
156
+ return [enc(PDA_SEEDS.VK_REGISTRY), Uint8Array.of(nInputs), Uint8Array.of(nOutputs)];
157
+ }
158
+
159
+ /** `["deposit_receipt", txid]`, or `[..., vout_le]` for the OP_RETURN-free
160
+ * `verify_deposit` flow. See deriveDepositReceiptPDA for which is which. */
161
+ export function depositReceiptSeeds(
162
+ depositTxid: Uint8Array,
163
+ depositVout?: number,
164
+ ): Uint8Array[] {
165
+ if (depositTxid.length !== 32) {
166
+ throw new Error(`depositTxid must be 32 bytes, got ${depositTxid.length}`);
167
+ }
168
+ const seeds: Uint8Array[] = [enc("deposit_receipt"), depositTxid];
169
+ if (depositVout !== undefined) seeds.push(u32le(depositVout, "depositVout"));
170
+ return seeds;
171
+ }
172
+
173
+ /** `["policy_approval", pool_state, request_hash, nonce]`. */
174
+ export function policyApprovalSeeds(
175
+ poolState: Address | Uint8Array,
176
+ requestHash: Uint8Array,
177
+ nonce: Uint8Array,
178
+ ): Uint8Array[] {
179
+ if (requestHash.length !== 32 || nonce.length !== 32) {
180
+ throw new Error("requestHash and nonce must be 32 bytes");
181
+ }
182
+ return [
183
+ enc(PDA_SEEDS.POLICY_APPROVAL),
184
+ seedBytes(poolState, "poolState"),
185
+ requestHash,
186
+ nonce,
187
+ ];
188
+ }
189
+
190
+ /** `["exit_destination", pool_state, [kind], key]`. */
191
+ export function exitDestinationSeeds(
192
+ poolState: Address | Uint8Array,
193
+ kind: number,
194
+ key: Uint8Array,
195
+ ): Uint8Array[] {
196
+ if (key.length !== 32) throw new Error("exit destination key must be 32 bytes");
197
+ if (kind !== EXIT_KIND_SOLANA_OWNER && kind !== EXIT_KIND_BTC_SCRIPT) {
198
+ throw new Error("unknown exit destination kind");
199
+ }
200
+ return [
201
+ enc(PDA_SEEDS.EXIT_DESTINATION),
202
+ seedBytes(poolState, "poolState"),
203
+ Uint8Array.of(kind),
204
+ key,
205
+ ];
206
+ }
207
+
208
+ /** `["btc_light_client"]`. */
209
+ export function lightClientSeeds(): Uint8Array[] {
210
+ return [enc(PDA_SEEDS.LIGHT_CLIENT)];
211
+ }
212
+
213
+ /** `["block", block_hash]`. */
214
+ export function blockHeaderSeeds(blockHash: Uint8Array): Uint8Array[] {
215
+ // A short hash still derives *an* address, just not the block's — the caller would then read
216
+ // or write the wrong account with no error anywhere. Every other 32-byte seed here is checked.
217
+ return [enc(PDA_SEEDS.BLOCK_HEADER), seedBytes(blockHash, "blockHash")];
218
+ }
219
+
220
+ /** `["height_index", height_le(8)]`. */
221
+ export function heightIndexSeeds(height: number | bigint): Uint8Array[] {
222
+ return [enc(PDA_SEEDS.HEIGHT_INDEX), u64le(BigInt(height))];
223
+ }
224
+
225
+ /** `["verified_tx", block_hash, txid]`. */
226
+ export function verifiedTransactionSeeds(
227
+ blockHash: Uint8Array,
228
+ txid: Uint8Array,
229
+ ): Uint8Array[] {
230
+ return [enc(PDA_SEEDS.VERIFIED_TX), blockHash, txid];
231
+ }
232
+
233
+ // =============================================================================
234
+ // Core UTXOpia PDAs
235
+ // =============================================================================
236
+
237
+ /**
238
+ * Derive Pool State PDA
239
+ */
240
+ export async function derivePoolStatePDA(
241
+ poolId: Address | Uint8Array,
242
+ programId: Address = UTXOPIA_PROGRAM_ID
243
+ ): Promise<[Address, number]> {
244
+ const result = await getProgramDerivedAddress({
245
+ programAddress: programId,
246
+ seeds: poolStateSeeds(poolId),
247
+ });
248
+ return [result[0], result[1]];
249
+ }
250
+
251
+ /**
252
+ * Derive Commitment Tree PDA
253
+ *
254
+ * @param treeIndex - Tree rotation index (default 0).
255
+ */
256
+ export async function deriveCommitmentTreePDA(
257
+ poolState: Address | Uint8Array,
258
+ programId: Address = UTXOPIA_PROGRAM_ID,
259
+ treeIndex?: number,
260
+ ): Promise<[Address, number]> {
261
+ const result = await getProgramDerivedAddress({
262
+ programAddress: programId,
263
+ seeds: commitmentTreeSeeds(poolState, treeIndex ?? 0),
264
+ });
265
+ return [result[0], result[1]];
266
+ }
267
+
268
+ /**
269
+ * Derive TokenConfig PDA for a specific mint
270
+ * Seeds: ["token_config", mint_pubkey_bytes]
271
+ */
272
+ export async function deriveTokenConfigPDA(
273
+ poolState: Address | Uint8Array,
274
+ mintPubkey: Uint8Array,
275
+ programId: Address = UTXOPIA_PROGRAM_ID
276
+ ): Promise<[Address, number]> {
277
+ const result = await getProgramDerivedAddress({
278
+ programAddress: programId,
279
+ seeds: tokenConfigSeeds(poolState, mintPubkey),
280
+ });
281
+ return [result[0], result[1]];
282
+ }
283
+
284
+ /** Derive ["pool_config", pool_state]. */
285
+ export async function derivePoolConfigPDA(
286
+ poolState: Address | Uint8Array,
287
+ programId: Address = UTXOPIA_PROGRAM_ID
288
+ ): Promise<[Address, number]> {
289
+ const result = await getProgramDerivedAddress({
290
+ programAddress: programId,
291
+ seeds: poolConfigSeeds(poolState),
292
+ });
293
+ return [result[0], result[1]];
294
+ }
295
+
296
+ /**
297
+ * Derive Nullifier Record PDA.
298
+ *
299
+ * Seeds are `["nullifier", pool_state, nullifier]` on tree 0, and
300
+ * `["nullifier", pool_state, tree_index_le, nullifier]` on any tree a rotation
301
+ * created. Both scopes exist because a nullifier is Poseidon(nullifyingKey,
302
+ * leafIndex) and so identifies a note only within one pool and one tree:
303
+ *
304
+ * - Without the pool, the same seed spending into two vaults derives one PDA,
305
+ * and spending in either strands the twin note in the other.
306
+ * - Without the tree, the same happens across a rotation, because leaf indices
307
+ * restart at 0 in every new tree.
308
+ *
309
+ * Tree 0 keeps the shorter seeds so the records already on chain stay reachable
310
+ * — re-deriving them would make every already-spent note spendable again.
311
+ */
312
+ export async function deriveNullifierRecordPDA(
313
+ nullifierHash: Uint8Array,
314
+ poolState: Address | Uint8Array,
315
+ treeIndex = 0,
316
+ programId: Address = UTXOPIA_PROGRAM_ID
317
+ ): Promise<[Address, number]> {
318
+ const result = await getProgramDerivedAddress({
319
+ programAddress: programId,
320
+ seeds: nullifierRecordSeeds(nullifierHash, poolState, treeIndex),
321
+ });
322
+ return [result[0], result[1]];
323
+ }
324
+
325
+ /**
326
+ * Derive one-time PolicyApproval PDA.
327
+ * Seeds: ["policy_approval", pool_state, request_hash, nonce]
328
+ */
329
+ export async function derivePolicyApprovalPDA(
330
+ poolState: Address | Uint8Array,
331
+ requestHash: Uint8Array,
332
+ nonce: Uint8Array,
333
+ programId: Address = UTXOPIA_POLICY_PROGRAM_ID
334
+ ): Promise<[Address, number]> {
335
+ const result = await getProgramDerivedAddress({
336
+ programAddress: programId,
337
+ seeds: policyApprovalSeeds(poolState, requestHash, nonce),
338
+ });
339
+ return [result[0], result[1]];
340
+ }
341
+
342
+ /** Destination kinds in the exit registry. Kind is a PDA seed, so a Solana
343
+ * owner and a BTC script hash sharing the same 32 bytes stay distinct. */
344
+ export const EXIT_KIND_SOLANA_OWNER = 0;
345
+ export const EXIT_KIND_BTC_SCRIPT = 1;
346
+
347
+ /**
348
+ * Derive an ExitDestination PDA — the append-only registry of destinations a
349
+ * permissioned pool's ragequit path may pay.
350
+ *
351
+ * Seeds: ["exit_destination", pool_state, [kind], key]
352
+ *
353
+ * `key` is the recipient token account's OWNER for `EXIT_KIND_SOLANA_OWNER`,
354
+ * or `sha256(btcScript)` for `EXIT_KIND_BTC_SCRIPT`.
355
+ */
356
+ export async function deriveExitDestinationPDA(
357
+ poolState: Address | Uint8Array,
358
+ kind: number,
359
+ key: Uint8Array,
360
+ programId: Address
361
+ ): Promise<[Address, number]> {
362
+ const result = await getProgramDerivedAddress({
363
+ programAddress: programId,
364
+ seeds: exitDestinationSeeds(poolState, kind, key),
365
+ });
366
+ return [result[0], result[1]];
367
+ }
368
+
369
+ // =============================================================================
370
+ // BTC Light Client PDAs
371
+ // =============================================================================
372
+
373
+ /**
374
+ * Derive BTC Light Client PDA
375
+ */
376
+ export async function deriveLightClientPDA(
377
+ programId: Address = BTC_LIGHT_CLIENT_PROGRAM_ID
378
+ ): Promise<[Address, number]> {
379
+ const result = await getProgramDerivedAddress({
380
+ programAddress: programId,
381
+ seeds: lightClientSeeds(),
382
+ });
383
+ return [result[0], result[1]];
384
+ }
385
+
386
+ /**
387
+ * Derive Block Header PDA (hash-based)
388
+ * Seeds: ["block", blockHash(32)]
389
+ */
390
+ export async function deriveBlockHeaderPDA(
391
+ blockHash: Uint8Array,
392
+ programId: Address = BTC_LIGHT_CLIENT_PROGRAM_ID
393
+ ): Promise<[Address, number]> {
394
+ const result = await getProgramDerivedAddress({
395
+ programAddress: programId,
396
+ seeds: blockHeaderSeeds(blockHash),
397
+ });
398
+ return [result[0], result[1]];
399
+ }
400
+
401
+ /**
402
+ * Derive HeightIndex PDA
403
+ * Seeds: ["height_index", height_le_bytes(8)]
404
+ */
405
+ export async function deriveHeightIndexPDA(
406
+ height: number | bigint,
407
+ programId: Address = BTC_LIGHT_CLIENT_PROGRAM_ID
408
+ ): Promise<[Address, number]> {
409
+ const result = await getProgramDerivedAddress({
410
+ programAddress: programId,
411
+ seeds: heightIndexSeeds(height),
412
+ });
413
+ return [result[0], result[1]];
414
+ }
415
+
416
+ /**
417
+ * Derive VerifiedTransaction PDA (btc-light-client)
418
+ *
419
+ * Seeds: ["verified_tx", blockHash(32), txid(32)]
420
+ */
421
+ export async function deriveVerifiedTransactionPDA(
422
+ blockHash: Uint8Array,
423
+ txid: Uint8Array,
424
+ programId: Address = BTC_LIGHT_CLIENT_PROGRAM_ID
425
+ ): Promise<[Address, number]> {
426
+ const result = await getProgramDerivedAddress({
427
+ programAddress: programId,
428
+ seeds: verifiedTransactionSeeds(blockHash, txid),
429
+ });
430
+ return [result[0], result[1]];
431
+ }
432
+
433
+ // =============================================================================
434
+ // Redemption Request PDAs
435
+ // =============================================================================
436
+
437
+ /**
438
+ * Derive Redemption Request PDA
439
+ *
440
+ * Seeds: ["redemption", pool_state, user_pubkey(32), nonce_le(8)]
441
+ */
442
+ export async function deriveRedemptionRequestPDA(
443
+ poolState: Address | Uint8Array,
444
+ userPubkey: Uint8Array,
445
+ nonce: bigint,
446
+ programId: Address = UTXOPIA_PROGRAM_ID
447
+ ): Promise<[Address, number]> {
448
+ const result = await getProgramDerivedAddress({
449
+ programAddress: programId,
450
+ seeds: redemptionRequestSeeds(poolState, userPubkey, nonce),
451
+ });
452
+ return [result[0], result[1]];
453
+ }
454
+
455
+ // =============================================================================
456
+ // VK Registry PDAs
457
+ // =============================================================================
458
+
459
+ /**
460
+ * Derive VK Registry PDA for a JoinSplit variant
461
+ *
462
+ * Seeds: ["vk_registry", &[n_inputs], &[n_outputs]]
463
+ */
464
+ export async function deriveVkRegistryPDA(
465
+ nInputs: number,
466
+ nOutputs: number,
467
+ programId: Address = UTXOPIA_PROGRAM_ID
468
+ ): Promise<[Address, number]> {
469
+ const result = await getProgramDerivedAddress({
470
+ programAddress: programId,
471
+ seeds: vkRegistrySeeds(nInputs, nOutputs),
472
+ });
473
+ return [result[0], result[1]];
474
+ }
475
+
476
+ // =============================================================================
477
+ // Deposit Receipt PDAs
478
+ // =============================================================================
479
+
480
+ /**
481
+ * Derive Deposit Receipt PDA.
482
+ *
483
+ * Two on-chain schemes exist:
484
+ * - `complete_deposit` (disc 11, the active direct-vault flow): seeds ["deposit_receipt", txid] —
485
+ * call WITHOUT `depositVout`.
486
+ * - `verify_deposit` (disc 25, OP_RETURN-free flow): seeds ["deposit_receipt", txid, vout(4 LE)] —
487
+ * pass `depositVout` so a funding tx with multiple independent deposit outputs gets one receipt
488
+ * per output (each creditable once) instead of the first output blocking the rest.
489
+ */
490
+ export async function deriveDepositReceiptPDA(
491
+ depositTxid: Uint8Array,
492
+ depositVout?: number,
493
+ programId: Address = UTXOPIA_PROGRAM_ID
494
+ ): Promise<[Address, number]> {
495
+ const result = await getProgramDerivedAddress({
496
+ programAddress: programId,
497
+ seeds: depositReceiptSeeds(depositTxid, depositVout),
498
+ });
499
+ return [result[0], result[1]];
500
+ }
501
+
502
+ // =============================================================================
503
+ // Utility Functions
504
+ // =============================================================================
505
+
506
+ /**
507
+ * Convert bigint commitment to bytes for PDA derivation
508
+ */
509
+ export function commitmentToBytes(commitment: bigint): Uint8Array {
510
+ const hex = commitment.toString(16).padStart(64, "0");
511
+ const bytes = new Uint8Array(32);
512
+ for (let i = 0; i < 32; i++) {
513
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
514
+ }
515
+ return bytes;
516
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * PoolState account layout and fee arithmetic.
3
+ *
4
+ * Both mirror the program (`state/pool.rs`, `utils/fees.rs`), so both belong
5
+ * here rather than in an app: a consumer that re-derives either gets it right
6
+ * until the struct grows a field, and then reads the wrong bytes with no error.
7
+ * TokenConfig and CommitmentTree are already decoded in this package; this
8
+ * closes the gap for the third account.
9
+ *
10
+ * @module pool-state
11
+ */
12
+
13
+ export const POOL_STATE_DISCRIMINATOR = 0x01;
14
+
15
+ /** `core::mem::size_of::<PoolState>()`. Fields are appended into `_reserved`,
16
+ * so the length is stable across the additions made so far. */
17
+ export const POOL_STATE_LEN = 332;
18
+
19
+ /** Byte offsets of every field, in declaration order (`#[repr(C)]`). */
20
+ export const POOL_STATE_OFFSETS = {
21
+ discriminator: 0,
22
+ bump: 1,
23
+ flags: 2,
24
+ authority: 4,
25
+ zkbtcMint: 36,
26
+ poolVault: 68,
27
+ depositVault: 100,
28
+ depositCount: 132,
29
+ totalMinted: 140,
30
+ totalBurned: 148,
31
+ pendingRedemptions: 156,
32
+ lastUpdate: 164,
33
+ minDeposit: 172,
34
+ maxDeposit: 180,
35
+ totalShielded: 188,
36
+ serviceFeeBase: 196,
37
+ feePool: 204,
38
+ pendingMinDeposit: 212,
39
+ pendingMaxDeposit: 220,
40
+ pendingServiceFee: 228,
41
+ pendingExecuteAfter: 236,
42
+ depositFeeBps: 244,
43
+ withdrawalFeeBps: 246,
44
+ totalBtcHeld: 248,
45
+ utxoCount: 256,
46
+ activeTreeIndex: 258,
47
+ utxoCountHi: 262,
48
+ auditor: 264,
49
+ auditorViewingPubkey: 296,
50
+ nullifierCount: 328,
51
+ } as const;
52
+
53
+ /** `flags` bits, from `state/pool.rs`. */
54
+ export const POOL_FLAG = {
55
+ PAUSED: 1 << 0,
56
+ PERMISSIONED: 1 << 1,
57
+ AUDITOR_FROZEN: 1 << 2,
58
+ VK_REGISTRY_FROZEN: 1 << 3,
59
+ } as const;
60
+
61
+ export interface PoolFees {
62
+ depositFeeBps: number;
63
+ withdrawalFeeBps: number;
64
+ }
65
+
66
+ export interface PoolState extends PoolFees {
67
+ bump: number;
68
+ paused: boolean;
69
+ permissioned: boolean;
70
+ auditorFrozen: boolean;
71
+ vkRegistryFrozen: boolean;
72
+ zkbtcMint: Uint8Array;
73
+ poolVault: Uint8Array;
74
+ depositCount: bigint;
75
+ totalMinted: bigint;
76
+ totalBurned: bigint;
77
+ pendingRedemptions: bigint;
78
+ minDeposit: bigint;
79
+ maxDeposit: bigint;
80
+ totalShielded: bigint;
81
+ serviceFeeBase: bigint;
82
+ feePool: bigint;
83
+ totalBtcHeld: bigint;
84
+ activeTreeIndex: number;
85
+ auditor: Uint8Array;
86
+ auditorViewingPubkey: Uint8Array;
87
+ /** Counts only records created since the counter was added; older pools
88
+ * start from zero, so treat it as a floor rather than a total. */
89
+ nullifierCount: number;
90
+ /** `utxo_count` widened by `utxo_count_hi`, which was carved out later. */
91
+ utxoCount: number;
92
+ }
93
+
94
+ const view = (data: Uint8Array): DataView =>
95
+ new DataView(data.buffer, data.byteOffset, data.byteLength);
96
+
97
+ /** Fees only — the common case, and readable from a truncated account. */
98
+ export function parsePoolFees(data: Uint8Array): PoolFees | null {
99
+ if (
100
+ data.length < POOL_STATE_OFFSETS.withdrawalFeeBps + 2 ||
101
+ data[0] !== POOL_STATE_DISCRIMINATOR
102
+ ) {
103
+ return null;
104
+ }
105
+ const v = view(data);
106
+ return {
107
+ depositFeeBps: v.getUint16(POOL_STATE_OFFSETS.depositFeeBps, true),
108
+ withdrawalFeeBps: v.getUint16(POOL_STATE_OFFSETS.withdrawalFeeBps, true),
109
+ };
110
+ }
111
+
112
+ /** Full decode. Returns null rather than throwing: callers poll this against
113
+ * whatever the RPC hands back, including a pool from an older deployment. */
114
+ export function parsePoolState(data: Uint8Array): PoolState | null {
115
+ if (data.length < POOL_STATE_LEN || data[0] !== POOL_STATE_DISCRIMINATOR) {
116
+ return null;
117
+ }
118
+ const v = view(data);
119
+ const o = POOL_STATE_OFFSETS;
120
+ const flags = data[o.flags];
121
+ const bytes = (at: number, len = 32) => data.subarray(at, at + len);
122
+
123
+ return {
124
+ bump: data[o.bump],
125
+ paused: (flags & POOL_FLAG.PAUSED) !== 0,
126
+ permissioned: (flags & POOL_FLAG.PERMISSIONED) !== 0,
127
+ auditorFrozen: (flags & POOL_FLAG.AUDITOR_FROZEN) !== 0,
128
+ vkRegistryFrozen: (flags & POOL_FLAG.VK_REGISTRY_FROZEN) !== 0,
129
+ zkbtcMint: bytes(o.zkbtcMint),
130
+ poolVault: bytes(o.poolVault),
131
+ depositCount: v.getBigUint64(o.depositCount, true),
132
+ totalMinted: v.getBigUint64(o.totalMinted, true),
133
+ totalBurned: v.getBigUint64(o.totalBurned, true),
134
+ pendingRedemptions: v.getBigUint64(o.pendingRedemptions, true),
135
+ minDeposit: v.getBigUint64(o.minDeposit, true),
136
+ maxDeposit: v.getBigUint64(o.maxDeposit, true),
137
+ totalShielded: v.getBigUint64(o.totalShielded, true),
138
+ serviceFeeBase: v.getBigUint64(o.serviceFeeBase, true),
139
+ feePool: v.getBigUint64(o.feePool, true),
140
+ depositFeeBps: v.getUint16(o.depositFeeBps, true),
141
+ withdrawalFeeBps: v.getUint16(o.withdrawalFeeBps, true),
142
+ totalBtcHeld: v.getBigUint64(o.totalBtcHeld, true),
143
+ activeTreeIndex: v.getUint32(o.activeTreeIndex, true),
144
+ utxoCount: v.getUint16(o.utxoCount, true) + (v.getUint16(o.utxoCountHi, true) << 16),
145
+ auditor: bytes(o.auditor),
146
+ auditorViewingPubkey: bytes(o.auditorViewingPubkey),
147
+ nullifierCount: v.getUint32(o.nullifierCount, true),
148
+ };
149
+ }
150
+
151
+ // =============================================================================
152
+ // Fee arithmetic
153
+ // =============================================================================
154
+
155
+ export const BPS_DENOMINATOR = 10_000n;
156
+
157
+ /**
158
+ * Compute an on-chain basis-point fee. Unshield withdrawals enforce a one-unit
159
+ * minimum; deposits use plain floor division. Quoting a fee any other way shows
160
+ * the user a number the program will not charge.
161
+ */
162
+ export function computeBpsFee(
163
+ amount: bigint,
164
+ bps: number,
165
+ minimumOne = true,
166
+ ): bigint {
167
+ if (amount <= 0n || bps <= 0) return 0n;
168
+ const fee = (amount * BigInt(bps)) / BPS_DENOMINATOR;
169
+ return fee > 0n || !minimumOne ? fee : 1n;
170
+ }
171
+
172
+ /** What share of `gross` a fee represents, in basis points. */
173
+ export function feeShareBps(fee: bigint, gross: bigint): number {
174
+ if (fee <= 0n || gross <= 0n) return 0;
175
+ return Number((fee * BPS_DENOMINATOR) / gross);
176
+ }