@utxopia/sdk 0.1.0-alpha.1 → 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}/config.d.ts +34 -3
  5. package/{packages/sdk/dist → dist}/config.js +88 -18
  6. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  7. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  8. package/{packages/sdk/dist → dist}/index.d.ts +5 -4
  9. package/{packages/sdk/dist → dist}/index.js +5 -4
  10. package/{packages/sdk/dist → dist}/instructions.d.ts +88 -1
  11. package/{packages/sdk/dist → dist}/instructions.js +104 -3
  12. package/dist/spend-doc.d.ts +63 -0
  13. package/dist/spend-doc.js +100 -0
  14. package/{packages/sdk/dist → dist}/stealth.d.ts +124 -1
  15. package/{packages/sdk/dist → dist}/stealth.js +172 -4
  16. package/{packages/sdk/dist → dist}/taproot.d.ts +48 -0
  17. package/{packages/sdk/dist → dist}/taproot.js +71 -0
  18. package/package.json +86 -63
  19. package/src/announcement-client.ts +457 -0
  20. package/src/auditor-ciphertext.ts +181 -0
  21. package/src/auditor.ts +409 -0
  22. package/src/bitcoin/ika.ts +103 -0
  23. package/src/bitcoin/index.ts +5 -0
  24. package/src/bound-params.ts +322 -0
  25. package/src/chadbuffer.ts +603 -0
  26. package/src/circomlibjs.d.ts +51 -0
  27. package/src/claim-link.ts +53 -0
  28. package/src/client.ts +638 -0
  29. package/src/commitment-tree.ts +736 -0
  30. package/src/config.ts +772 -0
  31. package/src/core/esplora.ts +332 -0
  32. package/src/core/mempool.ts +159 -0
  33. package/src/crypto-babyjub.ts +385 -0
  34. package/src/crypto-ed25519.ts +297 -0
  35. package/src/crypto.ts +199 -0
  36. package/src/event-client.ts +231 -0
  37. package/src/events.ts +384 -0
  38. package/src/explorer.ts +300 -0
  39. package/src/index.ts +902 -0
  40. package/src/instructions.ts +2820 -0
  41. package/src/keys.ts +1228 -0
  42. package/src/logger.ts +41 -0
  43. package/src/magicblock.ts +278 -0
  44. package/src/merkle.ts +197 -0
  45. package/src/note.ts +754 -0
  46. package/src/pda.ts +516 -0
  47. package/src/pool-state.ts +176 -0
  48. package/src/poseidon.ts +175 -0
  49. package/src/prover/index.ts +19 -0
  50. package/src/prover/mobile.ts +303 -0
  51. package/src/prover/web.ts +771 -0
  52. package/src/psbt.ts +333 -0
  53. package/src/selective-disclosure.ts +284 -0
  54. package/src/sender-memo.ts +343 -0
  55. package/src/snarkjs.d.ts +19 -0
  56. package/src/sns-resolver.ts +333 -0
  57. package/src/solana/connection.ts +189 -0
  58. package/src/solana/priority-fee.ts +201 -0
  59. package/src/spend-doc.ts +163 -0
  60. package/src/stealth.ts +1477 -0
  61. package/src/taproot.ts +707 -0
  62. package/src/token-registry.ts +207 -0
  63. package/src/utils/encoding.ts +33 -0
  64. package/src/vk-registry.ts +295 -0
  65. package/LICENSE +0 -21
  66. package/packages/btc-client/src/esplora-client.ts +0 -153
  67. package/packages/btc-client/src/index.ts +0 -3
  68. package/packages/btc-client/src/op-return.ts +0 -93
  69. package/packages/btc-client/src/types.ts +0 -112
  70. package/packages/sdk/README.md +0 -277
  71. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  73. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  75. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  79. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  81. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  83. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  85. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  87. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  91. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  95. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  97. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  99. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/events.js +0 -0
  101. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  103. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  105. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  107. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  109. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  111. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/note.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  115. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  117. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  123. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  125. /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/psbt.js +0 -0
  127. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  131. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  135. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/solana/priority-fee.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/README.md CHANGED
@@ -1,170 +1,276 @@
1
- # UTXOpia SDK
1
+ # @utxopia/sdk
2
2
 
3
- TypeScript client for [UTXOpia](https://utxopia.com) a privacy-preserving Bitcoin-to-Solana
4
- bridge. Deposits arrive as commitments in a Merkle tree, transfers move between commitments with
5
- Groth16 JoinSplit proofs, and an amount only becomes public when someone withdraws.
3
+ TypeScript SDK for interacting with the UTXOpia protocol - a privacy-preserving Bitcoin-to-Solana bridge using Zero-Knowledge Proofs.
6
4
 
7
- > **Alpha.** `0.1.0-alpha.1`. The protocol has not been deployed to mainnet — do not point this at
8
- > funds you cannot lose.
9
- >
10
- > **The export surface is not a contract yet.** Roughly 430 things are exported; about a third are
11
- > exercised by the reference apps, and the rest is protocol surface nobody has needed yet or
12
- > helpers that were never deliberately made public. Being pre-1.0 is what makes that survivable:
13
- > the surface will be narrowed to the workflows below before 1.0, and anything outside them may
14
- > disappear in a minor release. Build against what `docs/API-MAP.md` marks as used; if you need
15
- > something else, open an issue so it gets kept on purpose rather than by accident.
16
-
17
- ## Install
5
+ ## Installation
18
6
 
19
7
  ```bash
20
8
  bun add @utxopia/sdk
9
+ # or
10
+ npm install @utxopia/sdk
21
11
  ```
22
12
 
23
- Installing from git instead? Pin a commit, never a branch. A floating `#main` means two apps that
24
- installed a week apart disagree about the note format, and the failure surfaces as a proof that
25
- will not verify rather than as an install error:
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import {
17
+ deriveKeysFromWallet,
18
+ createStealthDeposit,
19
+ scanAnnouncements,
20
+ resolveSnsName,
21
+ } from '@utxopia/sdk';
26
22
 
27
- ```jsonc
28
- // package.json
29
- "@utxopia/sdk": "github:UTXOpia/utxopia-sdk#<full-commit-sha>"
23
+ // 1. Derive keys from wallet
24
+ const keys = await deriveKeysFromWallet(walletAdapter);
25
+
26
+ // 2. Look up recipient by .utxopia.sol name
27
+ const recipient = await resolveSnsName(connection, 'alice');
28
+
29
+ // 3. Create stealth deposit
30
+ const deposit = await createStealthDeposit(recipient, 100000n);
31
+
32
+ // 4. Scan for incoming deposits
33
+ const notes = await scanAnnouncements(keys, announcements);
30
34
  ```
31
35
 
32
- `snarkjs` and `react` are optional peers — install `snarkjs` only if you generate proofs, `react`
33
- only if you use the hooks.
36
+ ## Core Features
37
+
38
+ ### Key Derivation
39
+
40
+ Derive spending and viewing keys from a Solana wallet signature (RAILGUN-style):
34
41
 
35
- ## What the pieces are
42
+ ```typescript
43
+ import { deriveKeysFromWallet, type UTXOpiaKeys } from '@utxopia/sdk';
44
+
45
+ const keys: UTXOpiaKeys = await deriveKeysFromWallet(walletAdapter);
46
+ // keys.spendingPubKey - for receiving funds
47
+ // keys.viewingPubKey - for scanning deposits
48
+ // keys.spendingPrivKey - for claiming (keep secret!)
49
+ // keys.viewingPrivKey - for scanning (can delegate)
50
+ ```
36
51
 
37
- | Concept | What it means here |
38
- |---|---|
39
- | **Note** | A commitment `Poseidon(npk, token, amount)` in the tree. Owning one means knowing the key behind `npk`. |
40
- | **Nullifier** | `Poseidon(nullifyingKey, leafIndex)`. Published on spend so a note cannot be spent twice; it reveals nothing about which note it was. |
41
- | **Stealth address** | A recipient publishes a meta-address; each sender derives a fresh one-time `npk` from it, so payments to the same person are unlinkable on-chain. |
42
- | **JoinSplit(N,M)** | One proof spends N notes and creates M. Shapes with `N + M ≤ 10` are accepted on-chain, 45 in total. |
43
- | **Viewing key** | Decrypts incoming announcements. Separate from the spending key, so scanning can be delegated. |
52
+ ### Stealth Addresses (EIP-5564/DKSAP Pattern)
44
53
 
45
- ## Quick start
54
+ Create private deposits that only the recipient can detect and claim:
46
55
 
47
56
  ```typescript
48
57
  import {
49
- deriveKeysFromWallet,
50
- resolveSnsName,
51
58
  createStealthDeposit,
52
59
  scanAnnouncements,
53
- } from "@utxopia/sdk";
60
+ prepareClaimInputs,
61
+ } from '@utxopia/sdk';
54
62
 
55
- // Keys come from a wallet signature over a fixed message — nothing to store.
56
- const keys = await deriveKeysFromWallet(walletAdapter);
63
+ // Sender: Create stealth deposit
64
+ const deposit = await createStealthDeposit(recipientMeta, amountSats);
65
+ // deposit.ephemeralPub - publish on-chain
66
+ // deposit.commitment - add to Merkle tree
67
+ // deposit.amountSats - verified BTC amount
57
68
 
58
- // Recipients can be a .utxopia.sol name instead of a raw meta-address.
59
- const recipient = await resolveSnsName(connection, "alice");
69
+ // Recipient: Scan for deposits
70
+ const notes = await scanAnnouncements(keys, onChainAnnouncements);
60
71
 
61
- const deposit = await createStealthDeposit(recipient, 100_000n);
72
+ // Recipient: Prepare claim inputs for ZK proof
73
+ const claimInputs = await prepareClaimInputs(keys, note, merkleProof);
74
+ ```
62
75
 
63
- // Scanning is a local trial-decrypt of every announcement — there is no
64
- // "my transactions" endpoint to ask, by design.
65
- const notes = await scanAnnouncements(keys, announcements);
76
+ ### Note Generation
77
+
78
+ Create and manage shielded notes:
79
+
80
+ ```typescript
81
+ import {
82
+ generateNote,
83
+ deriveNote,
84
+ createClaimLink,
85
+ parseClaimLink,
86
+ } from '@utxopia/sdk';
87
+
88
+ // Generate random note
89
+ const note = generateNote(100000n);
90
+
91
+ // Derive deterministic note from seed
92
+ const note = deriveNote('my-secret-phrase', 0, 100000n);
93
+
94
+ // Create shareable claim link
95
+ const link = createClaimLink(note);
96
+
97
+ // Parse claim link
98
+ const parsed = parseClaimLink(link);
66
99
  ```
67
100
 
68
- `docs/API-MAP.md` lists every export grouped by what you are building — config, keys, PDAs,
69
- notes, stealth, Bitcoin, proving, instructions, chain reads, disclosure — and marks which ones
70
- the reference apps actually use. `packages/sdk/README.md` covers each area in depth;
71
- `packages/sdk/docs/SDK.md` has the full type reference.
101
+ ### Taproot Address Derivation
72
102
 
73
- ## Entry points
103
+ Generate BTC deposit addresses:
74
104
 
75
105
  ```typescript
76
- import { } from "@utxopia/sdk"; // everything below is re-exported here
77
- import { … } from "@utxopia/sdk/prover/web"; // browser proving (snarkjs + WebAssembly)
78
- import { } from "@utxopia/sdk/prover/mobile"; // React Native proving
79
- import { } from "@utxopia/sdk/bitcoin"; // taproot, Ika custody
80
- import { } from "@utxopia/sdk/stealth"; // stealth addresses on their own
81
- import { … } from "@utxopia/sdk/btc-client"; // Esplora client, OP_RETURN encoding
106
+ import { deriveTaprootAddress, verifyTaprootAddress } from '@utxopia/sdk';
107
+
108
+ // Use the configured FROST/Ika custody public key; there is no safe default.
109
+ const custodyInternalKey = getConfiguredCustodyInternalKey();
110
+ const { address } = deriveTaprootAddress(
111
+ commitment,
112
+ 'testnet',
113
+ custodyInternalKey,
114
+ );
115
+
116
+ // Verify address matches commitment
117
+ const isValid = verifyTaprootAddress(address, commitment, custodyInternalKey);
82
118
  ```
83
119
 
84
- The root export is the whole surface. The subpaths exist so a bundle that only needs, say,
85
- Esplora does not pull in the prover.
120
+ ### Merkle Proofs
121
+
122
+ Work with the on-chain commitment tree:
123
+
124
+ ```typescript
125
+ import {
126
+ createMerkleProof,
127
+ proofToNoirFormat,
128
+ TREE_DEPTH,
129
+ } from '@utxopia/sdk';
130
+
131
+ const proof = createMerkleProof(leaves, leafIndex);
132
+ const noirProof = proofToNoirFormat(proof);
133
+ ```
86
134
 
87
- ## Naming
135
+ ## API Reference
88
136
 
89
- The prefix tells you what a call costs, so you can read an import list without opening anything:
137
+ ### Stealth Module
90
138
 
91
- | prefix | meaning |
92
- |---|---|
93
- | `derive*PDA` | pure address math, synchronous |
94
- | `fetch*` | hits the network always async |
95
- | `get*` | reads local or already-fetched state synchronous |
96
- | `parse*` | bytes in, typed object out; no IO |
97
- | `build*InstructionData` / `build*Instruction` | encode instruction data / assemble the full instruction |
98
- | `compute*Sync` | Poseidon hashes. Only the `Sync` form exists — poseidon-lite has nothing to await |
99
- | `encode`/`decode`, `pack`/`unpack`, `serialize`/`deserialize` | symmetric pairs, always both directions |
139
+ | Function | Description |
140
+ |----------|-------------|
141
+ | `createStealthDeposit(recipient, amount)` | Create stealth deposit for recipient |
142
+ | `scanAnnouncements(keys, announcements)` | Scan for deposits using viewing key |
143
+ | `prepareClaimInputs(keys, note, proof)` | Prepare inputs for ZK claim proof |
144
+ | `scanUnifiedNotes(keys, announcements)` | Scan announcement events for owned notes |
145
+ | `resolveSnsName(conn, name)` | Look up .utxopia.sol name to stealth address |
100
146
 
101
- `PDA` is spelled in caps. Seven MagicBlock helpers used to say `Pda` and two RPC calls were named
102
- `get*`; they were renamed before the first publish, while doing so was still free.
147
+ ### Key Derivation Module
103
148
 
104
- ## Proving
149
+ | Function | Description |
150
+ |----------|-------------|
151
+ | `deriveKeysFromWallet(wallet)` | Derive keys from wallet signature |
152
+ | `deriveKeysFromSignature(sig)` | Derive keys from raw signature |
153
+ | `deriveKeysFromSeed(seed)` | Derive keys from seed bytes |
154
+ | `createStealthMetaAddress(keys)` | Create stealth meta-address |
155
+ | `createDelegatedViewKey(keys, perms, expiry)` | Create delegated view key |
105
156
 
106
- Proof generation needs circuit artifacts — a `.wasm` witness generator and a `.zkey` — which are
107
- too large to ship in the package. Point the prover at a host serving them:
157
+ ### Constants
108
158
 
109
159
  ```typescript
110
- import { setCircuitPath, generateJoinSplitProof } from "@utxopia/sdk/prover/web";
160
+ // Program IDs
161
+ UTXOPIA_PROGRAM_ID // Main UTXOpia program (devnet)
162
+ CHADBUFFER_PROGRAM_ID // ChadBuffer for SPV proofs
163
+
164
+ // Merkle Tree
165
+ TREE_DEPTH // 20
166
+ MAX_LEAVES // 2^20
167
+ ZERO_VALUE // Empty leaf value
111
168
 
112
- setCircuitPath("https://circuit.utxopia.com/circuits/v2/groth16");
113
- const proof = await generateJoinSplitProof(inputs);
114
169
  ```
115
170
 
116
- **Pin the artifacts you are willing to prove with.** The `.wasm` is the witness generator: it
117
- receives every private input a JoinSplit has — spending key, nullifying key, note randomness,
118
- amounts, the full Merkle path — in the clear. It arrives over plain `fetch`, where subresource
119
- integrity does not apply, and CDNs serve it `immutable, max-age=31536000`, so a substituted file
120
- survives in browser and edge caches long after the origin is cleaned. Proofs built from a poisoned
121
- generator still verify, so nothing fails and no user notices.
171
+ ## Types
122
172
 
123
- ```typescript
124
- import { setCircuitArtifactDigests } from "@utxopia/sdk/prover/web";
125
- import digests from "./circuit-manifest.json"; // generated by your build, committed
173
+ ### UTXOpiaKeys
126
174
 
127
- setCircuitArtifactDigests(digests.artifacts);
175
+ ```typescript
176
+ interface UTXOpiaKeys {
177
+ spendingPubKey: BabyJubPoint;
178
+ spendingPrivKey: bigint;
179
+ viewingPubKey: Uint8Array;
180
+ viewingPrivKey: Uint8Array;
181
+ nullifyingKey: bigint;
182
+ }
128
183
  ```
129
184
 
130
- Keys are paths under the circuit base (`joinsplit_1x1/joinsplit_1x1_js/joinsplit_1x1.wasm`), so one
131
- manifest covers local dev and the CDN. Once set it fails closed: an artifact with no recorded
132
- digest is refused rather than trusted, so the manifest has to cover every shape your origin serves.
133
- The SDK cannot ship these — they change whenever circuits are rebuilt.
185
+ ### StealthDeposit
134
186
 
135
- ## Layout
187
+ ```typescript
188
+ interface StealthDeposit {
189
+ ephemeralPub: Uint8Array; // 32 bytes (Ed25519)
190
+ amountSats: bigint;
191
+ commitment: Uint8Array; // 32 bytes
192
+ createdAt: number;
193
+ }
194
+ ```
136
195
 
196
+ ### ScannedNote
197
+
198
+ ```typescript
199
+ interface ScannedNote {
200
+ amount: bigint;
201
+ ephemeralPub: Uint8Array;
202
+ stealthPub: BabyJubPoint;
203
+ leafIndex: number;
204
+ commitment: Uint8Array;
205
+ }
137
206
  ```
138
- packages/sdk/ the SDK — keys, notes, stealth, proving, Solana instructions
139
- packages/btc-client/ standalone Esplora client and deposit OP_RETURN encoding
207
+
208
+ ### ConnectionAdapter
209
+
210
+ ```typescript
211
+ interface ConnectionAdapter {
212
+ getAccountInfo: (
213
+ pubkey: { toBytes(): Uint8Array }
214
+ ) => Promise<{ data: Uint8Array } | null>;
215
+ }
140
216
  ```
141
217
 
218
+ ## Security Considerations
219
+
220
+ 1. **Never expose spending private key** - Only needed for claiming
221
+ 2. **Viewing key can be delegated** - For balance monitoring without spend capability
222
+ 3. **Nullifiers prevent double-spending** - Derived from spending key + leaf index
223
+ 4. **Commitments hide amounts** - Poseidon hash of NPK, token, and amount
224
+
142
225
  ## Development
143
226
 
144
227
  ```bash
228
+ # Install dependencies
145
229
  bun install
146
- bun run test # 782 tests
147
- bun run build # tsc
230
+
231
+ # Build
232
+ bun run build
233
+
234
+ # Run tests
235
+ bun test
236
+ ```
237
+
238
+ ## Verify it yourself: rebuild the tree from chain
239
+
240
+ Spending a note needs a Merkle proof, and a Merkle proof needs the whole leaf
241
+ set — which normally comes from our indexer. If the leaves weren't recoverable
242
+ from the chain itself, "you can exit without the operator" would be a promise
243
+ rather than a property.
244
+
245
+ This script rebuilds a pool's entire leaf set from Solana transaction logs and
246
+ checks the result against the root the program is verifying against. It talks to
247
+ a public RPC endpoint and nothing else — no backend, no indexer, no API key:
248
+
249
+ ```bash
250
+ TREE=<commitment-tree-pda> bun run scripts/rebuild-tree-from-chain.ts
148
251
  ```
149
252
 
150
- Tests are pure unit tests — no network, no validator, no proving. They run in about eight seconds,
151
- so there is no reason to skip them.
253
+ ```
254
+ on-chain: 123 leaves, root 2994f7d670d12cd8dcbd89af708ff55f9e877bbb387aac4cf09e688a615650ce
255
+ 134/134 scanned, 123 leaves
256
+ recovered 123/123 leaves from logs
257
+ rebuilt: 123 leaves, root 2994f7d670d12cd8dcbd89af708ff55f9e877bbb387aac4cf09e688a615650ce
152
258
 
153
- ## Security
259
+ MATCH — the leaf set is recoverable from chain alone
260
+ ```
154
261
 
155
- Two things worth knowing before you build on this:
262
+ It exits non-zero on a mismatch or on missing leaves, so it works as a check in
263
+ CI as well as by hand.
156
264
 
157
- - **`createDelegatedViewKey` currently hands over the `nullifyingKey`.** Because
158
- `nullifier = Poseidon(nullifyingKey, leafIndex)` does not depend on note contents and leaf
159
- indices are small public integers, anyone holding it can compute every nullifier that key will
160
- ever produce and link the owner's entire spend history, retroactively and permanently. The
161
- `fromSlot`/`toSlot` and FULL/SCAN/INCOMING_ONLY fields on a delegated key are metadata, not
162
- enforcement. Treat a delegated viewing key as handing over the whole account, not a slice of it.
163
- `extractViewOnlyBundle` correctly omits the key and is the safer thing to reach for.
164
- - Keys derive from a wallet signature, so **any party who can make that wallet sign the derivation
165
- message can reconstruct the shielded identity.** A hardware wallet does not change this.
265
+ | Env | Default | |
266
+ |---|---|---|
267
+ | `RPC` | devnet | Any Solana RPC endpoint |
268
+ | `TREE` | `DEVNET_CONFIG.commitmentTreePda` | Commitment tree PDA to rebuild |
269
+ | `EPOCH_SIG` | | Tree's `INITIALIZE` signature. Optional; set it to skip a dead epoch if the PDA was closed and recreated |
270
+ | `PACE_MS` | `120` | Delay between requests. Raise it if your endpoint throttles you |
166
271
 
167
- Report anything else privately rather than in a public issue.
272
+ Public endpoints rate-limit this scan aggressively the script paces itself and
273
+ backs off, so expect it to take a minute or two rather than to fail.
168
274
 
169
275
  ## License
170
276
 
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { type NetworkConfig, type NetworkId } from "./config";
20
20
  import { type AuthSignatureKeyDerivationOptions, type UTXOpiaKeys, type StealthMetaAddress, type WalletSignerAdapter, type KeySetupResult } from "./keys";
21
- import { type ViewOnlyKeys, type StealthOutputWithKeys, type NonInteractiveDepositResult } from "./stealth";
21
+ import { type ViewOnlyKeys, type StealthOutputWithKeys, type NonInteractiveDepositResult, type TweakDepositResult } from "./stealth";
22
22
  import { type UtxoDescriptor } from "./psbt";
23
23
  import { type DepositOpReturnContext } from "./taproot";
24
24
  export interface UTXOpiaClientConfig {
@@ -137,6 +137,30 @@ export declare class UTXOpiaClient {
137
137
  computeNullifier(note: {
138
138
  leafIndex: number;
139
139
  }): Uint8Array;
140
+ /**
141
+ * Prepare an OP_RETURN-free BTC deposit address (`verify_deposit`, disc 25).
142
+ *
143
+ * The ephemeral key is derived from this wallet's own viewing node and
144
+ * `depositIndex`, never randomly. A deposit address commits to the ephemeral
145
+ * key through its tapleaf and its key path is a NUMS point, so an address whose
146
+ * ephemeral key is lost is one nobody can ever spend. Indexing it makes the
147
+ * viewing key a complete backup: walk `depositIndex` upward and every address
148
+ * comes back.
149
+ *
150
+ * Self-deposit only. The address is recovered by whoever holds the viewing key
151
+ * it was derived from, so generating one *for someone else* would hand them a
152
+ * note they own but coins they could never recover. Pay a third party with a
153
+ * shielded transfer instead.
154
+ *
155
+ * `depositIndex` must be persisted and monotonic. Reusing one re-derives the
156
+ * same address, which is safe on chain but links the two deposits.
157
+ */
158
+ prepareTweakDeposit(opts: {
159
+ depositIndex: number;
160
+ ikaXOnlyPubkey: Uint8Array;
161
+ recipient?: StealthMetaAddress;
162
+ network?: "mainnet" | "testnet" | "regtest";
163
+ }): Promise<TweakDepositResult>;
140
164
  /**
141
165
  * Prepare a BTC deposit: generate stealth deposit address + OP_RETURN.
142
166
  * Returns the deposit result (btcAddress, opReturnPayload) ready for PSBT building.
@@ -22,7 +22,7 @@ import { initConfig, getConfig } from "./config";
22
22
  import { parseMerkleProofResponse } from "./merkle";
23
23
  import { eddsaPoseidonSign } from "./keys";
24
24
  import { setupKeysFromWallet, setupKeysFromSeed, setupKeysFromAuthSignature, recreateStealthAddress, serializeKeysForStorage, deserializeKeysFromStorage, clearUTXOpiaKeys, } from "./keys";
25
- import { scanUnifiedNotesMulti, scanAnnouncementsViewOnlyMulti, computeNullifierHashForNote, computeNullifierBytes, isDepositForViewerHex, createDepositFromConfig, createStealthOutputWithKeys, } from "./stealth";
25
+ import { scanUnifiedNotesMulti, scanAnnouncementsViewOnlyMulti, computeNullifierHashForNote, computeNullifierBytes, isDepositForViewerHex, createDepositFromConfig, createTweakDeposit, depositViewingNode, createStealthOutputWithKeys, } from "./stealth";
26
26
  import { selectUtxos } from "./psbt";
27
27
  import { hexToBytes } from "./crypto";
28
28
  import { EventClient } from "./event-client";
@@ -308,6 +308,41 @@ export class UTXOpiaClient {
308
308
  throw new Error("Not authenticated");
309
309
  }
310
310
  // ─── Phase 2: Deposit + Shield ─────────────────────────────────
311
+ /**
312
+ * Prepare an OP_RETURN-free BTC deposit address (`verify_deposit`, disc 25).
313
+ *
314
+ * The ephemeral key is derived from this wallet's own viewing node and
315
+ * `depositIndex`, never randomly. A deposit address commits to the ephemeral
316
+ * key through its tapleaf and its key path is a NUMS point, so an address whose
317
+ * ephemeral key is lost is one nobody can ever spend. Indexing it makes the
318
+ * viewing key a complete backup: walk `depositIndex` upward and every address
319
+ * comes back.
320
+ *
321
+ * Self-deposit only. The address is recovered by whoever holds the viewing key
322
+ * it was derived from, so generating one *for someone else* would hand them a
323
+ * note they own but coins they could never recover. Pay a third party with a
324
+ * shielded transfer instead.
325
+ *
326
+ * `depositIndex` must be persisted and monotonic. Reusing one re-derives the
327
+ * same address, which is safe on chain but links the two deposits.
328
+ */
329
+ async prepareTweakDeposit(opts) {
330
+ if (!this._keys) {
331
+ throw new Error("No keys (login first)");
332
+ }
333
+ const meta = opts.recipient ?? this._stealthAddress;
334
+ if (!meta)
335
+ throw new Error("No recipient stealth address (login first or provide recipient)");
336
+ if (opts.recipient && this._stealthAddress && opts.recipient !== this._stealthAddress) {
337
+ throw new Error("prepareTweakDeposit is self-deposit only: the recipient must be able to derive the " +
338
+ "ephemeral key to recover the coins, and only this wallet's viewing key can");
339
+ }
340
+ const network = opts.network ?? sdkBitcoinNetworkToAddressNetwork(this.config.bitcoinNetwork);
341
+ return createTweakDeposit(meta, opts.ikaXOnlyPubkey, {
342
+ viewingNode: depositViewingNode(this._keys.viewingPrivKey),
343
+ depositIndex: opts.depositIndex,
344
+ }, network);
345
+ }
311
346
  /**
312
347
  * Prepare a BTC deposit: generate stealth deposit address + OP_RETURN.
313
348
  * Returns the deposit result (btcAddress, opReturnPayload) ready for PSBT building.
@@ -20,7 +20,7 @@ import { type Address } from "@solana/kit";
20
20
  */
21
21
  export declare function address(input: string): Address;
22
22
  export type NetworkType = "devnet" | "mainnet" | "localnet";
23
- export type AppNetworkId = NetworkType | "devnet-regtest";
23
+ export type AppNetworkId = NetworkType | "devnet-regtest" | "devnet-testnet4";
24
24
  export interface NetworkConfig {
25
25
  /** Network identifier */
26
26
  network: NetworkType;
@@ -99,10 +99,41 @@ export declare const CHADBUFFER_PROGRAM_ID: Address;
99
99
  /** ChadBuffer Program ID for localnet testing */
100
100
  export declare const LOCALNET_CHADBUFFER_PROGRAM_ID: Address;
101
101
  /**
102
- * Fresh multi-pool devnet deployment (2026-07-26).
103
- * The default domain is public; institution pools are selected explicitly.
102
+ * Greenfield devnet + Bitcoin testnet4 deployment (2026-08-26).
103
+ *
104
+ * The previous deployment's programs were closed on chain, so this is not a redeploy and none
105
+ * of the old addresses resolve any more — anything still pinned to CvfSyACR… is talking to a
106
+ * program that no longer exists.
107
+ *
108
+ * Two pools were deployed, each with its own Ika DKG and therefore its own dWallet. The default
109
+ * below is the OPEN pool. Select the verified pool by overriding `zkbtcMint` and
110
+ * `ikaDwalletXOnlyPubkey` — `getConfig` re-derives poolStatePda, commitmentTreePda and poolVault
111
+ * from the mint, so those two values are all a caller needs:
112
+ *
113
+ * verified mint G78CTddWGDaNaSKQayAt7m3pzcMyaUNxgR8y3R34YvEv
114
+ * state Eqn9SmFYtacrdfPE9Shbi8bDXLjCNNz3WhHfqtJnwbKY
115
+ * xonly 16c563baa11bfc8fe93acafe8fe169b954b3ead3ecda7754c615dec9e840b5a5
116
+ *
117
+ * Read off chain, not from the deploy notes: Eqn9SmFY… is the pool whose PoolState flags carry
118
+ * the permissioned bit (0b10 at offset 2). The deployment also contains a THIRD pool —
119
+ * 3chHiDqM… / 7wDtDd1u… — which the deploy notes name as the verified one but which is not
120
+ * permissioned and whose mint has zero supply. Treat it as abandoned; do not wire it up.
104
121
  */
105
122
  export declare const DEVNET_CONFIG: NetworkConfig;
123
+ /**
124
+ * Solana devnet + Bitcoin **regtest** deployment — the environment app.utxopia.com serves.
125
+ *
126
+ * This is a different deployment from DEVNET_CONFIG above, not a Bitcoin-side variation of it:
127
+ * a different program (CvfSyACR…), a different pool, a different Ika dWallet. It used to share
128
+ * DEVNET_CONFIG and differ only by bitcoinNetwork, which worked while one program hosted both.
129
+ * It no longer does — after 2026-08-26 there are two programs — so `devnet-regtest` resolving
130
+ * to DEVNET_CONFIG would silently point this environment at testnet4's program and pool.
131
+ *
132
+ * Pool addresses derived from the mint with the same seeds getConfig uses, then confirmed on
133
+ * devnet: pool_state and commitment_tree owned by CvfSyACR… at 332 and 8816 bytes, the vault by
134
+ * Token-2022 at 170.
135
+ */
136
+ export declare const DEVNET_REGTEST_CONFIG: NetworkConfig;
106
137
  /**
107
138
  * Mainnet Configuration (placeholder - not yet deployed)
108
139
  */