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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/psbt.d.ts +12 -2
  11. package/{packages/sdk/dist → dist}/psbt.js +17 -12
  12. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  13. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  14. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  15. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  16. package/package.json +86 -63
  17. package/src/announcement-client.ts +457 -0
  18. package/src/auditor-ciphertext.ts +181 -0
  19. package/src/auditor.ts +409 -0
  20. package/src/bitcoin/ika.ts +103 -0
  21. package/src/bitcoin/index.ts +5 -0
  22. package/src/bound-params.ts +322 -0
  23. package/src/chadbuffer.ts +603 -0
  24. package/src/circomlibjs.d.ts +51 -0
  25. package/src/claim-link.ts +53 -0
  26. package/src/client.ts +638 -0
  27. package/src/commitment-tree.ts +736 -0
  28. package/src/config.ts +772 -0
  29. package/src/core/esplora.ts +332 -0
  30. package/src/core/mempool.ts +159 -0
  31. package/src/crypto-babyjub.ts +385 -0
  32. package/src/crypto-ed25519.ts +297 -0
  33. package/src/crypto.ts +199 -0
  34. package/src/event-client.ts +231 -0
  35. package/src/events.ts +384 -0
  36. package/src/explorer.ts +300 -0
  37. package/src/index.ts +902 -0
  38. package/src/instructions.ts +2820 -0
  39. package/src/keys.ts +1228 -0
  40. package/src/logger.ts +41 -0
  41. package/src/magicblock.ts +278 -0
  42. package/src/merkle.ts +197 -0
  43. package/src/note.ts +754 -0
  44. package/src/pda.ts +516 -0
  45. package/src/pool-state.ts +176 -0
  46. package/src/poseidon.ts +175 -0
  47. package/src/prover/index.ts +19 -0
  48. package/src/prover/mobile.ts +303 -0
  49. package/src/prover/web.ts +771 -0
  50. package/src/psbt.ts +345 -0
  51. package/src/selective-disclosure.ts +284 -0
  52. package/src/sender-memo.ts +343 -0
  53. package/src/snarkjs.d.ts +19 -0
  54. package/src/sns-resolver.ts +333 -0
  55. package/src/solana/connection.ts +189 -0
  56. package/src/solana/priority-fee.ts +201 -0
  57. package/src/spend-doc.ts +163 -0
  58. package/src/stealth.ts +1477 -0
  59. package/src/taproot.ts +707 -0
  60. package/src/token-registry.ts +207 -0
  61. package/src/utils/encoding.ts +33 -0
  62. package/src/vk-registry.ts +295 -0
  63. package/LICENSE +0 -21
  64. package/packages/btc-client/src/esplora-client.ts +0 -153
  65. package/packages/btc-client/src/index.ts +0 -3
  66. package/packages/btc-client/src/op-return.ts +0 -93
  67. package/packages/btc-client/src/types.ts +0 -112
  68. package/packages/sdk/README.md +0 -277
  69. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  73. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  79. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  81. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  83. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  85. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  87. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/config.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}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
@@ -0,0 +1,603 @@
1
+ /**
2
+ * ChadBuffer Client
3
+ *
4
+ * Helper functions to upload Bitcoin transaction data to ChadBuffer
5
+ * for SPV verification on Solana.
6
+ *
7
+ * Networks: Bitcoin Testnet4, Solana Devnet
8
+ *
9
+ * Reference: https://github.com/deanmlittle/chadbuffer
10
+ */
11
+
12
+ import { fromBase64 } from "./utils/encoding";
13
+ import { hexToBytes } from "./crypto";
14
+ import {
15
+ getProgramDerivedAddress,
16
+ generateKeyPairSigner,
17
+ createKeyPairSignerFromBytes,
18
+ pipe,
19
+ createTransactionMessage,
20
+ setTransactionMessageFeePayer,
21
+ setTransactionMessageLifetimeUsingBlockhash,
22
+ appendTransactionMessageInstruction,
23
+ appendTransactionMessageInstructions,
24
+ signTransactionMessageWithSigners,
25
+ getSignatureFromTransaction,
26
+ sendAndConfirmTransactionFactory,
27
+ AccountRole,
28
+ type Address,
29
+ type KeyPairSigner,
30
+ type Rpc,
31
+ type RpcSubscriptions,
32
+ type SolanaRpcApi,
33
+ type SolanaRpcSubscriptionsApi,
34
+ } from "@solana/kit";
35
+ import { getCreateAccountInstruction } from "@solana-program/system";
36
+ import { address } from "./config";
37
+ import { debug } from "./logger";
38
+
39
+ /** Instruction type for v2 */
40
+ interface Instruction {
41
+ programAddress: Address;
42
+ accounts: Array<{ address: Address; role: (typeof AccountRole)[keyof typeof AccountRole] }>;
43
+ data: Uint8Array;
44
+ }
45
+
46
+ // ChadBuffer Program ID (deployed to devnet 2025-01-30)
47
+ export const CHADBUFFER_PROGRAM_ID: Address = address(
48
+ "C5RpjtTMFXKVZCtXSzKXD4CDNTaWBg3dVeMfYvjZYHDF"
49
+ );
50
+
51
+ // System Program ID
52
+ const SYSTEM_PROGRAM_ID: Address = address("11111111111111111111111111111111");
53
+
54
+ // Buffer authority size (32 bytes)
55
+ export const AUTHORITY_SIZE = 32;
56
+
57
+ // Solana transaction limit
58
+ export const SOLANA_TX_SIZE_LIMIT = 1232;
59
+
60
+ /**
61
+ * ChadBuffer Write TX overhead breakdown:
62
+ * - Signature: 64 bytes
63
+ * - Message header: 3 bytes
64
+ * - 2 account metas (payer + buffer): 66 bytes (33 each)
65
+ * - Instruction header: 4 bytes
66
+ * - Discriminator: 1 byte
67
+ * - u24 offset: 3 bytes
68
+ * Total fixed overhead: ~141 bytes (using 176 for safety margin)
69
+ */
70
+ const WRITE_TX_OVERHEAD = 176;
71
+
72
+ /**
73
+ * Maximum data bytes per ChadBuffer Write transaction
74
+ * Dynamically calculated from TX limit minus overhead
75
+ */
76
+ export const MAX_DATA_PER_WRITE = SOLANA_TX_SIZE_LIMIT - WRITE_TX_OVERHEAD;
77
+
78
+ /**
79
+ * ChadBuffer instruction discriminators
80
+ * See: https://github.com/deanmlittle/chadbuffer/blob/main/src/lib.rs
81
+ */
82
+ enum ChadBufferInstruction {
83
+ Create = 0, // Init buffer with initial data
84
+ Assign = 1, // Transfer authority
85
+ Write = 2, // Write at offset (u24 offset + data)
86
+ Close = 3, // Close buffer and reclaim lamports
87
+ }
88
+
89
+ /**
90
+ * Create instruction data for ChadBuffer Init (discriminator 0)
91
+ * Format: discriminator(1) + data
92
+ */
93
+ function createInitInstructionData(data: Uint8Array): Uint8Array {
94
+ const buffer = new Uint8Array(1 + data.length);
95
+ buffer[0] = ChadBufferInstruction.Create;
96
+ buffer.set(data, 1);
97
+ return buffer;
98
+ }
99
+
100
+ /**
101
+ * Create instruction data for ChadBuffer Write (discriminator 2)
102
+ * Format: discriminator(1) + u24_offset(3) + data
103
+ */
104
+ function createWriteInstructionData(offset: number, data: Uint8Array): Uint8Array {
105
+ const buffer = new Uint8Array(1 + 3 + data.length);
106
+ buffer[0] = ChadBufferInstruction.Write;
107
+ // u24 offset (little-endian)
108
+ buffer[1] = offset & 0xff;
109
+ buffer[2] = (offset >> 8) & 0xff;
110
+ buffer[3] = (offset >> 16) & 0xff;
111
+ buffer.set(data, 4);
112
+ return buffer;
113
+ }
114
+
115
+ /**
116
+ * Create instruction data for ChadBuffer Close (discriminator 3)
117
+ * Format: discriminator(1) only
118
+ */
119
+ function createCloseInstructionData(): Uint8Array {
120
+ return new Uint8Array([ChadBufferInstruction.Close]);
121
+ }
122
+
123
+ /**
124
+ * Upload raw Bitcoin transaction to ChadBuffer
125
+ *
126
+ * @param rpc - Solana RPC client
127
+ * @param rpcSubscriptions - Solana RPC subscriptions client
128
+ * @param payer - Transaction fee payer (KeyPairSigner)
129
+ * @param rawTx - Raw Bitcoin transaction bytes
130
+ * @param seed - Optional seed for buffer keypair derivation
131
+ * @returns Buffer address
132
+ */
133
+ export async function uploadTransactionToBuffer(
134
+ rpc: Rpc<SolanaRpcApi>,
135
+ rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>,
136
+ payer: KeyPairSigner,
137
+ rawTx: Uint8Array,
138
+ seed?: Uint8Array
139
+ ): Promise<Address> {
140
+ // Generate buffer keypair
141
+ const bufferKeypair = seed
142
+ ? await createKeyPairSignerFromBytes(seed.slice(0, 64).length === 64 ? seed.slice(0, 64) : padTo64Bytes(seed.slice(0, 32)))
143
+ : await generateKeyPairSigner();
144
+
145
+ // Calculate space: authority (32) + data
146
+ const space = AUTHORITY_SIZE + rawTx.length;
147
+ const rentExemption = await rpc.getMinimumBalanceForRentExemption(BigInt(space)).send();
148
+
149
+ // Split data into chunks using dynamic MAX_DATA_PER_WRITE
150
+ const chunks = splitIntoChunks(rawTx, MAX_DATA_PER_WRITE);
151
+
152
+ const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
153
+
154
+ // TX 1: CreateAccount only (no data yet)
155
+ const createAccountIx = getCreateAccountInstruction({
156
+ payer,
157
+ newAccount: bufferKeypair,
158
+ lamports: rentExemption,
159
+ space: BigInt(space),
160
+ programAddress: CHADBUFFER_PROGRAM_ID,
161
+ });
162
+
163
+ const { value: blockhash1 } = await rpc.getLatestBlockhash().send();
164
+ const tx1 = pipe(
165
+ createTransactionMessage({ version: 0 }),
166
+ (msg) => setTransactionMessageFeePayer(payer.address, msg),
167
+ (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash1, msg),
168
+ (msg) => appendTransactionMessageInstruction(createAccountIx, msg)
169
+ );
170
+ await sendAndConfirm(await signTransactionMessageWithSigners(tx1) as any, { commitment: "confirmed" });
171
+
172
+ // TX 2: ChadBuffer Init with first chunk
173
+ const initIx = {
174
+ programAddress: CHADBUFFER_PROGRAM_ID,
175
+ accounts: [
176
+ { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
177
+ { address: bufferKeypair.address, role: AccountRole.WRITABLE },
178
+ ],
179
+ data: createInitInstructionData(chunks[0]),
180
+ };
181
+
182
+ const { value: blockhash2 } = await rpc.getLatestBlockhash().send();
183
+ const tx2 = pipe(
184
+ createTransactionMessage({ version: 0 }),
185
+ (msg) => setTransactionMessageFeePayer(payer.address, msg),
186
+ (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash2, msg),
187
+ (msg) => appendTransactionMessageInstruction(initIx as any, msg)
188
+ );
189
+ await sendAndConfirm(await signTransactionMessageWithSigners(tx2) as any, { commitment: "confirmed" });
190
+
191
+ // TX 3+: Write remaining chunks
192
+ let offset = chunks[0].length;
193
+ for (let i = 1; i < chunks.length; i++) {
194
+ const writeIx = {
195
+ programAddress: CHADBUFFER_PROGRAM_ID,
196
+ accounts: [
197
+ { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
198
+ { address: bufferKeypair.address, role: AccountRole.WRITABLE },
199
+ ],
200
+ data: createWriteInstructionData(offset, chunks[i]),
201
+ };
202
+ offset += chunks[i].length;
203
+
204
+ const { value: blockhash } = await rpc.getLatestBlockhash().send();
205
+ const tx = pipe(
206
+ createTransactionMessage({ version: 0 }),
207
+ (msg) => setTransactionMessageFeePayer(payer.address, msg),
208
+ (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash, msg),
209
+ (msg) => appendTransactionMessageInstruction(writeIx as any, msg)
210
+ );
211
+ await sendAndConfirm(await signTransactionMessageWithSigners(tx) as any, { commitment: "confirmed" });
212
+ }
213
+
214
+ debug("chadbuffer", `Buffer created, tx ${rawTx.length}B, ${chunks.length} chunks`);
215
+
216
+ return bufferKeypair.address;
217
+ }
218
+
219
+ /**
220
+ * Helper to pad a 32-byte seed to 64 bytes for createKeyPairSignerFromBytes
221
+ */
222
+ function padTo64Bytes(seed32: Uint8Array): Uint8Array {
223
+ const padded = new Uint8Array(64);
224
+ padded.set(seed32, 0);
225
+ return padded;
226
+ }
227
+
228
+ /**
229
+ * Close buffer and reclaim rent
230
+ */
231
+ export async function closeBuffer(
232
+ rpc: Rpc<SolanaRpcApi>,
233
+ rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>,
234
+ payer: KeyPairSigner,
235
+ bufferAddress: Address,
236
+ recipient?: Address
237
+ ): Promise<string> {
238
+ // ChadBuffer Close instruction (expects exactly 2 accounts: signer + buffer)
239
+ // Lamports go back to signer automatically
240
+ const closeIx = {
241
+ programAddress: CHADBUFFER_PROGRAM_ID,
242
+ accounts: [
243
+ { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
244
+ { address: bufferAddress, role: AccountRole.WRITABLE },
245
+ ],
246
+ data: createCloseInstructionData(),
247
+ };
248
+
249
+ // Get blockhash
250
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
251
+
252
+ // Build transaction message
253
+ const txMessage = pipe(
254
+ createTransactionMessage({ version: 0 }),
255
+ (msg) => setTransactionMessageFeePayer(payer.address, msg),
256
+ (msg) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, msg),
257
+ (msg) => appendTransactionMessageInstruction(closeIx as any, msg)
258
+ );
259
+
260
+ const signedTx = await signTransactionMessageWithSigners(txMessage);
261
+
262
+ // Send and confirm
263
+ const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
264
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
+ await sendAndConfirm(signedTx as any, { commitment: "confirmed" });
266
+
267
+ // Return the signature as string
268
+ return getSignatureFromTransaction(signedTx);
269
+ }
270
+
271
+ /**
272
+ * Read buffer data
273
+ */
274
+ export async function readBufferData(
275
+ rpc: Rpc<SolanaRpcApi>,
276
+ bufferAddress: Address
277
+ ): Promise<{ authority: Address; data: Uint8Array }> {
278
+ const accountInfo = await rpc.getAccountInfo(bufferAddress, { encoding: "base64" }).send();
279
+ if (!accountInfo.value) {
280
+ throw new Error("Buffer account not found");
281
+ }
282
+
283
+ // Decode base64 data
284
+ const rawData = fromBase64(accountInfo.value.data[0]);
285
+ const authorityBytes = rawData.slice(0, AUTHORITY_SIZE);
286
+ const data = new Uint8Array(rawData.slice(AUTHORITY_SIZE));
287
+
288
+ // Convert authority bytes to Address
289
+ const authority = address(bs58Encode(authorityBytes));
290
+
291
+ return { authority, data };
292
+ }
293
+
294
+ /**
295
+ * Simple base58 encoding for addresses
296
+ */
297
+ function bs58Encode(bytes: Uint8Array): string {
298
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
299
+ const byteArray = bytes;
300
+
301
+ let num = BigInt(0);
302
+ for (let i = 0; i < byteArray.length; i++) {
303
+ num = num * BigInt(256) + BigInt(byteArray[i]);
304
+ }
305
+
306
+ let result = "";
307
+ while (num > BigInt(0)) {
308
+ result = ALPHABET[Number(num % BigInt(58))] + result;
309
+ num = num / BigInt(58);
310
+ }
311
+
312
+ // Add leading zeros
313
+ for (let i = 0; i < byteArray.length; i++) {
314
+ if (byteArray[i] === 0) {
315
+ result = "1" + result;
316
+ } else {
317
+ break;
318
+ }
319
+ }
320
+
321
+ return result || "1";
322
+ }
323
+
324
+ /**
325
+ * Split data into equal-sized chunks
326
+ */
327
+ function splitIntoChunks(data: Uint8Array, chunkSize: number): Uint8Array[] {
328
+ const chunks: Uint8Array[] = [];
329
+ for (let i = 0; i < data.length; i += chunkSize) {
330
+ chunks.push(data.slice(i, Math.min(i + chunkSize, data.length)));
331
+ }
332
+ return chunks;
333
+ }
334
+
335
+ /**
336
+ * Fetch raw Bitcoin transaction from Esplora/Blockstream API
337
+ */
338
+ export async function fetchRawTransaction(
339
+ txid: string,
340
+ network: "mainnet" | "testnet" | "testnet4" = "testnet4"
341
+ ): Promise<Uint8Array> {
342
+ const baseUrl =
343
+ network === "mainnet"
344
+ ? "https://mempool.space/api"
345
+ : `https://mempool.space/${network}/api`;
346
+
347
+ const response = await fetch(`${baseUrl}/tx/${txid}/raw`);
348
+ if (!response.ok) {
349
+ throw new Error(`Failed to fetch transaction: ${response.statusText}`);
350
+ }
351
+
352
+ const buffer = await response.arrayBuffer();
353
+ return new Uint8Array(buffer);
354
+ }
355
+
356
+ /**
357
+ * Fetch merkle proof from Esplora/Blockstream API
358
+ */
359
+ export async function fetchMerkleProof(
360
+ txid: string,
361
+ network: "mainnet" | "testnet" | "testnet4" = "testnet4"
362
+ ): Promise<{
363
+ blockHeight: number;
364
+ merkleProof: Uint8Array[];
365
+ txIndex: number;
366
+ }> {
367
+ const baseUrl =
368
+ network === "mainnet"
369
+ ? "https://mempool.space/api"
370
+ : `https://mempool.space/${network}/api`;
371
+
372
+ const response = await fetch(`${baseUrl}/tx/${txid}/merkle-proof`);
373
+ if (!response.ok) {
374
+ throw new Error(`Failed to fetch merkle proof: ${response.statusText}`);
375
+ }
376
+
377
+ const data = (await response.json()) as {
378
+ merkle: string[];
379
+ block_height: number;
380
+ pos: number;
381
+ };
382
+
383
+ // Parse merkle proof
384
+ const merkleProof = data.merkle.map((hash: string) => {
385
+ const bytes = hexToBytes(hash);
386
+ // Reverse for internal byte order
387
+ bytes.reverse();
388
+ return bytes;
389
+ });
390
+
391
+ return {
392
+ blockHeight: data.block_height,
393
+ merkleProof,
394
+ txIndex: data.pos,
395
+ };
396
+ }
397
+
398
+ // bytesToHex lives in ./crypto (single source); re-exported to preserve surface.
399
+ export { bytesToHex } from "./crypto";
400
+
401
+ /**
402
+ * Build merkle proof data for on-chain verification
403
+ *
404
+ * Layout:
405
+ * - txid: [u8; 32]
406
+ * - num_siblings: u8
407
+ * - siblings: [[u8; 32]; num_siblings]
408
+ * - tx_index: u32 (little-endian)
409
+ *
410
+ * @param txidBytes - 32-byte txid (already reversed for internal byte order)
411
+ * @param merkleProof - Array of 32-byte sibling hashes
412
+ * @param txIndex - Transaction index in block
413
+ * @returns Merkle proof data as Uint8Array
414
+ */
415
+ export function buildMerkleProof(
416
+ txidBytes: Uint8Array,
417
+ merkleProof: Uint8Array[],
418
+ txIndex: number
419
+ ): Uint8Array {
420
+ if (txidBytes.length !== 32) {
421
+ throw new Error("txid must be 32 bytes");
422
+ }
423
+
424
+ const numSiblings = merkleProof.length;
425
+ // txid (32) + num_siblings (1) + siblings (32 * n) + tx_index (4)
426
+ const totalSize = 32 + 1 + numSiblings * 32 + 4;
427
+ const data = new Uint8Array(totalSize);
428
+
429
+ let offset = 0;
430
+
431
+ // txid
432
+ data.set(txidBytes, offset);
433
+ offset += 32;
434
+
435
+ // num_siblings
436
+ data[offset++] = numSiblings;
437
+
438
+ // siblings
439
+ for (const sibling of merkleProof) {
440
+ if (sibling.length !== 32) {
441
+ throw new Error("Each sibling must be 32 bytes");
442
+ }
443
+ data.set(sibling, offset);
444
+ offset += 32;
445
+ }
446
+
447
+ // tx_index (u32 little-endian)
448
+ const indexView = new DataView(data.buffer, offset, 4);
449
+ indexView.setUint32(0, txIndex, true);
450
+
451
+ return data;
452
+ }
453
+
454
+ /**
455
+ * Complete flow: Fetch tx, upload to buffer, return verification data
456
+ */
457
+ export async function prepareVerifyDeposit(
458
+ rpc: Rpc<SolanaRpcApi>,
459
+ rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>,
460
+ payer: KeyPairSigner,
461
+ txid: string,
462
+ network: "mainnet" | "testnet" | "testnet4" = "testnet4"
463
+ ): Promise<{
464
+ bufferAddress: Address;
465
+ transactionSize: number;
466
+ merkleProof: Uint8Array[];
467
+ blockHeight: number;
468
+ txIndex: number;
469
+ txidBytes: Uint8Array;
470
+ }> {
471
+ debug("chadbuffer", `Preparing verification for txid: ${txid.slice(0, 12)}...`);
472
+
473
+ // Fetch raw transaction
474
+ debug("chadbuffer", "Fetching raw transaction...");
475
+ const rawTx = await fetchRawTransaction(txid, network);
476
+ debug("chadbuffer", `Raw tx size: ${rawTx.length} bytes`);
477
+
478
+ // Fetch merkle proof
479
+ debug("chadbuffer", "Fetching merkle proof...");
480
+ const { blockHeight, merkleProof, txIndex } = await fetchMerkleProof(
481
+ txid,
482
+ network
483
+ );
484
+ debug("chadbuffer", `Block height: ${blockHeight}, tx index: ${txIndex}`);
485
+
486
+ // Upload to ChadBuffer
487
+ debug("chadbuffer", "Uploading to ChadBuffer...");
488
+ const bufferAddress = await uploadTransactionToBuffer(
489
+ rpc,
490
+ rpcSubscriptions,
491
+ payer,
492
+ rawTx
493
+ );
494
+
495
+ // Convert txid to bytes (reversed)
496
+ const txidBytes = hexToBytes(txid);
497
+ txidBytes.reverse();
498
+
499
+ return {
500
+ bufferAddress,
501
+ transactionSize: rawTx.length,
502
+ merkleProof,
503
+ blockHeight,
504
+ txIndex,
505
+ txidBytes,
506
+ };
507
+ }
508
+
509
+ // =============================================================================
510
+ // Proof Upload Utilities
511
+ // =============================================================================
512
+
513
+ /**
514
+ * Check if a proof needs buffer mode (too large for inline)
515
+ *
516
+ * @param proofBytes - Proof data
517
+ * @param availableSpace - Available space in transaction (default: use buffer for any proof > 900 bytes)
518
+ */
519
+ export function needsBuffer(proofBytes: Uint8Array, availableSpace: number = 900): boolean {
520
+ return proofBytes.length > availableSpace;
521
+ }
522
+
523
+ /**
524
+ * Result of uploading a proof to buffer
525
+ */
526
+ export interface ProofUploadResult {
527
+ /** Buffer account address */
528
+ bufferAddress: Address;
529
+ /** Whether buffer was used (false = inline) */
530
+ usedBuffer: boolean;
531
+ /** Number of chunks uploaded */
532
+ chunksUploaded: number;
533
+ /** Total proof size */
534
+ proofSize: number;
535
+ }
536
+
537
+ /**
538
+ * Upload proof to ChadBuffer if needed
539
+ *
540
+ * Automatically determines if buffer mode is needed based on proof size.
541
+ * Returns buffer address for buffer mode, or null for inline mode.
542
+ *
543
+ * @param rpc - Solana RPC client
544
+ * @param rpcSubscriptions - Solana RPC subscriptions client
545
+ * @param payer - Transaction fee payer
546
+ * @param proofBytes - Raw proof bytes
547
+ * @returns Upload result with buffer address (or null if inline)
548
+ */
549
+ export async function uploadProofToBuffer(
550
+ rpc: Rpc<SolanaRpcApi>,
551
+ rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>,
552
+ payer: KeyPairSigner,
553
+ proofBytes: Uint8Array
554
+ ): Promise<ProofUploadResult> {
555
+ if (!needsBuffer(proofBytes)) {
556
+ return {
557
+ bufferAddress: null as unknown as Address,
558
+ usedBuffer: false,
559
+ chunksUploaded: 0,
560
+ proofSize: proofBytes.length,
561
+ };
562
+ }
563
+
564
+ const bufferAddress = await uploadTransactionToBuffer(
565
+ rpc,
566
+ rpcSubscriptions,
567
+ payer,
568
+ proofBytes
569
+ );
570
+
571
+ const chunks = Math.ceil(proofBytes.length / MAX_DATA_PER_WRITE);
572
+
573
+ return {
574
+ bufferAddress,
575
+ usedBuffer: true,
576
+ chunksUploaded: chunks,
577
+ proofSize: proofBytes.length,
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Helper to determine proof source mode
583
+ */
584
+ export function getProofSource(proofBytes: Uint8Array): "inline" | "buffer" {
585
+ return needsBuffer(proofBytes) ? "buffer" : "inline";
586
+ }
587
+
588
+ /**
589
+ * Calculate required number of transactions for a proof upload
590
+ *
591
+ * @param proofSize - Size of proof in bytes
592
+ * @param useBuffer - Force buffer mode (default: true for proofs > MAX_DATA_PER_WRITE)
593
+ */
594
+ export function calculateUploadTransactions(proofSize: number, useBuffer: boolean = true): number {
595
+ if (!useBuffer) {
596
+ return 0; // Inline mode
597
+ }
598
+ // TX 1: CreateAccount
599
+ // TX 2: Init with first chunk
600
+ // TX 3+: Write remaining chunks
601
+ const chunks = Math.ceil(proofSize / MAX_DATA_PER_WRITE);
602
+ return 1 + chunks; // CreateAccount + Init + (chunks-1) writes = 1 + chunks
603
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Type declarations for circomlibjs
3
+ *
4
+ * Circomlibjs provides Circom-compatible cryptographic primitives
5
+ * including Poseidon hash function and EdDSA signing.
6
+ */
7
+
8
+ declare module 'circomlibjs' {
9
+ /** Finite field element type */
10
+ export interface F {
11
+ toObject(element: unknown): bigint;
12
+ e(value: bigint | number | string): unknown;
13
+ }
14
+
15
+ /** Poseidon hash function instance */
16
+ export interface Poseidon {
17
+ (inputs: bigint[]): unknown;
18
+ F: F;
19
+ }
20
+
21
+ /**
22
+ * Build a Poseidon hash function instance
23
+ * Uses BN254 parameters compatible with Solana's sol_poseidon syscall
24
+ */
25
+ export function buildPoseidon(): Promise<Poseidon>;
26
+
27
+ /** Baby Jubjub curve interface */
28
+ export interface BabyJub {
29
+ F: F;
30
+ }
31
+
32
+ /** EdDSA-Poseidon signature */
33
+ export interface EdDSAPoseidonSignature {
34
+ R8: [unknown, unknown];
35
+ S: bigint;
36
+ }
37
+
38
+ /** EdDSA instance for Poseidon-based signing */
39
+ export interface Eddsa {
40
+ babyJub: BabyJub;
41
+ pruneBuffer(buff: Buffer): Buffer;
42
+ prv2pub(privKey: Buffer | Uint8Array): [unknown, unknown];
43
+ signPoseidon(privKey: Buffer | Uint8Array, msg: unknown): EdDSAPoseidonSignature;
44
+ }
45
+
46
+ /**
47
+ * Build an EdDSA instance for Poseidon-based signing
48
+ * Compatible with circom's EdDSAPoseidonVerifier
49
+ */
50
+ export function buildEddsa(): Promise<Eddsa>;
51
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Claim link utilities for UTXOpia
3
+ *
4
+ * Claim links encode a seed phrase in a URL fragment (#note=...) so the
5
+ * secret is never sent to the server (fragments are client-side only).
6
+ *
7
+ * SECURITY: Claim links are bearer instruments - anyone with the link can claim!
8
+ */
9
+
10
+ /**
11
+ * Encode a seed phrase for use in claim links.
12
+ * URL-encodes the seed — much shorter than encoding nullifier+secret.
13
+ *
14
+ * @param seed - Seed phrase (user's secret note)
15
+ * @returns URL-safe encoded string
16
+ */
17
+ export function encodeClaimLink(seed: string): string {
18
+ return encodeURIComponent(seed);
19
+ }
20
+
21
+ /**
22
+ * Decode a claim link seed.
23
+ *
24
+ * @param encoded - URL-encoded seed string
25
+ * @returns Decoded seed string, or null if invalid
26
+ */
27
+ export function decodeClaimLink(encoded: string): string | null {
28
+ try {
29
+ const decoded = decodeURIComponent(encoded);
30
+ if (/^[a-zA-Z0-9]/.test(decoded) && decoded.length >= 8) {
31
+ return decoded;
32
+ }
33
+ } catch {
34
+ // Not URL-encoded
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Parse claim URL — reads seed from URL fragment (#note=...).
42
+ *
43
+ * @param url - URL string
44
+ * @returns Seed string or null if invalid
45
+ */
46
+ export function parseClaimUrl(url: string): string | null {
47
+ if (url.includes("#note=")) {
48
+ const encoded = url.split("#note=")[1].split("&")[0];
49
+ if (encoded) return decodeClaimLink(encoded);
50
+ }
51
+
52
+ return null;
53
+ }