@utxopia/sdk 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- package/packages/sdk/dist/vk-registry.js +212 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note (shielded commitment) utilities for UTXOpia
|
|
3
|
+
*
|
|
4
|
+
* A Note represents a shielded deposit with:
|
|
5
|
+
* - nullifier: Random secret for spending
|
|
6
|
+
* - secret: Additional entropy
|
|
7
|
+
* - amount: Value in satoshis
|
|
8
|
+
*
|
|
9
|
+
* Hash values (commitment, nullifierHash) are computed by circom circuits
|
|
10
|
+
* using Poseidon. This SDK stores the raw secrets and optionally
|
|
11
|
+
* accepts pre-computed hash values.
|
|
12
|
+
*/
|
|
13
|
+
import { randomFieldElement, bigintToBytes, sha256Hash, bytesToBigint, BN254_FIELD_PRIME, babyJubMul, BABYJUB_BASE8 } from "./crypto";
|
|
14
|
+
import { computeUnifiedCommitmentSync, computeNullifierSync, hashNullifierSync, computeJoinSplitCommitmentSync, computeNPKSync, computeJoinSplitNullifierSync } from "./poseidon";
|
|
15
|
+
/**
|
|
16
|
+
* Generate a new note with random nullifier and secret
|
|
17
|
+
*
|
|
18
|
+
* Note: Hash values are NOT computed here. They will be computed
|
|
19
|
+
* by the circom circuits during proof generation.
|
|
20
|
+
*
|
|
21
|
+
* @param amountSats - Amount in satoshis
|
|
22
|
+
* @returns Note with secrets (hashes are 0n until circuit execution)
|
|
23
|
+
*/
|
|
24
|
+
export function generateNote(amountSats) {
|
|
25
|
+
const nullifier = randomFieldElement();
|
|
26
|
+
const secret = randomFieldElement();
|
|
27
|
+
return createNoteFromSecrets(nullifier, secret, amountSats);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a note from known secrets
|
|
31
|
+
*
|
|
32
|
+
* @param nullifier - Nullifier field element
|
|
33
|
+
* @param secret - Secret field element
|
|
34
|
+
* @param amountSats - Amount in satoshis
|
|
35
|
+
* @param commitment - Optional pre-computed commitment (from circuit)
|
|
36
|
+
* @param nullifierHash - Optional pre-computed nullifier hash (from circuit)
|
|
37
|
+
* @returns Note structure
|
|
38
|
+
*/
|
|
39
|
+
export function createNoteFromSecrets(nullifier, secret, amountSats, commitment, nullifierHash) {
|
|
40
|
+
// Computed values are 0n until provided from circuit outputs
|
|
41
|
+
const comm = commitment ?? 0n;
|
|
42
|
+
const nullHash = nullifierHash ?? 0n;
|
|
43
|
+
const noteHash = 0n; // Intermediate hash not typically exposed
|
|
44
|
+
return {
|
|
45
|
+
amount: amountSats,
|
|
46
|
+
nullifier,
|
|
47
|
+
secret,
|
|
48
|
+
note: noteHash,
|
|
49
|
+
commitment: comm,
|
|
50
|
+
nullifierHash: nullHash,
|
|
51
|
+
nullifierBytes: bigintToBytes(nullifier),
|
|
52
|
+
secretBytes: bigintToBytes(secret),
|
|
53
|
+
commitmentBytes: bigintToBytes(comm),
|
|
54
|
+
nullifierHashBytes: bigintToBytes(nullHash),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Update note with computed hash values from circom circuit execution
|
|
59
|
+
*
|
|
60
|
+
* @param note - Note to update
|
|
61
|
+
* @param commitment - Commitment from circuit output
|
|
62
|
+
* @param nullifierHash - Nullifier hash from circuit output
|
|
63
|
+
* @returns Updated note
|
|
64
|
+
*/
|
|
65
|
+
export function updateNoteWithHashes(note, commitment, nullifierHash) {
|
|
66
|
+
return {
|
|
67
|
+
...note,
|
|
68
|
+
commitment,
|
|
69
|
+
nullifierHash,
|
|
70
|
+
commitmentBytes: bigintToBytes(commitment),
|
|
71
|
+
nullifierHashBytes: bigintToBytes(nullifierHash),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Serialize a note for storage or transmission
|
|
76
|
+
*
|
|
77
|
+
* Only stores the essential data (amount, nullifier, secret).
|
|
78
|
+
* Optionally includes pre-computed hash values.
|
|
79
|
+
*/
|
|
80
|
+
export function serializeNote(note) {
|
|
81
|
+
const serialized = {
|
|
82
|
+
amount: note.amount.toString(),
|
|
83
|
+
nullifier: note.nullifier.toString(),
|
|
84
|
+
secret: note.secret.toString(),
|
|
85
|
+
};
|
|
86
|
+
// Include computed values if available
|
|
87
|
+
if (note.commitment !== 0n) {
|
|
88
|
+
serialized.commitment = note.commitment.toString();
|
|
89
|
+
}
|
|
90
|
+
if (note.nullifierHash !== 0n) {
|
|
91
|
+
serialized.nullifierHash = note.nullifierHash.toString();
|
|
92
|
+
}
|
|
93
|
+
return serialized;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Deserialize and restore a note from stored data
|
|
97
|
+
*/
|
|
98
|
+
export function deserializeNote(data) {
|
|
99
|
+
const amount = BigInt(data.amount);
|
|
100
|
+
const nullifier = BigInt(data.nullifier);
|
|
101
|
+
const secret = BigInt(data.secret);
|
|
102
|
+
const commitment = data.commitment ? BigInt(data.commitment) : undefined;
|
|
103
|
+
const nullifierHash = data.nullifierHash
|
|
104
|
+
? BigInt(data.nullifierHash)
|
|
105
|
+
: undefined;
|
|
106
|
+
return createNoteFromSecrets(nullifier, secret, amount, commitment, nullifierHash);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format satoshis as BTC string
|
|
110
|
+
*/
|
|
111
|
+
export function formatBtc(sats) {
|
|
112
|
+
const btc = Number(sats) / 100000000;
|
|
113
|
+
return btc.toFixed(8) + " BTC";
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse BTC string to satoshis
|
|
117
|
+
*/
|
|
118
|
+
export function parseBtc(btcString) {
|
|
119
|
+
const btc = parseFloat(btcString.replace(" BTC", ""));
|
|
120
|
+
return BigInt(Math.round(btc * 100000000));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if a note has computed hash values
|
|
124
|
+
*/
|
|
125
|
+
export function noteHasComputedHashes(note) {
|
|
126
|
+
return note.commitment !== 0n && note.nullifierHash !== 0n;
|
|
127
|
+
}
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Note Commitment/Nullifier Helpers
|
|
130
|
+
// ============================================================================
|
|
131
|
+
/**
|
|
132
|
+
* Get the public key X coordinate from a note's nullifier (used as private key)
|
|
133
|
+
*
|
|
134
|
+
* In the unified model: pubKey = nullifier * BASE8 (on Baby Jubjub curve)
|
|
135
|
+
* Returns pubKey.x for commitment computation
|
|
136
|
+
*/
|
|
137
|
+
export function getNotePublicKeyX(note) {
|
|
138
|
+
const pubKey = babyJubMul(note.nullifier, BABYJUB_BASE8);
|
|
139
|
+
return pubKey.x;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Compute the commitment for a note
|
|
143
|
+
*
|
|
144
|
+
* commitment = Poseidon(pubKeyX, amount)
|
|
145
|
+
* where pubKeyX = (nullifier * G).x
|
|
146
|
+
*
|
|
147
|
+
* Returns the note with commitment fields populated
|
|
148
|
+
*/
|
|
149
|
+
export function computeNoteCommitment(note) {
|
|
150
|
+
const pubKeyX = getNotePublicKeyX(note);
|
|
151
|
+
const commitment = computeUnifiedCommitmentSync(pubKeyX, note.amount);
|
|
152
|
+
const commitmentBytes = bigintToBytes(commitment);
|
|
153
|
+
return {
|
|
154
|
+
...note,
|
|
155
|
+
commitment,
|
|
156
|
+
commitmentBytes,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Compute the nullifier and nullifier hash for a note at a given leaf index
|
|
161
|
+
*
|
|
162
|
+
* nullifier = Poseidon(privKey, leafIndex)
|
|
163
|
+
* nullifierHash = Poseidon(nullifier)
|
|
164
|
+
*
|
|
165
|
+
* @returns Object with computed nullifier values
|
|
166
|
+
*/
|
|
167
|
+
export function computeNoteNullifier(note, leafIndex) {
|
|
168
|
+
const nullifier = computeNullifierSync(note.nullifier, leafIndex);
|
|
169
|
+
const nullifierHash = hashNullifierSync(nullifier);
|
|
170
|
+
const nullifierHashBytes = bigintToBytes(nullifierHash);
|
|
171
|
+
return {
|
|
172
|
+
nullifier,
|
|
173
|
+
nullifierHash,
|
|
174
|
+
nullifierHashBytes,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Deterministic Note Derivation (HD-style)
|
|
179
|
+
// ============================================================================
|
|
180
|
+
/**
|
|
181
|
+
* Derive a note deterministically from a seed phrase/name and index.
|
|
182
|
+
*
|
|
183
|
+
* This allows recovery of all notes from just the seed + index.
|
|
184
|
+
* Similar to HD wallets in Bitcoin.
|
|
185
|
+
*
|
|
186
|
+
* The seed is first hashed to create a 32-byte master key, then
|
|
187
|
+
* nullifier and secret are derived from that master key.
|
|
188
|
+
*
|
|
189
|
+
* @param seed - Seed phrase, name, or password (any string)
|
|
190
|
+
* @param index - Note index (0, 1, 2, ...)
|
|
191
|
+
* @param amountSats - Amount in satoshis
|
|
192
|
+
* @returns Note with deterministically derived secrets
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* // Derive notes from a name
|
|
197
|
+
* const note0 = deriveNote("albertgogogo", 0, 100_000n);
|
|
198
|
+
* const note1 = deriveNote("albertgogogo", 1, 50_000n);
|
|
199
|
+
*
|
|
200
|
+
* // Later: recover the same notes
|
|
201
|
+
* const recovered = deriveNote("albertgogogo", 0, 100_000n);
|
|
202
|
+
* // recovered.nullifier === note0.nullifier ✓
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function deriveNote(seed, index, amountSats) {
|
|
206
|
+
// Step 1: Hash seed to get 32-byte master key (normalizes any input)
|
|
207
|
+
const master = deriveMasterKey(seed);
|
|
208
|
+
// Step 2: Derive nullifier and secret from master + index
|
|
209
|
+
const nullifier = deriveFromMaster(master, index, 0); // 0 = nullifier domain
|
|
210
|
+
const secret = deriveFromMaster(master, index, 1); // 1 = secret domain
|
|
211
|
+
return createNoteFromSecrets(nullifier, secret, amountSats);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Derive master key from seed (32 bytes)
|
|
215
|
+
*
|
|
216
|
+
* master = SHA256(seed)
|
|
217
|
+
*
|
|
218
|
+
* You can cache this and use deriveNoteFromMaster() for efficiency.
|
|
219
|
+
*/
|
|
220
|
+
export function deriveMasterKey(seed) {
|
|
221
|
+
const encoder = new TextEncoder();
|
|
222
|
+
const seedBytes = encoder.encode(seed);
|
|
223
|
+
return sha256Hash(seedBytes);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Derive a note from a pre-computed master key
|
|
227
|
+
*
|
|
228
|
+
* More efficient if deriving many notes from the same seed.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const master = deriveMasterKey("albertgogogo");
|
|
233
|
+
* const note0 = deriveNoteFromMaster(master, 0, 100_000n);
|
|
234
|
+
* const note1 = deriveNoteFromMaster(master, 1, 50_000n);
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function deriveNoteFromMaster(master, index, amountSats) {
|
|
238
|
+
const nullifier = deriveFromMaster(master, index, 0);
|
|
239
|
+
const secret = deriveFromMaster(master, index, 1);
|
|
240
|
+
return createNoteFromSecrets(nullifier, secret, amountSats);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Derive field element from master key + index + domain
|
|
244
|
+
*
|
|
245
|
+
* result = SHA256(master || index || domain) mod BN254_PRIME
|
|
246
|
+
*
|
|
247
|
+
* @param master - 32-byte master key
|
|
248
|
+
* @param index - Note index
|
|
249
|
+
* @param domain - 0 for nullifier, 1 for secret
|
|
250
|
+
*/
|
|
251
|
+
function deriveFromMaster(master, index, domain) {
|
|
252
|
+
// Build input: master (32) || index (4) || domain (1) = 37 bytes
|
|
253
|
+
const input = new Uint8Array(37);
|
|
254
|
+
input.set(master, 0);
|
|
255
|
+
// Index as 4 bytes little-endian
|
|
256
|
+
const view = new DataView(input.buffer);
|
|
257
|
+
view.setUint32(32, index, true);
|
|
258
|
+
// Domain as 1 byte
|
|
259
|
+
input[36] = domain;
|
|
260
|
+
// Hash and reduce to field
|
|
261
|
+
const hash = sha256Hash(input);
|
|
262
|
+
return bytesToBigint(hash) % BN254_FIELD_PRIME;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Derive multiple notes at once from a seed
|
|
266
|
+
*
|
|
267
|
+
* More efficient than calling deriveNote() multiple times
|
|
268
|
+
* because master key is computed only once.
|
|
269
|
+
*
|
|
270
|
+
* @param seed - Seed phrase
|
|
271
|
+
* @param amounts - Array of amounts for each note
|
|
272
|
+
* @param startIndex - Starting index (default 0)
|
|
273
|
+
* @returns Array of derived notes
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* // Create a wallet with 3 notes
|
|
278
|
+
* const notes = deriveNotes("albertgogogo", [100_000n, 50_000n, 25_000n]);
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function deriveNotes(seed, amounts, startIndex = 0) {
|
|
282
|
+
// Compute master once, derive all notes from it
|
|
283
|
+
const master = deriveMasterKey(seed);
|
|
284
|
+
return amounts.map((amount, i) => deriveNoteFromMaster(master, startIndex + i, amount));
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Check the strength of a seed phrase
|
|
288
|
+
*
|
|
289
|
+
* Returns estimated bits of entropy.
|
|
290
|
+
* Recommended: >= 80 bits for moderate security, >= 128 bits for high security
|
|
291
|
+
*/
|
|
292
|
+
export function estimateSeedStrength(seed) {
|
|
293
|
+
// Very rough entropy estimation
|
|
294
|
+
const length = seed.length;
|
|
295
|
+
const hasLower = /[a-z]/.test(seed);
|
|
296
|
+
const hasUpper = /[A-Z]/.test(seed);
|
|
297
|
+
const hasDigit = /[0-9]/.test(seed);
|
|
298
|
+
const hasSpecial = /[^a-zA-Z0-9]/.test(seed);
|
|
299
|
+
let charsetSize = 0;
|
|
300
|
+
if (hasLower)
|
|
301
|
+
charsetSize += 26;
|
|
302
|
+
if (hasUpper)
|
|
303
|
+
charsetSize += 26;
|
|
304
|
+
if (hasDigit)
|
|
305
|
+
charsetSize += 10;
|
|
306
|
+
if (hasSpecial)
|
|
307
|
+
charsetSize += 32;
|
|
308
|
+
// Entropy = length * log2(charsetSize)
|
|
309
|
+
const bits = Math.floor(length * Math.log2(charsetSize || 1));
|
|
310
|
+
let strength;
|
|
311
|
+
let warning;
|
|
312
|
+
if (bits < 40) {
|
|
313
|
+
strength = "weak";
|
|
314
|
+
warning = "DANGER: This seed can be easily brute-forced. Use a longer passphrase!";
|
|
315
|
+
}
|
|
316
|
+
else if (bits < 80) {
|
|
317
|
+
strength = "moderate";
|
|
318
|
+
warning = "Consider using a longer passphrase for better security.";
|
|
319
|
+
}
|
|
320
|
+
else if (bits < 128) {
|
|
321
|
+
strength = "strong";
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
strength = "very_strong";
|
|
325
|
+
}
|
|
326
|
+
return { bits, strength, warning };
|
|
327
|
+
}
|
|
328
|
+
// Poseidon initialization state (for API compatibility)
|
|
329
|
+
/**
|
|
330
|
+
* Create a simple note (alias for generateNote but returns NoteData)
|
|
331
|
+
*
|
|
332
|
+
* Use this to generate note data for circom circuit inputs.
|
|
333
|
+
*
|
|
334
|
+
* @param amount - Amount in satoshis
|
|
335
|
+
* @returns NoteData with nullifier, secret, and amount
|
|
336
|
+
*/
|
|
337
|
+
export function createNote(amount) {
|
|
338
|
+
const note = generateNote(amount);
|
|
339
|
+
return {
|
|
340
|
+
nullifier: note.nullifier,
|
|
341
|
+
secret: note.secret,
|
|
342
|
+
amount: note.amount,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
// initPoseidon/isPoseidonReady used to live here as well, over a flag local to this module. The
|
|
346
|
+
// barrel exported initPoseidon from poseidon.ts and isPoseidonReady from here, so they read
|
|
347
|
+
// different state: `await initPoseidon(); isPoseidonReady()` returned false forever. Both now
|
|
348
|
+
// belong to poseidon.ts, which owns the actual hasher.
|
|
349
|
+
/**
|
|
350
|
+
* Prepare withdrawal - creates change note for remaining balance
|
|
351
|
+
*
|
|
352
|
+
* PRIVACY: All withdrawals are indistinguishable on-chain.
|
|
353
|
+
* Change amount can be >= 0, and the commitment always looks random.
|
|
354
|
+
*
|
|
355
|
+
* @param inputNote - Note being spent
|
|
356
|
+
* @param withdrawAmount - Amount to withdraw
|
|
357
|
+
* @returns Change note with remaining balance (can be 0)
|
|
358
|
+
*/
|
|
359
|
+
export function prepareWithdrawal(inputNote, withdrawAmount) {
|
|
360
|
+
if (withdrawAmount <= 0n) {
|
|
361
|
+
throw new Error("Withdraw amount must be positive");
|
|
362
|
+
}
|
|
363
|
+
if (withdrawAmount > inputNote.amount) {
|
|
364
|
+
throw new Error("Withdraw amount exceeds note balance");
|
|
365
|
+
}
|
|
366
|
+
const changeAmount = inputNote.amount - withdrawAmount;
|
|
367
|
+
const changeNote = createNote(changeAmount);
|
|
368
|
+
return { changeNote, changeAmount };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Create a stealth note from scanned announcement data
|
|
372
|
+
*
|
|
373
|
+
* @param amount - Decrypted amount
|
|
374
|
+
* @param random - Decrypted random value
|
|
375
|
+
* @param ephemeralSpendPub - Sender's ephemeral pubkey
|
|
376
|
+
* @param leafIndex - Merkle tree leaf index
|
|
377
|
+
* @returns StealthNote structure
|
|
378
|
+
*/
|
|
379
|
+
export function createStealthNote(amount, random, ephemeralSpendPub, leafIndex) {
|
|
380
|
+
return {
|
|
381
|
+
amount,
|
|
382
|
+
random,
|
|
383
|
+
ephemeralSpendPubX: ephemeralSpendPub.x,
|
|
384
|
+
ephemeralSpendPubY: ephemeralSpendPub.y,
|
|
385
|
+
leafIndex,
|
|
386
|
+
notePubKey: 0n, // Computed in circuit
|
|
387
|
+
commitment: 0n, // Computed in circuit
|
|
388
|
+
randomBytes: bigintToBytes(random),
|
|
389
|
+
commitmentBytes: new Uint8Array(32),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Update stealth note with computed values from circuit
|
|
394
|
+
*/
|
|
395
|
+
export function updateStealthNoteWithHashes(note, notePubKey, commitment) {
|
|
396
|
+
return {
|
|
397
|
+
...note,
|
|
398
|
+
notePubKey,
|
|
399
|
+
commitment,
|
|
400
|
+
commitmentBytes: bigintToBytes(commitment),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Serialize stealth note for storage
|
|
405
|
+
*/
|
|
406
|
+
export function serializeStealthNote(note) {
|
|
407
|
+
const serialized = {
|
|
408
|
+
amount: note.amount.toString(),
|
|
409
|
+
random: note.random.toString(),
|
|
410
|
+
ephemeralSpendPubX: note.ephemeralSpendPubX.toString(),
|
|
411
|
+
ephemeralSpendPubY: note.ephemeralSpendPubY.toString(),
|
|
412
|
+
leafIndex: note.leafIndex,
|
|
413
|
+
};
|
|
414
|
+
if (note.notePubKey !== 0n) {
|
|
415
|
+
serialized.notePubKey = note.notePubKey.toString();
|
|
416
|
+
}
|
|
417
|
+
if (note.commitment !== 0n) {
|
|
418
|
+
serialized.commitment = note.commitment.toString();
|
|
419
|
+
}
|
|
420
|
+
return serialized;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Deserialize stealth note from storage
|
|
424
|
+
*/
|
|
425
|
+
export function deserializeStealthNote(data) {
|
|
426
|
+
const note = createStealthNote(BigInt(data.amount), BigInt(data.random), {
|
|
427
|
+
x: BigInt(data.ephemeralSpendPubX),
|
|
428
|
+
y: BigInt(data.ephemeralSpendPubY),
|
|
429
|
+
}, data.leafIndex);
|
|
430
|
+
if (data.notePubKey && data.commitment) {
|
|
431
|
+
return updateStealthNoteWithHashes(note, BigInt(data.notePubKey), BigInt(data.commitment));
|
|
432
|
+
}
|
|
433
|
+
return note;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Check if stealth note has computed hashes
|
|
437
|
+
*/
|
|
438
|
+
export function stealthNoteHasComputedHashes(note) {
|
|
439
|
+
return note.notePubKey !== 0n && note.commitment !== 0n;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Create a JoinSplit note from key components
|
|
443
|
+
*
|
|
444
|
+
* @param tokenId - Token identifier from computeTokenId(mintBytes)
|
|
445
|
+
*/
|
|
446
|
+
export function createJoinSplitNote(mpk, random, amount, tokenId, leafIndex = -1) {
|
|
447
|
+
const npk = computeNPKSync(mpk, random);
|
|
448
|
+
const commitment = computeJoinSplitCommitmentSync(npk, tokenId, amount);
|
|
449
|
+
return {
|
|
450
|
+
npk,
|
|
451
|
+
token: tokenId,
|
|
452
|
+
amount,
|
|
453
|
+
random,
|
|
454
|
+
leafIndex,
|
|
455
|
+
commitment,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Compute nullifier for a JoinSplit note
|
|
460
|
+
*/
|
|
461
|
+
export function computeJoinSplitNoteNullifier(nullifyingKey, leafIndex) {
|
|
462
|
+
return computeJoinSplitNullifierSync(nullifyingKey, BigInt(leafIndex));
|
|
463
|
+
}
|
|
464
|
+
export function serializeJoinSplitNote(note) {
|
|
465
|
+
return {
|
|
466
|
+
npk: note.npk.toString(),
|
|
467
|
+
token: note.token.toString(),
|
|
468
|
+
amount: note.amount.toString(),
|
|
469
|
+
random: note.random.toString(),
|
|
470
|
+
leafIndex: note.leafIndex,
|
|
471
|
+
commitment: note.commitment.toString(),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
export function deserializeJoinSplitNote(data) {
|
|
475
|
+
return {
|
|
476
|
+
npk: BigInt(data.npk),
|
|
477
|
+
token: BigInt(data.token),
|
|
478
|
+
amount: BigInt(data.amount),
|
|
479
|
+
random: BigInt(data.random),
|
|
480
|
+
leafIndex: data.leafIndex,
|
|
481
|
+
commitment: BigInt(data.commitment),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
import { type Address } from "@solana/kit";
|
|
10
|
+
export declare const PDA_SEEDS: {
|
|
11
|
+
readonly POOL_STATE: "pool_state";
|
|
12
|
+
readonly COMMITMENT_TREE: "commitment_tree";
|
|
13
|
+
readonly LIGHT_CLIENT: "btc_light_client";
|
|
14
|
+
readonly BLOCK_HEADER: "block";
|
|
15
|
+
readonly HEIGHT_INDEX: "height_index";
|
|
16
|
+
readonly VERIFIED_TX: "verified_tx";
|
|
17
|
+
readonly DEPOSIT: "deposit";
|
|
18
|
+
readonly NULLIFIER: "nullifier";
|
|
19
|
+
readonly POLICY_APPROVAL: "policy_approval";
|
|
20
|
+
readonly EXIT_DESTINATION: "exit_destination";
|
|
21
|
+
readonly VK_REGISTRY: "vk_registry";
|
|
22
|
+
readonly TOKEN_CONFIG: "token_config";
|
|
23
|
+
readonly POOL_CONFIG: "pool_config";
|
|
24
|
+
};
|
|
25
|
+
/** `["pool_state", pool_id]` — pool_id is the pool's zkBTC mint. */
|
|
26
|
+
export declare function poolStateSeeds(poolId: Address | Uint8Array): Uint8Array[];
|
|
27
|
+
/** `["commitment_tree", pool_state, tree_index_le]`. */
|
|
28
|
+
export declare function commitmentTreeSeeds(poolState: Address | Uint8Array, treeIndex?: number): Uint8Array[];
|
|
29
|
+
/** `["token_config", pool_state, mint]`. */
|
|
30
|
+
export declare function tokenConfigSeeds(poolState: Address | Uint8Array, mint: Address | Uint8Array): Uint8Array[];
|
|
31
|
+
/** `["pool_config", pool_state]`. */
|
|
32
|
+
export declare function poolConfigSeeds(poolState: Address | Uint8Array): Uint8Array[];
|
|
33
|
+
/** `["nullifier", pool_state, nullifier]` on tree 0, and
|
|
34
|
+
* `["nullifier", pool_state, tree_index_le, nullifier]` after a rotation.
|
|
35
|
+
*
|
|
36
|
+
* A nullifier is Poseidon(nullifyingKey, leafIndex), so it names a note only
|
|
37
|
+
* within one pool and one tree — leaf indices restart at 0 in each new tree.
|
|
38
|
+
* Drop either scope and two distinct notes collapse onto one PDA, where
|
|
39
|
+
* spending either strands the other. Tree 0 keeps the shorter seeds so records
|
|
40
|
+
* already on chain stay reachable (`joinsplit_common.rs`). */
|
|
41
|
+
export declare function nullifierRecordSeeds(nullifierHash: Uint8Array, poolState: Address | Uint8Array, treeIndex?: number): Uint8Array[];
|
|
42
|
+
/** `["redemption", pool_state, user, nonce_le]` (`redeem.rs`). */
|
|
43
|
+
export declare function redemptionRequestSeeds(poolState: Address | Uint8Array, userPubkey: Address | Uint8Array, nonce: bigint): Uint8Array[];
|
|
44
|
+
/** `["vk_registry", [n_inputs], [n_outputs]]`. */
|
|
45
|
+
export declare function vkRegistrySeeds(nInputs: number, nOutputs: number): Uint8Array[];
|
|
46
|
+
/** `["deposit_receipt", txid]`, or `[..., vout_le]` for the OP_RETURN-free
|
|
47
|
+
* `verify_deposit` flow. See deriveDepositReceiptPDA for which is which. */
|
|
48
|
+
export declare function depositReceiptSeeds(depositTxid: Uint8Array, depositVout?: number): Uint8Array[];
|
|
49
|
+
/** `["policy_approval", pool_state, request_hash, nonce]`. */
|
|
50
|
+
export declare function policyApprovalSeeds(poolState: Address | Uint8Array, requestHash: Uint8Array, nonce: Uint8Array): Uint8Array[];
|
|
51
|
+
/** `["exit_destination", pool_state, [kind], key]`. */
|
|
52
|
+
export declare function exitDestinationSeeds(poolState: Address | Uint8Array, kind: number, key: Uint8Array): Uint8Array[];
|
|
53
|
+
/** `["btc_light_client"]`. */
|
|
54
|
+
export declare function lightClientSeeds(): Uint8Array[];
|
|
55
|
+
/** `["block", block_hash]`. */
|
|
56
|
+
export declare function blockHeaderSeeds(blockHash: Uint8Array): Uint8Array[];
|
|
57
|
+
/** `["height_index", height_le(8)]`. */
|
|
58
|
+
export declare function heightIndexSeeds(height: number | bigint): Uint8Array[];
|
|
59
|
+
/** `["verified_tx", block_hash, txid]`. */
|
|
60
|
+
export declare function verifiedTransactionSeeds(blockHash: Uint8Array, txid: Uint8Array): Uint8Array[];
|
|
61
|
+
/**
|
|
62
|
+
* Derive Pool State PDA
|
|
63
|
+
*/
|
|
64
|
+
export declare function derivePoolStatePDA(poolId: Address | Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
65
|
+
/**
|
|
66
|
+
* Derive Commitment Tree PDA
|
|
67
|
+
*
|
|
68
|
+
* @param treeIndex - Tree rotation index (default 0).
|
|
69
|
+
*/
|
|
70
|
+
export declare function deriveCommitmentTreePDA(poolState: Address | Uint8Array, programId?: Address, treeIndex?: number): Promise<[Address, number]>;
|
|
71
|
+
/**
|
|
72
|
+
* Derive TokenConfig PDA for a specific mint
|
|
73
|
+
* Seeds: ["token_config", mint_pubkey_bytes]
|
|
74
|
+
*/
|
|
75
|
+
export declare function deriveTokenConfigPDA(poolState: Address | Uint8Array, mintPubkey: Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
76
|
+
/** Derive ["pool_config", pool_state]. */
|
|
77
|
+
export declare function derivePoolConfigPDA(poolState: Address | Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
78
|
+
/**
|
|
79
|
+
* Derive Nullifier Record PDA.
|
|
80
|
+
*
|
|
81
|
+
* Seeds are `["nullifier", pool_state, nullifier]` on tree 0, and
|
|
82
|
+
* `["nullifier", pool_state, tree_index_le, nullifier]` on any tree a rotation
|
|
83
|
+
* created. Both scopes exist because a nullifier is Poseidon(nullifyingKey,
|
|
84
|
+
* leafIndex) and so identifies a note only within one pool and one tree:
|
|
85
|
+
*
|
|
86
|
+
* - Without the pool, the same seed spending into two vaults derives one PDA,
|
|
87
|
+
* and spending in either strands the twin note in the other.
|
|
88
|
+
* - Without the tree, the same happens across a rotation, because leaf indices
|
|
89
|
+
* restart at 0 in every new tree.
|
|
90
|
+
*
|
|
91
|
+
* Tree 0 keeps the shorter seeds so the records already on chain stay reachable
|
|
92
|
+
* — re-deriving them would make every already-spent note spendable again.
|
|
93
|
+
*/
|
|
94
|
+
export declare function deriveNullifierRecordPDA(nullifierHash: Uint8Array, poolState: Address | Uint8Array, treeIndex?: number, programId?: Address): Promise<[Address, number]>;
|
|
95
|
+
/**
|
|
96
|
+
* Derive one-time PolicyApproval PDA.
|
|
97
|
+
* Seeds: ["policy_approval", pool_state, request_hash, nonce]
|
|
98
|
+
*/
|
|
99
|
+
export declare function derivePolicyApprovalPDA(poolState: Address | Uint8Array, requestHash: Uint8Array, nonce: Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
100
|
+
/** Destination kinds in the exit registry. Kind is a PDA seed, so a Solana
|
|
101
|
+
* owner and a BTC script hash sharing the same 32 bytes stay distinct. */
|
|
102
|
+
export declare const EXIT_KIND_SOLANA_OWNER = 0;
|
|
103
|
+
export declare const EXIT_KIND_BTC_SCRIPT = 1;
|
|
104
|
+
/**
|
|
105
|
+
* Derive an ExitDestination PDA — the append-only registry of destinations a
|
|
106
|
+
* permissioned pool's ragequit path may pay.
|
|
107
|
+
*
|
|
108
|
+
* Seeds: ["exit_destination", pool_state, [kind], key]
|
|
109
|
+
*
|
|
110
|
+
* `key` is the recipient token account's OWNER for `EXIT_KIND_SOLANA_OWNER`,
|
|
111
|
+
* or `sha256(btcScript)` for `EXIT_KIND_BTC_SCRIPT`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function deriveExitDestinationPDA(poolState: Address | Uint8Array, kind: number, key: Uint8Array, programId: Address): Promise<[Address, number]>;
|
|
114
|
+
/**
|
|
115
|
+
* Derive BTC Light Client PDA
|
|
116
|
+
*/
|
|
117
|
+
export declare function deriveLightClientPDA(programId?: Address): Promise<[Address, number]>;
|
|
118
|
+
/**
|
|
119
|
+
* Derive Block Header PDA (hash-based)
|
|
120
|
+
* Seeds: ["block", blockHash(32)]
|
|
121
|
+
*/
|
|
122
|
+
export declare function deriveBlockHeaderPDA(blockHash: Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
123
|
+
/**
|
|
124
|
+
* Derive HeightIndex PDA
|
|
125
|
+
* Seeds: ["height_index", height_le_bytes(8)]
|
|
126
|
+
*/
|
|
127
|
+
export declare function deriveHeightIndexPDA(height: number | bigint, programId?: Address): Promise<[Address, number]>;
|
|
128
|
+
/**
|
|
129
|
+
* Derive VerifiedTransaction PDA (btc-light-client)
|
|
130
|
+
*
|
|
131
|
+
* Seeds: ["verified_tx", blockHash(32), txid(32)]
|
|
132
|
+
*/
|
|
133
|
+
export declare function deriveVerifiedTransactionPDA(blockHash: Uint8Array, txid: Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
134
|
+
/**
|
|
135
|
+
* Derive Redemption Request PDA
|
|
136
|
+
*
|
|
137
|
+
* Seeds: ["redemption", pool_state, user_pubkey(32), nonce_le(8)]
|
|
138
|
+
*/
|
|
139
|
+
export declare function deriveRedemptionRequestPDA(poolState: Address | Uint8Array, userPubkey: Uint8Array, nonce: bigint, programId?: Address): Promise<[Address, number]>;
|
|
140
|
+
/**
|
|
141
|
+
* Derive VK Registry PDA for a JoinSplit variant
|
|
142
|
+
*
|
|
143
|
+
* Seeds: ["vk_registry", &[n_inputs], &[n_outputs]]
|
|
144
|
+
*/
|
|
145
|
+
export declare function deriveVkRegistryPDA(nInputs: number, nOutputs: number, programId?: Address): Promise<[Address, number]>;
|
|
146
|
+
/**
|
|
147
|
+
* Derive Deposit Receipt PDA.
|
|
148
|
+
*
|
|
149
|
+
* Two on-chain schemes exist:
|
|
150
|
+
* - `complete_deposit` (disc 11, the active direct-vault flow): seeds ["deposit_receipt", txid] —
|
|
151
|
+
* call WITHOUT `depositVout`.
|
|
152
|
+
* - `verify_deposit` (disc 25, OP_RETURN-free flow): seeds ["deposit_receipt", txid, vout(4 LE)] —
|
|
153
|
+
* pass `depositVout` so a funding tx with multiple independent deposit outputs gets one receipt
|
|
154
|
+
* per output (each creditable once) instead of the first output blocking the rest.
|
|
155
|
+
*/
|
|
156
|
+
export declare function deriveDepositReceiptPDA(depositTxid: Uint8Array, depositVout?: number, programId?: Address): Promise<[Address, number]>;
|
|
157
|
+
/**
|
|
158
|
+
* Convert bigint commitment to bytes for PDA derivation
|
|
159
|
+
*/
|
|
160
|
+
export declare function commitmentToBytes(commitment: bigint): Uint8Array;
|