@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.
- package/README.md +214 -108
- package/{packages/sdk/dist → dist}/client.d.ts +25 -1
- package/{packages/sdk/dist → dist}/client.js +36 -1
- package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +3 -3
- package/{packages/sdk/dist → dist}/index.js +3 -3
- package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
- package/{packages/sdk/dist → dist}/instructions.js +45 -15
- package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
- package/{packages/sdk/dist → dist}/stealth.js +146 -14
- package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
- package/{packages/sdk/dist → dist}/taproot.js +54 -2
- package/package.json +86 -63
- package/src/announcement-client.ts +457 -0
- package/src/auditor-ciphertext.ts +181 -0
- package/src/auditor.ts +409 -0
- package/src/bitcoin/ika.ts +103 -0
- package/src/bitcoin/index.ts +5 -0
- package/src/bound-params.ts +322 -0
- package/src/chadbuffer.ts +603 -0
- package/src/circomlibjs.d.ts +51 -0
- package/src/claim-link.ts +53 -0
- package/src/client.ts +638 -0
- package/src/commitment-tree.ts +736 -0
- package/src/config.ts +772 -0
- package/src/core/esplora.ts +332 -0
- package/src/core/mempool.ts +159 -0
- package/src/crypto-babyjub.ts +385 -0
- package/src/crypto-ed25519.ts +297 -0
- package/src/crypto.ts +199 -0
- package/src/event-client.ts +231 -0
- package/src/events.ts +384 -0
- package/src/explorer.ts +300 -0
- package/src/index.ts +902 -0
- package/src/instructions.ts +2820 -0
- package/src/keys.ts +1228 -0
- package/src/logger.ts +41 -0
- package/src/magicblock.ts +278 -0
- package/src/merkle.ts +197 -0
- package/src/note.ts +754 -0
- package/src/pda.ts +516 -0
- package/src/pool-state.ts +176 -0
- package/src/poseidon.ts +175 -0
- package/src/prover/index.ts +19 -0
- package/src/prover/mobile.ts +303 -0
- package/src/prover/web.ts +771 -0
- package/src/psbt.ts +333 -0
- package/src/selective-disclosure.ts +284 -0
- package/src/sender-memo.ts +343 -0
- package/src/snarkjs.d.ts +19 -0
- package/src/sns-resolver.ts +333 -0
- package/src/solana/connection.ts +189 -0
- package/src/solana/priority-fee.ts +201 -0
- package/src/spend-doc.ts +163 -0
- package/src/stealth.ts +1477 -0
- package/src/taproot.ts +707 -0
- package/src/token-registry.ts +207 -0
- package/src/utils/encoding.ts +33 -0
- package/src/vk-registry.ts +295 -0
- package/LICENSE +0 -21
- package/packages/btc-client/src/esplora-client.ts +0 -153
- package/packages/btc-client/src/index.ts +0 -3
- package/packages/btc-client/src/op-return.ts +0 -93
- package/packages/btc-client/src/types.ts +0 -112
- package/packages/sdk/README.md +0 -277
- /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
- /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/config.js +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto.js +0 -0
- /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/event-client.js +0 -0
- /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/events.js +0 -0
- /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/explorer.js +0 -0
- /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/keys.js +0 -0
- /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/logger.js +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
- /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/merkle.js +0 -0
- /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/note.js +0 -0
- /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pda.js +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
- /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/psbt.js +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
package/src/note.ts
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
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
|
+
|
|
14
|
+
import { randomFieldElement, bigintToBytes, sha256Hash, bytesToBigint, BN254_FIELD_PRIME, babyJubMul, BABYJUB_BASE8 } from "./crypto";
|
|
15
|
+
import { computeUnifiedCommitmentSync, computeNullifierSync, hashNullifierSync, computeJoinSplitCommitmentSync, computeNPKSync, computeMPKSync, computeJoinSplitNullifierSync } from "./poseidon";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Note structure for shielded amounts
|
|
19
|
+
*
|
|
20
|
+
* Core secrets (always required):
|
|
21
|
+
* - amount, nullifier, secret
|
|
22
|
+
*
|
|
23
|
+
* Computed values (from circom circuits):
|
|
24
|
+
* - note, commitment, nullifierHash
|
|
25
|
+
* These are 0n until computed by circuits
|
|
26
|
+
*/
|
|
27
|
+
export interface Note {
|
|
28
|
+
// Amount in satoshis
|
|
29
|
+
amount: bigint;
|
|
30
|
+
// Random nullifier (field element)
|
|
31
|
+
nullifier: bigint;
|
|
32
|
+
// Random secret (field element)
|
|
33
|
+
secret: bigint;
|
|
34
|
+
// note = Poseidon(nullifier, secret) - computed by circuit
|
|
35
|
+
note: bigint;
|
|
36
|
+
// commitment = Poseidon(note, amount) - computed by circuit
|
|
37
|
+
commitment: bigint;
|
|
38
|
+
// nullifierHash = Poseidon(nullifier) - computed by circuit
|
|
39
|
+
nullifierHash: bigint;
|
|
40
|
+
// 32-byte representations
|
|
41
|
+
nullifierBytes: Uint8Array;
|
|
42
|
+
secretBytes: Uint8Array;
|
|
43
|
+
commitmentBytes: Uint8Array;
|
|
44
|
+
nullifierHashBytes: Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Serializable note data (for storage/transmission)
|
|
49
|
+
* Only stores essential secrets - hashes recomputed by circuits
|
|
50
|
+
*/
|
|
51
|
+
export interface SerializedNote {
|
|
52
|
+
amount: string;
|
|
53
|
+
nullifier: string;
|
|
54
|
+
secret: string;
|
|
55
|
+
// Optional pre-computed values
|
|
56
|
+
commitment?: string;
|
|
57
|
+
nullifierHash?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate a new note with random nullifier and secret
|
|
62
|
+
*
|
|
63
|
+
* Note: Hash values are NOT computed here. They will be computed
|
|
64
|
+
* by the circom circuits during proof generation.
|
|
65
|
+
*
|
|
66
|
+
* @param amountSats - Amount in satoshis
|
|
67
|
+
* @returns Note with secrets (hashes are 0n until circuit execution)
|
|
68
|
+
*/
|
|
69
|
+
export function generateNote(amountSats: bigint): Note {
|
|
70
|
+
const nullifier = randomFieldElement();
|
|
71
|
+
const secret = randomFieldElement();
|
|
72
|
+
|
|
73
|
+
return createNoteFromSecrets(nullifier, secret, amountSats);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a note from known secrets
|
|
78
|
+
*
|
|
79
|
+
* @param nullifier - Nullifier field element
|
|
80
|
+
* @param secret - Secret field element
|
|
81
|
+
* @param amountSats - Amount in satoshis
|
|
82
|
+
* @param commitment - Optional pre-computed commitment (from circuit)
|
|
83
|
+
* @param nullifierHash - Optional pre-computed nullifier hash (from circuit)
|
|
84
|
+
* @returns Note structure
|
|
85
|
+
*/
|
|
86
|
+
export function createNoteFromSecrets(
|
|
87
|
+
nullifier: bigint,
|
|
88
|
+
secret: bigint,
|
|
89
|
+
amountSats: bigint,
|
|
90
|
+
commitment?: bigint,
|
|
91
|
+
nullifierHash?: bigint
|
|
92
|
+
): Note {
|
|
93
|
+
// Computed values are 0n until provided from circuit outputs
|
|
94
|
+
const comm = commitment ?? 0n;
|
|
95
|
+
const nullHash = nullifierHash ?? 0n;
|
|
96
|
+
const noteHash = 0n; // Intermediate hash not typically exposed
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
amount: amountSats,
|
|
100
|
+
nullifier,
|
|
101
|
+
secret,
|
|
102
|
+
note: noteHash,
|
|
103
|
+
commitment: comm,
|
|
104
|
+
nullifierHash: nullHash,
|
|
105
|
+
nullifierBytes: bigintToBytes(nullifier),
|
|
106
|
+
secretBytes: bigintToBytes(secret),
|
|
107
|
+
commitmentBytes: bigintToBytes(comm),
|
|
108
|
+
nullifierHashBytes: bigintToBytes(nullHash),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update note with computed hash values from circom circuit execution
|
|
114
|
+
*
|
|
115
|
+
* @param note - Note to update
|
|
116
|
+
* @param commitment - Commitment from circuit output
|
|
117
|
+
* @param nullifierHash - Nullifier hash from circuit output
|
|
118
|
+
* @returns Updated note
|
|
119
|
+
*/
|
|
120
|
+
export function updateNoteWithHashes(
|
|
121
|
+
note: Note,
|
|
122
|
+
commitment: bigint,
|
|
123
|
+
nullifierHash: bigint
|
|
124
|
+
): Note {
|
|
125
|
+
return {
|
|
126
|
+
...note,
|
|
127
|
+
commitment,
|
|
128
|
+
nullifierHash,
|
|
129
|
+
commitmentBytes: bigintToBytes(commitment),
|
|
130
|
+
nullifierHashBytes: bigintToBytes(nullifierHash),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Serialize a note for storage or transmission
|
|
136
|
+
*
|
|
137
|
+
* Only stores the essential data (amount, nullifier, secret).
|
|
138
|
+
* Optionally includes pre-computed hash values.
|
|
139
|
+
*/
|
|
140
|
+
export function serializeNote(note: Note): SerializedNote {
|
|
141
|
+
const serialized: SerializedNote = {
|
|
142
|
+
amount: note.amount.toString(),
|
|
143
|
+
nullifier: note.nullifier.toString(),
|
|
144
|
+
secret: note.secret.toString(),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Include computed values if available
|
|
148
|
+
if (note.commitment !== 0n) {
|
|
149
|
+
serialized.commitment = note.commitment.toString();
|
|
150
|
+
}
|
|
151
|
+
if (note.nullifierHash !== 0n) {
|
|
152
|
+
serialized.nullifierHash = note.nullifierHash.toString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return serialized;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Deserialize and restore a note from stored data
|
|
160
|
+
*/
|
|
161
|
+
export function deserializeNote(data: SerializedNote): Note {
|
|
162
|
+
const amount = BigInt(data.amount);
|
|
163
|
+
const nullifier = BigInt(data.nullifier);
|
|
164
|
+
const secret = BigInt(data.secret);
|
|
165
|
+
const commitment = data.commitment ? BigInt(data.commitment) : undefined;
|
|
166
|
+
const nullifierHash = data.nullifierHash
|
|
167
|
+
? BigInt(data.nullifierHash)
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
return createNoteFromSecrets(
|
|
171
|
+
nullifier,
|
|
172
|
+
secret,
|
|
173
|
+
amount,
|
|
174
|
+
commitment,
|
|
175
|
+
nullifierHash
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Format satoshis as BTC string
|
|
181
|
+
*/
|
|
182
|
+
export function formatBtc(sats: bigint): string {
|
|
183
|
+
const btc = Number(sats) / 100_000_000;
|
|
184
|
+
return btc.toFixed(8) + " BTC";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse BTC string to satoshis
|
|
189
|
+
*/
|
|
190
|
+
export function parseBtc(btcString: string): bigint {
|
|
191
|
+
const btc = parseFloat(btcString.replace(" BTC", ""));
|
|
192
|
+
return BigInt(Math.round(btc * 100_000_000));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if a note has computed hash values
|
|
197
|
+
*/
|
|
198
|
+
export function noteHasComputedHashes(note: Note): boolean {
|
|
199
|
+
return note.commitment !== 0n && note.nullifierHash !== 0n;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Note Commitment/Nullifier Helpers
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the public key X coordinate from a note's nullifier (used as private key)
|
|
208
|
+
*
|
|
209
|
+
* In the unified model: pubKey = nullifier * BASE8 (on Baby Jubjub curve)
|
|
210
|
+
* Returns pubKey.x for commitment computation
|
|
211
|
+
*/
|
|
212
|
+
export function getNotePublicKeyX(note: Note): bigint {
|
|
213
|
+
const pubKey = babyJubMul(note.nullifier, BABYJUB_BASE8);
|
|
214
|
+
return pubKey.x;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compute the commitment for a note
|
|
219
|
+
*
|
|
220
|
+
* commitment = Poseidon(pubKeyX, amount)
|
|
221
|
+
* where pubKeyX = (nullifier * G).x
|
|
222
|
+
*
|
|
223
|
+
* Returns the note with commitment fields populated
|
|
224
|
+
*/
|
|
225
|
+
export function computeNoteCommitment(note: Note): Note {
|
|
226
|
+
const pubKeyX = getNotePublicKeyX(note);
|
|
227
|
+
const commitment = computeUnifiedCommitmentSync(pubKeyX, note.amount);
|
|
228
|
+
const commitmentBytes = bigintToBytes(commitment);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...note,
|
|
232
|
+
commitment,
|
|
233
|
+
commitmentBytes,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Compute the nullifier and nullifier hash for a note at a given leaf index
|
|
239
|
+
*
|
|
240
|
+
* nullifier = Poseidon(privKey, leafIndex)
|
|
241
|
+
* nullifierHash = Poseidon(nullifier)
|
|
242
|
+
*
|
|
243
|
+
* @returns Object with computed nullifier values
|
|
244
|
+
*/
|
|
245
|
+
export function computeNoteNullifier(
|
|
246
|
+
note: Note,
|
|
247
|
+
leafIndex: bigint
|
|
248
|
+
): {
|
|
249
|
+
nullifier: bigint;
|
|
250
|
+
nullifierHash: bigint;
|
|
251
|
+
nullifierHashBytes: Uint8Array;
|
|
252
|
+
} {
|
|
253
|
+
const nullifier = computeNullifierSync(note.nullifier, leafIndex);
|
|
254
|
+
const nullifierHash = hashNullifierSync(nullifier);
|
|
255
|
+
const nullifierHashBytes = bigintToBytes(nullifierHash);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
nullifier,
|
|
259
|
+
nullifierHash,
|
|
260
|
+
nullifierHashBytes,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Deterministic Note Derivation (HD-style)
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Derive a note deterministically from a seed phrase/name and index.
|
|
270
|
+
*
|
|
271
|
+
* This allows recovery of all notes from just the seed + index.
|
|
272
|
+
* Similar to HD wallets in Bitcoin.
|
|
273
|
+
*
|
|
274
|
+
* The seed is first hashed to create a 32-byte master key, then
|
|
275
|
+
* nullifier and secret are derived from that master key.
|
|
276
|
+
*
|
|
277
|
+
* @param seed - Seed phrase, name, or password (any string)
|
|
278
|
+
* @param index - Note index (0, 1, 2, ...)
|
|
279
|
+
* @param amountSats - Amount in satoshis
|
|
280
|
+
* @returns Note with deterministically derived secrets
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* // Derive notes from a name
|
|
285
|
+
* const note0 = deriveNote("albertgogogo", 0, 100_000n);
|
|
286
|
+
* const note1 = deriveNote("albertgogogo", 1, 50_000n);
|
|
287
|
+
*
|
|
288
|
+
* // Later: recover the same notes
|
|
289
|
+
* const recovered = deriveNote("albertgogogo", 0, 100_000n);
|
|
290
|
+
* // recovered.nullifier === note0.nullifier ✓
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export function deriveNote(
|
|
294
|
+
seed: string,
|
|
295
|
+
index: number,
|
|
296
|
+
amountSats: bigint
|
|
297
|
+
): Note {
|
|
298
|
+
// Step 1: Hash seed to get 32-byte master key (normalizes any input)
|
|
299
|
+
const master = deriveMasterKey(seed);
|
|
300
|
+
|
|
301
|
+
// Step 2: Derive nullifier and secret from master + index
|
|
302
|
+
const nullifier = deriveFromMaster(master, index, 0); // 0 = nullifier domain
|
|
303
|
+
const secret = deriveFromMaster(master, index, 1); // 1 = secret domain
|
|
304
|
+
|
|
305
|
+
return createNoteFromSecrets(nullifier, secret, amountSats);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Derive master key from seed (32 bytes)
|
|
310
|
+
*
|
|
311
|
+
* master = SHA256(seed)
|
|
312
|
+
*
|
|
313
|
+
* You can cache this and use deriveNoteFromMaster() for efficiency.
|
|
314
|
+
*/
|
|
315
|
+
export function deriveMasterKey(seed: string): Uint8Array {
|
|
316
|
+
const encoder = new TextEncoder();
|
|
317
|
+
const seedBytes = encoder.encode(seed);
|
|
318
|
+
return sha256Hash(seedBytes);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Derive a note from a pre-computed master key
|
|
323
|
+
*
|
|
324
|
+
* More efficient if deriving many notes from the same seed.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* const master = deriveMasterKey("albertgogogo");
|
|
329
|
+
* const note0 = deriveNoteFromMaster(master, 0, 100_000n);
|
|
330
|
+
* const note1 = deriveNoteFromMaster(master, 1, 50_000n);
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export function deriveNoteFromMaster(
|
|
334
|
+
master: Uint8Array,
|
|
335
|
+
index: number,
|
|
336
|
+
amountSats: bigint
|
|
337
|
+
): Note {
|
|
338
|
+
const nullifier = deriveFromMaster(master, index, 0);
|
|
339
|
+
const secret = deriveFromMaster(master, index, 1);
|
|
340
|
+
return createNoteFromSecrets(nullifier, secret, amountSats);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Derive field element from master key + index + domain
|
|
345
|
+
*
|
|
346
|
+
* result = SHA256(master || index || domain) mod BN254_PRIME
|
|
347
|
+
*
|
|
348
|
+
* @param master - 32-byte master key
|
|
349
|
+
* @param index - Note index
|
|
350
|
+
* @param domain - 0 for nullifier, 1 for secret
|
|
351
|
+
*/
|
|
352
|
+
function deriveFromMaster(
|
|
353
|
+
master: Uint8Array,
|
|
354
|
+
index: number,
|
|
355
|
+
domain: number
|
|
356
|
+
): bigint {
|
|
357
|
+
// Build input: master (32) || index (4) || domain (1) = 37 bytes
|
|
358
|
+
const input = new Uint8Array(37);
|
|
359
|
+
input.set(master, 0);
|
|
360
|
+
|
|
361
|
+
// Index as 4 bytes little-endian
|
|
362
|
+
const view = new DataView(input.buffer);
|
|
363
|
+
view.setUint32(32, index, true);
|
|
364
|
+
|
|
365
|
+
// Domain as 1 byte
|
|
366
|
+
input[36] = domain;
|
|
367
|
+
|
|
368
|
+
// Hash and reduce to field
|
|
369
|
+
const hash = sha256Hash(input);
|
|
370
|
+
return bytesToBigint(hash) % BN254_FIELD_PRIME;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Derive multiple notes at once from a seed
|
|
375
|
+
*
|
|
376
|
+
* More efficient than calling deriveNote() multiple times
|
|
377
|
+
* because master key is computed only once.
|
|
378
|
+
*
|
|
379
|
+
* @param seed - Seed phrase
|
|
380
|
+
* @param amounts - Array of amounts for each note
|
|
381
|
+
* @param startIndex - Starting index (default 0)
|
|
382
|
+
* @returns Array of derived notes
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```typescript
|
|
386
|
+
* // Create a wallet with 3 notes
|
|
387
|
+
* const notes = deriveNotes("albertgogogo", [100_000n, 50_000n, 25_000n]);
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
export function deriveNotes(
|
|
391
|
+
seed: string,
|
|
392
|
+
amounts: bigint[],
|
|
393
|
+
startIndex: number = 0
|
|
394
|
+
): Note[] {
|
|
395
|
+
// Compute master once, derive all notes from it
|
|
396
|
+
const master = deriveMasterKey(seed);
|
|
397
|
+
return amounts.map((amount, i) => deriveNoteFromMaster(master, startIndex + i, amount));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check the strength of a seed phrase
|
|
402
|
+
*
|
|
403
|
+
* Returns estimated bits of entropy.
|
|
404
|
+
* Recommended: >= 80 bits for moderate security, >= 128 bits for high security
|
|
405
|
+
*/
|
|
406
|
+
export function estimateSeedStrength(seed: string): {
|
|
407
|
+
bits: number;
|
|
408
|
+
strength: "weak" | "moderate" | "strong" | "very_strong";
|
|
409
|
+
warning?: string;
|
|
410
|
+
} {
|
|
411
|
+
// Very rough entropy estimation
|
|
412
|
+
const length = seed.length;
|
|
413
|
+
const hasLower = /[a-z]/.test(seed);
|
|
414
|
+
const hasUpper = /[A-Z]/.test(seed);
|
|
415
|
+
const hasDigit = /[0-9]/.test(seed);
|
|
416
|
+
const hasSpecial = /[^a-zA-Z0-9]/.test(seed);
|
|
417
|
+
|
|
418
|
+
let charsetSize = 0;
|
|
419
|
+
if (hasLower) charsetSize += 26;
|
|
420
|
+
if (hasUpper) charsetSize += 26;
|
|
421
|
+
if (hasDigit) charsetSize += 10;
|
|
422
|
+
if (hasSpecial) charsetSize += 32;
|
|
423
|
+
|
|
424
|
+
// Entropy = length * log2(charsetSize)
|
|
425
|
+
const bits = Math.floor(length * Math.log2(charsetSize || 1));
|
|
426
|
+
|
|
427
|
+
let strength: "weak" | "moderate" | "strong" | "very_strong";
|
|
428
|
+
let warning: string | undefined;
|
|
429
|
+
|
|
430
|
+
if (bits < 40) {
|
|
431
|
+
strength = "weak";
|
|
432
|
+
warning = "DANGER: This seed can be easily brute-forced. Use a longer passphrase!";
|
|
433
|
+
} else if (bits < 80) {
|
|
434
|
+
strength = "moderate";
|
|
435
|
+
warning = "Consider using a longer passphrase for better security.";
|
|
436
|
+
} else if (bits < 128) {
|
|
437
|
+
strength = "strong";
|
|
438
|
+
} else {
|
|
439
|
+
strength = "very_strong";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { bits, strength, warning };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ============================================================================
|
|
446
|
+
// Circom-Compatible Note Helpers
|
|
447
|
+
// ============================================================================
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Simple note data structure (for circom circuit inputs)
|
|
451
|
+
*
|
|
452
|
+
* NOTE: When using circom circuits, the commitment is computed INSIDE the circuit
|
|
453
|
+
* using Poseidon. The SDK just provides the raw note data.
|
|
454
|
+
*/
|
|
455
|
+
export interface NoteData {
|
|
456
|
+
nullifier: bigint;
|
|
457
|
+
secret: bigint;
|
|
458
|
+
amount: bigint;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Poseidon initialization state (for API compatibility)
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Create a simple note (alias for generateNote but returns NoteData)
|
|
465
|
+
*
|
|
466
|
+
* Use this to generate note data for circom circuit inputs.
|
|
467
|
+
*
|
|
468
|
+
* @param amount - Amount in satoshis
|
|
469
|
+
* @returns NoteData with nullifier, secret, and amount
|
|
470
|
+
*/
|
|
471
|
+
export function createNote(amount: bigint): NoteData {
|
|
472
|
+
const note = generateNote(amount);
|
|
473
|
+
return {
|
|
474
|
+
nullifier: note.nullifier,
|
|
475
|
+
secret: note.secret,
|
|
476
|
+
amount: note.amount,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// initPoseidon/isPoseidonReady used to live here as well, over a flag local to this module. The
|
|
481
|
+
// barrel exported initPoseidon from poseidon.ts and isPoseidonReady from here, so they read
|
|
482
|
+
// different state: `await initPoseidon(); isPoseidonReady()` returned false forever. Both now
|
|
483
|
+
// belong to poseidon.ts, which owns the actual hasher.
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Prepare withdrawal - creates change note for remaining balance
|
|
487
|
+
*
|
|
488
|
+
* PRIVACY: All withdrawals are indistinguishable on-chain.
|
|
489
|
+
* Change amount can be >= 0, and the commitment always looks random.
|
|
490
|
+
*
|
|
491
|
+
* @param inputNote - Note being spent
|
|
492
|
+
* @param withdrawAmount - Amount to withdraw
|
|
493
|
+
* @returns Change note with remaining balance (can be 0)
|
|
494
|
+
*/
|
|
495
|
+
export function prepareWithdrawal(
|
|
496
|
+
inputNote: NoteData,
|
|
497
|
+
withdrawAmount: bigint
|
|
498
|
+
): { changeNote: NoteData; changeAmount: bigint } {
|
|
499
|
+
if (withdrawAmount <= 0n) {
|
|
500
|
+
throw new Error("Withdraw amount must be positive");
|
|
501
|
+
}
|
|
502
|
+
if (withdrawAmount > inputNote.amount) {
|
|
503
|
+
throw new Error("Withdraw amount exceeds note balance");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const changeAmount = inputNote.amount - withdrawAmount;
|
|
507
|
+
const changeNote = createNote(changeAmount);
|
|
508
|
+
|
|
509
|
+
return { changeNote, changeAmount };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ============================================================================
|
|
513
|
+
// Stealth Note Types (Dual-Key ECDH Support)
|
|
514
|
+
// ============================================================================
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Stealth note structure for dual-key ECDH system
|
|
518
|
+
*
|
|
519
|
+
* Key differences from basic Note:
|
|
520
|
+
* - Uses random value instead of nullifier/secret for commitment
|
|
521
|
+
* - Stores ephemeral spending pubkey for proof generation
|
|
522
|
+
* - Nullifier derived from (spendingPrivKey, leafIndex) in circuit
|
|
523
|
+
*/
|
|
524
|
+
export interface StealthNote {
|
|
525
|
+
/** Amount in satoshis */
|
|
526
|
+
amount: bigint;
|
|
527
|
+
|
|
528
|
+
/** Random value for commitment */
|
|
529
|
+
random: bigint;
|
|
530
|
+
|
|
531
|
+
/** Ephemeral spending public key (from sender) */
|
|
532
|
+
ephemeralSpendPubX: bigint;
|
|
533
|
+
ephemeralSpendPubY: bigint;
|
|
534
|
+
|
|
535
|
+
/** Leaf index in Merkle tree (set when commitment added on-chain) */
|
|
536
|
+
leafIndex: number;
|
|
537
|
+
|
|
538
|
+
/** Note public key = Poseidon(ECDHShared.x, ECDHShared.y) */
|
|
539
|
+
notePubKey: bigint;
|
|
540
|
+
|
|
541
|
+
/** Commitment = Poseidon(notePubKey, amount, random) */
|
|
542
|
+
commitment: bigint;
|
|
543
|
+
|
|
544
|
+
/** Byte representations */
|
|
545
|
+
randomBytes: Uint8Array;
|
|
546
|
+
commitmentBytes: Uint8Array;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Serializable stealth note data
|
|
551
|
+
*/
|
|
552
|
+
export interface SerializedStealthNote {
|
|
553
|
+
amount: string;
|
|
554
|
+
random: string;
|
|
555
|
+
ephemeralSpendPubX: string;
|
|
556
|
+
ephemeralSpendPubY: string;
|
|
557
|
+
leafIndex: number;
|
|
558
|
+
notePubKey?: string;
|
|
559
|
+
commitment?: string;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Create a stealth note from scanned announcement data
|
|
564
|
+
*
|
|
565
|
+
* @param amount - Decrypted amount
|
|
566
|
+
* @param random - Decrypted random value
|
|
567
|
+
* @param ephemeralSpendPub - Sender's ephemeral pubkey
|
|
568
|
+
* @param leafIndex - Merkle tree leaf index
|
|
569
|
+
* @returns StealthNote structure
|
|
570
|
+
*/
|
|
571
|
+
export function createStealthNote(
|
|
572
|
+
amount: bigint,
|
|
573
|
+
random: bigint,
|
|
574
|
+
ephemeralSpendPub: { x: bigint; y: bigint },
|
|
575
|
+
leafIndex: number
|
|
576
|
+
): StealthNote {
|
|
577
|
+
return {
|
|
578
|
+
amount,
|
|
579
|
+
random,
|
|
580
|
+
ephemeralSpendPubX: ephemeralSpendPub.x,
|
|
581
|
+
ephemeralSpendPubY: ephemeralSpendPub.y,
|
|
582
|
+
leafIndex,
|
|
583
|
+
notePubKey: 0n, // Computed in circuit
|
|
584
|
+
commitment: 0n, // Computed in circuit
|
|
585
|
+
randomBytes: bigintToBytes(random),
|
|
586
|
+
commitmentBytes: new Uint8Array(32),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Update stealth note with computed values from circuit
|
|
592
|
+
*/
|
|
593
|
+
export function updateStealthNoteWithHashes(
|
|
594
|
+
note: StealthNote,
|
|
595
|
+
notePubKey: bigint,
|
|
596
|
+
commitment: bigint
|
|
597
|
+
): StealthNote {
|
|
598
|
+
return {
|
|
599
|
+
...note,
|
|
600
|
+
notePubKey,
|
|
601
|
+
commitment,
|
|
602
|
+
commitmentBytes: bigintToBytes(commitment),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Serialize stealth note for storage
|
|
608
|
+
*/
|
|
609
|
+
export function serializeStealthNote(note: StealthNote): SerializedStealthNote {
|
|
610
|
+
const serialized: SerializedStealthNote = {
|
|
611
|
+
amount: note.amount.toString(),
|
|
612
|
+
random: note.random.toString(),
|
|
613
|
+
ephemeralSpendPubX: note.ephemeralSpendPubX.toString(),
|
|
614
|
+
ephemeralSpendPubY: note.ephemeralSpendPubY.toString(),
|
|
615
|
+
leafIndex: note.leafIndex,
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
if (note.notePubKey !== 0n) {
|
|
619
|
+
serialized.notePubKey = note.notePubKey.toString();
|
|
620
|
+
}
|
|
621
|
+
if (note.commitment !== 0n) {
|
|
622
|
+
serialized.commitment = note.commitment.toString();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return serialized;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Deserialize stealth note from storage
|
|
630
|
+
*/
|
|
631
|
+
export function deserializeStealthNote(data: SerializedStealthNote): StealthNote {
|
|
632
|
+
const note = createStealthNote(
|
|
633
|
+
BigInt(data.amount),
|
|
634
|
+
BigInt(data.random),
|
|
635
|
+
{
|
|
636
|
+
x: BigInt(data.ephemeralSpendPubX),
|
|
637
|
+
y: BigInt(data.ephemeralSpendPubY),
|
|
638
|
+
},
|
|
639
|
+
data.leafIndex
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (data.notePubKey && data.commitment) {
|
|
643
|
+
return updateStealthNoteWithHashes(
|
|
644
|
+
note,
|
|
645
|
+
BigInt(data.notePubKey),
|
|
646
|
+
BigInt(data.commitment)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return note;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Check if stealth note has computed hashes
|
|
655
|
+
*/
|
|
656
|
+
export function stealthNoteHasComputedHashes(note: StealthNote): boolean {
|
|
657
|
+
return note.notePubKey !== 0n && note.commitment !== 0n;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ============================================================================
|
|
661
|
+
// JoinSplit Note (Railgun-aligned 3-key model)
|
|
662
|
+
// ============================================================================
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* JoinSplit note structure
|
|
666
|
+
*
|
|
667
|
+
* Commitment = Poseidon(npk, token, amount)
|
|
668
|
+
* where npk = Poseidon(MPK, random)
|
|
669
|
+
* and MPK = Poseidon(pkX, pkY, nullifyingKey)
|
|
670
|
+
*/
|
|
671
|
+
export interface JoinSplitNote {
|
|
672
|
+
/** Note public key: Poseidon(MPK, random) */
|
|
673
|
+
npk: bigint;
|
|
674
|
+
/** Token identifier: computeTokenId(mintBytes) */
|
|
675
|
+
token: bigint;
|
|
676
|
+
/** Amount in token's native units */
|
|
677
|
+
amount: bigint;
|
|
678
|
+
/** Blinding factor (used in NPK derivation) */
|
|
679
|
+
random: bigint;
|
|
680
|
+
/** Merkle tree leaf position (-1 if not yet inserted) */
|
|
681
|
+
leafIndex: number;
|
|
682
|
+
/** Commitment: Poseidon(npk, token, amount) */
|
|
683
|
+
commitment: bigint;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Create a JoinSplit note from key components
|
|
688
|
+
*
|
|
689
|
+
* @param tokenId - Token identifier from computeTokenId(mintBytes)
|
|
690
|
+
*/
|
|
691
|
+
export function createJoinSplitNote(
|
|
692
|
+
mpk: bigint,
|
|
693
|
+
random: bigint,
|
|
694
|
+
amount: bigint,
|
|
695
|
+
tokenId: bigint,
|
|
696
|
+
leafIndex: number = -1,
|
|
697
|
+
): JoinSplitNote {
|
|
698
|
+
const npk = computeNPKSync(mpk, random);
|
|
699
|
+
const commitment = computeJoinSplitCommitmentSync(npk, tokenId, amount);
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
npk,
|
|
703
|
+
token: tokenId,
|
|
704
|
+
amount,
|
|
705
|
+
random,
|
|
706
|
+
leafIndex,
|
|
707
|
+
commitment,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Compute nullifier for a JoinSplit note
|
|
713
|
+
*/
|
|
714
|
+
export function computeJoinSplitNoteNullifier(
|
|
715
|
+
nullifyingKey: bigint,
|
|
716
|
+
leafIndex: number,
|
|
717
|
+
): bigint {
|
|
718
|
+
return computeJoinSplitNullifierSync(nullifyingKey, BigInt(leafIndex));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Serialize a JoinSplit note for storage
|
|
723
|
+
*/
|
|
724
|
+
export interface SerializedJoinSplitNote {
|
|
725
|
+
npk: string;
|
|
726
|
+
token: string;
|
|
727
|
+
amount: string;
|
|
728
|
+
random: string;
|
|
729
|
+
leafIndex: number;
|
|
730
|
+
commitment: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function serializeJoinSplitNote(note: JoinSplitNote): SerializedJoinSplitNote {
|
|
734
|
+
return {
|
|
735
|
+
npk: note.npk.toString(),
|
|
736
|
+
token: note.token.toString(),
|
|
737
|
+
amount: note.amount.toString(),
|
|
738
|
+
random: note.random.toString(),
|
|
739
|
+
leafIndex: note.leafIndex,
|
|
740
|
+
commitment: note.commitment.toString(),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function deserializeJoinSplitNote(data: SerializedJoinSplitNote): JoinSplitNote {
|
|
745
|
+
return {
|
|
746
|
+
npk: BigInt(data.npk),
|
|
747
|
+
token: BigInt(data.token),
|
|
748
|
+
amount: BigInt(data.amount),
|
|
749
|
+
random: BigInt(data.random),
|
|
750
|
+
leafIndex: data.leafIndex,
|
|
751
|
+
commitment: BigInt(data.commitment),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|