@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,405 @@
|
|
|
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
|
+
import { fromBase64 } from "./utils/encoding";
|
|
12
|
+
import { hexToBytes } from "./crypto";
|
|
13
|
+
import { generateKeyPairSigner, createKeyPairSignerFromBytes, pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, signTransactionMessageWithSigners, getSignatureFromTransaction, sendAndConfirmTransactionFactory, AccountRole, } from "@solana/kit";
|
|
14
|
+
import { getCreateAccountInstruction } from "@solana-program/system";
|
|
15
|
+
import { address } from "./config";
|
|
16
|
+
import { debug } from "./logger";
|
|
17
|
+
// ChadBuffer Program ID (deployed to devnet 2025-01-30)
|
|
18
|
+
export const CHADBUFFER_PROGRAM_ID = address("C5RpjtTMFXKVZCtXSzKXD4CDNTaWBg3dVeMfYvjZYHDF");
|
|
19
|
+
// System Program ID
|
|
20
|
+
const SYSTEM_PROGRAM_ID = address("11111111111111111111111111111111");
|
|
21
|
+
// Buffer authority size (32 bytes)
|
|
22
|
+
export const AUTHORITY_SIZE = 32;
|
|
23
|
+
// Solana transaction limit
|
|
24
|
+
export const SOLANA_TX_SIZE_LIMIT = 1232;
|
|
25
|
+
/**
|
|
26
|
+
* ChadBuffer Write TX overhead breakdown:
|
|
27
|
+
* - Signature: 64 bytes
|
|
28
|
+
* - Message header: 3 bytes
|
|
29
|
+
* - 2 account metas (payer + buffer): 66 bytes (33 each)
|
|
30
|
+
* - Instruction header: 4 bytes
|
|
31
|
+
* - Discriminator: 1 byte
|
|
32
|
+
* - u24 offset: 3 bytes
|
|
33
|
+
* Total fixed overhead: ~141 bytes (using 176 for safety margin)
|
|
34
|
+
*/
|
|
35
|
+
const WRITE_TX_OVERHEAD = 176;
|
|
36
|
+
/**
|
|
37
|
+
* Maximum data bytes per ChadBuffer Write transaction
|
|
38
|
+
* Dynamically calculated from TX limit minus overhead
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_DATA_PER_WRITE = SOLANA_TX_SIZE_LIMIT - WRITE_TX_OVERHEAD;
|
|
41
|
+
/**
|
|
42
|
+
* ChadBuffer instruction discriminators
|
|
43
|
+
* See: https://github.com/deanmlittle/chadbuffer/blob/main/src/lib.rs
|
|
44
|
+
*/
|
|
45
|
+
var ChadBufferInstruction;
|
|
46
|
+
(function (ChadBufferInstruction) {
|
|
47
|
+
ChadBufferInstruction[ChadBufferInstruction["Create"] = 0] = "Create";
|
|
48
|
+
ChadBufferInstruction[ChadBufferInstruction["Assign"] = 1] = "Assign";
|
|
49
|
+
ChadBufferInstruction[ChadBufferInstruction["Write"] = 2] = "Write";
|
|
50
|
+
ChadBufferInstruction[ChadBufferInstruction["Close"] = 3] = "Close";
|
|
51
|
+
})(ChadBufferInstruction || (ChadBufferInstruction = {}));
|
|
52
|
+
/**
|
|
53
|
+
* Create instruction data for ChadBuffer Init (discriminator 0)
|
|
54
|
+
* Format: discriminator(1) + data
|
|
55
|
+
*/
|
|
56
|
+
function createInitInstructionData(data) {
|
|
57
|
+
const buffer = new Uint8Array(1 + data.length);
|
|
58
|
+
buffer[0] = ChadBufferInstruction.Create;
|
|
59
|
+
buffer.set(data, 1);
|
|
60
|
+
return buffer;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create instruction data for ChadBuffer Write (discriminator 2)
|
|
64
|
+
* Format: discriminator(1) + u24_offset(3) + data
|
|
65
|
+
*/
|
|
66
|
+
function createWriteInstructionData(offset, data) {
|
|
67
|
+
const buffer = new Uint8Array(1 + 3 + data.length);
|
|
68
|
+
buffer[0] = ChadBufferInstruction.Write;
|
|
69
|
+
// u24 offset (little-endian)
|
|
70
|
+
buffer[1] = offset & 0xff;
|
|
71
|
+
buffer[2] = (offset >> 8) & 0xff;
|
|
72
|
+
buffer[3] = (offset >> 16) & 0xff;
|
|
73
|
+
buffer.set(data, 4);
|
|
74
|
+
return buffer;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create instruction data for ChadBuffer Close (discriminator 3)
|
|
78
|
+
* Format: discriminator(1) only
|
|
79
|
+
*/
|
|
80
|
+
function createCloseInstructionData() {
|
|
81
|
+
return new Uint8Array([ChadBufferInstruction.Close]);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Upload raw Bitcoin transaction to ChadBuffer
|
|
85
|
+
*
|
|
86
|
+
* @param rpc - Solana RPC client
|
|
87
|
+
* @param rpcSubscriptions - Solana RPC subscriptions client
|
|
88
|
+
* @param payer - Transaction fee payer (KeyPairSigner)
|
|
89
|
+
* @param rawTx - Raw Bitcoin transaction bytes
|
|
90
|
+
* @param seed - Optional seed for buffer keypair derivation
|
|
91
|
+
* @returns Buffer address
|
|
92
|
+
*/
|
|
93
|
+
export async function uploadTransactionToBuffer(rpc, rpcSubscriptions, payer, rawTx, seed) {
|
|
94
|
+
// Generate buffer keypair
|
|
95
|
+
const bufferKeypair = seed
|
|
96
|
+
? await createKeyPairSignerFromBytes(seed.slice(0, 64).length === 64 ? seed.slice(0, 64) : padTo64Bytes(seed.slice(0, 32)))
|
|
97
|
+
: await generateKeyPairSigner();
|
|
98
|
+
// Calculate space: authority (32) + data
|
|
99
|
+
const space = AUTHORITY_SIZE + rawTx.length;
|
|
100
|
+
const rentExemption = await rpc.getMinimumBalanceForRentExemption(BigInt(space)).send();
|
|
101
|
+
// Split data into chunks using dynamic MAX_DATA_PER_WRITE
|
|
102
|
+
const chunks = splitIntoChunks(rawTx, MAX_DATA_PER_WRITE);
|
|
103
|
+
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
|
|
104
|
+
// TX 1: CreateAccount only (no data yet)
|
|
105
|
+
const createAccountIx = getCreateAccountInstruction({
|
|
106
|
+
payer,
|
|
107
|
+
newAccount: bufferKeypair,
|
|
108
|
+
lamports: rentExemption,
|
|
109
|
+
space: BigInt(space),
|
|
110
|
+
programAddress: CHADBUFFER_PROGRAM_ID,
|
|
111
|
+
});
|
|
112
|
+
const { value: blockhash1 } = await rpc.getLatestBlockhash().send();
|
|
113
|
+
const tx1 = pipe(createTransactionMessage({ version: 0 }), (msg) => setTransactionMessageFeePayer(payer.address, msg), (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash1, msg), (msg) => appendTransactionMessageInstruction(createAccountIx, msg));
|
|
114
|
+
await sendAndConfirm(await signTransactionMessageWithSigners(tx1), { commitment: "confirmed" });
|
|
115
|
+
// TX 2: ChadBuffer Init with first chunk
|
|
116
|
+
const initIx = {
|
|
117
|
+
programAddress: CHADBUFFER_PROGRAM_ID,
|
|
118
|
+
accounts: [
|
|
119
|
+
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
|
|
120
|
+
{ address: bufferKeypair.address, role: AccountRole.WRITABLE },
|
|
121
|
+
],
|
|
122
|
+
data: createInitInstructionData(chunks[0]),
|
|
123
|
+
};
|
|
124
|
+
const { value: blockhash2 } = await rpc.getLatestBlockhash().send();
|
|
125
|
+
const tx2 = pipe(createTransactionMessage({ version: 0 }), (msg) => setTransactionMessageFeePayer(payer.address, msg), (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash2, msg), (msg) => appendTransactionMessageInstruction(initIx, msg));
|
|
126
|
+
await sendAndConfirm(await signTransactionMessageWithSigners(tx2), { commitment: "confirmed" });
|
|
127
|
+
// TX 3+: Write remaining chunks
|
|
128
|
+
let offset = chunks[0].length;
|
|
129
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
130
|
+
const writeIx = {
|
|
131
|
+
programAddress: CHADBUFFER_PROGRAM_ID,
|
|
132
|
+
accounts: [
|
|
133
|
+
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
|
|
134
|
+
{ address: bufferKeypair.address, role: AccountRole.WRITABLE },
|
|
135
|
+
],
|
|
136
|
+
data: createWriteInstructionData(offset, chunks[i]),
|
|
137
|
+
};
|
|
138
|
+
offset += chunks[i].length;
|
|
139
|
+
const { value: blockhash } = await rpc.getLatestBlockhash().send();
|
|
140
|
+
const tx = pipe(createTransactionMessage({ version: 0 }), (msg) => setTransactionMessageFeePayer(payer.address, msg), (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash, msg), (msg) => appendTransactionMessageInstruction(writeIx, msg));
|
|
141
|
+
await sendAndConfirm(await signTransactionMessageWithSigners(tx), { commitment: "confirmed" });
|
|
142
|
+
}
|
|
143
|
+
debug("chadbuffer", `Buffer created, tx ${rawTx.length}B, ${chunks.length} chunks`);
|
|
144
|
+
return bufferKeypair.address;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Helper to pad a 32-byte seed to 64 bytes for createKeyPairSignerFromBytes
|
|
148
|
+
*/
|
|
149
|
+
function padTo64Bytes(seed32) {
|
|
150
|
+
const padded = new Uint8Array(64);
|
|
151
|
+
padded.set(seed32, 0);
|
|
152
|
+
return padded;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Close buffer and reclaim rent
|
|
156
|
+
*/
|
|
157
|
+
export async function closeBuffer(rpc, rpcSubscriptions, payer, bufferAddress, recipient) {
|
|
158
|
+
// ChadBuffer Close instruction (expects exactly 2 accounts: signer + buffer)
|
|
159
|
+
// Lamports go back to signer automatically
|
|
160
|
+
const closeIx = {
|
|
161
|
+
programAddress: CHADBUFFER_PROGRAM_ID,
|
|
162
|
+
accounts: [
|
|
163
|
+
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
|
|
164
|
+
{ address: bufferAddress, role: AccountRole.WRITABLE },
|
|
165
|
+
],
|
|
166
|
+
data: createCloseInstructionData(),
|
|
167
|
+
};
|
|
168
|
+
// Get blockhash
|
|
169
|
+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
|
|
170
|
+
// Build transaction message
|
|
171
|
+
const txMessage = pipe(createTransactionMessage({ version: 0 }), (msg) => setTransactionMessageFeePayer(payer.address, msg), (msg) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, msg), (msg) => appendTransactionMessageInstruction(closeIx, msg));
|
|
172
|
+
const signedTx = await signTransactionMessageWithSigners(txMessage);
|
|
173
|
+
// Send and confirm
|
|
174
|
+
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
await sendAndConfirm(signedTx, { commitment: "confirmed" });
|
|
177
|
+
// Return the signature as string
|
|
178
|
+
return getSignatureFromTransaction(signedTx);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Read buffer data
|
|
182
|
+
*/
|
|
183
|
+
export async function readBufferData(rpc, bufferAddress) {
|
|
184
|
+
const accountInfo = await rpc.getAccountInfo(bufferAddress, { encoding: "base64" }).send();
|
|
185
|
+
if (!accountInfo.value) {
|
|
186
|
+
throw new Error("Buffer account not found");
|
|
187
|
+
}
|
|
188
|
+
// Decode base64 data
|
|
189
|
+
const rawData = fromBase64(accountInfo.value.data[0]);
|
|
190
|
+
const authorityBytes = rawData.slice(0, AUTHORITY_SIZE);
|
|
191
|
+
const data = new Uint8Array(rawData.slice(AUTHORITY_SIZE));
|
|
192
|
+
// Convert authority bytes to Address
|
|
193
|
+
const authority = address(bs58Encode(authorityBytes));
|
|
194
|
+
return { authority, data };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Simple base58 encoding for addresses
|
|
198
|
+
*/
|
|
199
|
+
function bs58Encode(bytes) {
|
|
200
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
201
|
+
const byteArray = bytes;
|
|
202
|
+
let num = BigInt(0);
|
|
203
|
+
for (let i = 0; i < byteArray.length; i++) {
|
|
204
|
+
num = num * BigInt(256) + BigInt(byteArray[i]);
|
|
205
|
+
}
|
|
206
|
+
let result = "";
|
|
207
|
+
while (num > BigInt(0)) {
|
|
208
|
+
result = ALPHABET[Number(num % BigInt(58))] + result;
|
|
209
|
+
num = num / BigInt(58);
|
|
210
|
+
}
|
|
211
|
+
// Add leading zeros
|
|
212
|
+
for (let i = 0; i < byteArray.length; i++) {
|
|
213
|
+
if (byteArray[i] === 0) {
|
|
214
|
+
result = "1" + result;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result || "1";
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Split data into equal-sized chunks
|
|
224
|
+
*/
|
|
225
|
+
function splitIntoChunks(data, chunkSize) {
|
|
226
|
+
const chunks = [];
|
|
227
|
+
for (let i = 0; i < data.length; i += chunkSize) {
|
|
228
|
+
chunks.push(data.slice(i, Math.min(i + chunkSize, data.length)));
|
|
229
|
+
}
|
|
230
|
+
return chunks;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Fetch raw Bitcoin transaction from Esplora/Blockstream API
|
|
234
|
+
*/
|
|
235
|
+
export async function fetchRawTransaction(txid, network = "testnet4") {
|
|
236
|
+
const baseUrl = network === "mainnet"
|
|
237
|
+
? "https://mempool.space/api"
|
|
238
|
+
: `https://mempool.space/${network}/api`;
|
|
239
|
+
const response = await fetch(`${baseUrl}/tx/${txid}/raw`);
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(`Failed to fetch transaction: ${response.statusText}`);
|
|
242
|
+
}
|
|
243
|
+
const buffer = await response.arrayBuffer();
|
|
244
|
+
return new Uint8Array(buffer);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Fetch merkle proof from Esplora/Blockstream API
|
|
248
|
+
*/
|
|
249
|
+
export async function fetchMerkleProof(txid, network = "testnet4") {
|
|
250
|
+
const baseUrl = network === "mainnet"
|
|
251
|
+
? "https://mempool.space/api"
|
|
252
|
+
: `https://mempool.space/${network}/api`;
|
|
253
|
+
const response = await fetch(`${baseUrl}/tx/${txid}/merkle-proof`);
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
throw new Error(`Failed to fetch merkle proof: ${response.statusText}`);
|
|
256
|
+
}
|
|
257
|
+
const data = (await response.json());
|
|
258
|
+
// Parse merkle proof
|
|
259
|
+
const merkleProof = data.merkle.map((hash) => {
|
|
260
|
+
const bytes = hexToBytes(hash);
|
|
261
|
+
// Reverse for internal byte order
|
|
262
|
+
bytes.reverse();
|
|
263
|
+
return bytes;
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
blockHeight: data.block_height,
|
|
267
|
+
merkleProof,
|
|
268
|
+
txIndex: data.pos,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// bytesToHex lives in ./crypto (single source); re-exported to preserve surface.
|
|
272
|
+
export { bytesToHex } from "./crypto";
|
|
273
|
+
/**
|
|
274
|
+
* Build merkle proof data for on-chain verification
|
|
275
|
+
*
|
|
276
|
+
* Layout:
|
|
277
|
+
* - txid: [u8; 32]
|
|
278
|
+
* - num_siblings: u8
|
|
279
|
+
* - siblings: [[u8; 32]; num_siblings]
|
|
280
|
+
* - tx_index: u32 (little-endian)
|
|
281
|
+
*
|
|
282
|
+
* @param txidBytes - 32-byte txid (already reversed for internal byte order)
|
|
283
|
+
* @param merkleProof - Array of 32-byte sibling hashes
|
|
284
|
+
* @param txIndex - Transaction index in block
|
|
285
|
+
* @returns Merkle proof data as Uint8Array
|
|
286
|
+
*/
|
|
287
|
+
export function buildMerkleProof(txidBytes, merkleProof, txIndex) {
|
|
288
|
+
if (txidBytes.length !== 32) {
|
|
289
|
+
throw new Error("txid must be 32 bytes");
|
|
290
|
+
}
|
|
291
|
+
const numSiblings = merkleProof.length;
|
|
292
|
+
// txid (32) + num_siblings (1) + siblings (32 * n) + tx_index (4)
|
|
293
|
+
const totalSize = 32 + 1 + numSiblings * 32 + 4;
|
|
294
|
+
const data = new Uint8Array(totalSize);
|
|
295
|
+
let offset = 0;
|
|
296
|
+
// txid
|
|
297
|
+
data.set(txidBytes, offset);
|
|
298
|
+
offset += 32;
|
|
299
|
+
// num_siblings
|
|
300
|
+
data[offset++] = numSiblings;
|
|
301
|
+
// siblings
|
|
302
|
+
for (const sibling of merkleProof) {
|
|
303
|
+
if (sibling.length !== 32) {
|
|
304
|
+
throw new Error("Each sibling must be 32 bytes");
|
|
305
|
+
}
|
|
306
|
+
data.set(sibling, offset);
|
|
307
|
+
offset += 32;
|
|
308
|
+
}
|
|
309
|
+
// tx_index (u32 little-endian)
|
|
310
|
+
const indexView = new DataView(data.buffer, offset, 4);
|
|
311
|
+
indexView.setUint32(0, txIndex, true);
|
|
312
|
+
return data;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Complete flow: Fetch tx, upload to buffer, return verification data
|
|
316
|
+
*/
|
|
317
|
+
export async function prepareVerifyDeposit(rpc, rpcSubscriptions, payer, txid, network = "testnet4") {
|
|
318
|
+
debug("chadbuffer", `Preparing verification for txid: ${txid.slice(0, 12)}...`);
|
|
319
|
+
// Fetch raw transaction
|
|
320
|
+
debug("chadbuffer", "Fetching raw transaction...");
|
|
321
|
+
const rawTx = await fetchRawTransaction(txid, network);
|
|
322
|
+
debug("chadbuffer", `Raw tx size: ${rawTx.length} bytes`);
|
|
323
|
+
// Fetch merkle proof
|
|
324
|
+
debug("chadbuffer", "Fetching merkle proof...");
|
|
325
|
+
const { blockHeight, merkleProof, txIndex } = await fetchMerkleProof(txid, network);
|
|
326
|
+
debug("chadbuffer", `Block height: ${blockHeight}, tx index: ${txIndex}`);
|
|
327
|
+
// Upload to ChadBuffer
|
|
328
|
+
debug("chadbuffer", "Uploading to ChadBuffer...");
|
|
329
|
+
const bufferAddress = await uploadTransactionToBuffer(rpc, rpcSubscriptions, payer, rawTx);
|
|
330
|
+
// Convert txid to bytes (reversed)
|
|
331
|
+
const txidBytes = hexToBytes(txid);
|
|
332
|
+
txidBytes.reverse();
|
|
333
|
+
return {
|
|
334
|
+
bufferAddress,
|
|
335
|
+
transactionSize: rawTx.length,
|
|
336
|
+
merkleProof,
|
|
337
|
+
blockHeight,
|
|
338
|
+
txIndex,
|
|
339
|
+
txidBytes,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// =============================================================================
|
|
343
|
+
// Proof Upload Utilities
|
|
344
|
+
// =============================================================================
|
|
345
|
+
/**
|
|
346
|
+
* Check if a proof needs buffer mode (too large for inline)
|
|
347
|
+
*
|
|
348
|
+
* @param proofBytes - Proof data
|
|
349
|
+
* @param availableSpace - Available space in transaction (default: use buffer for any proof > 900 bytes)
|
|
350
|
+
*/
|
|
351
|
+
export function needsBuffer(proofBytes, availableSpace = 900) {
|
|
352
|
+
return proofBytes.length > availableSpace;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Upload proof to ChadBuffer if needed
|
|
356
|
+
*
|
|
357
|
+
* Automatically determines if buffer mode is needed based on proof size.
|
|
358
|
+
* Returns buffer address for buffer mode, or null for inline mode.
|
|
359
|
+
*
|
|
360
|
+
* @param rpc - Solana RPC client
|
|
361
|
+
* @param rpcSubscriptions - Solana RPC subscriptions client
|
|
362
|
+
* @param payer - Transaction fee payer
|
|
363
|
+
* @param proofBytes - Raw proof bytes
|
|
364
|
+
* @returns Upload result with buffer address (or null if inline)
|
|
365
|
+
*/
|
|
366
|
+
export async function uploadProofToBuffer(rpc, rpcSubscriptions, payer, proofBytes) {
|
|
367
|
+
if (!needsBuffer(proofBytes)) {
|
|
368
|
+
return {
|
|
369
|
+
bufferAddress: null,
|
|
370
|
+
usedBuffer: false,
|
|
371
|
+
chunksUploaded: 0,
|
|
372
|
+
proofSize: proofBytes.length,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const bufferAddress = await uploadTransactionToBuffer(rpc, rpcSubscriptions, payer, proofBytes);
|
|
376
|
+
const chunks = Math.ceil(proofBytes.length / MAX_DATA_PER_WRITE);
|
|
377
|
+
return {
|
|
378
|
+
bufferAddress,
|
|
379
|
+
usedBuffer: true,
|
|
380
|
+
chunksUploaded: chunks,
|
|
381
|
+
proofSize: proofBytes.length,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Helper to determine proof source mode
|
|
386
|
+
*/
|
|
387
|
+
export function getProofSource(proofBytes) {
|
|
388
|
+
return needsBuffer(proofBytes) ? "buffer" : "inline";
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Calculate required number of transactions for a proof upload
|
|
392
|
+
*
|
|
393
|
+
* @param proofSize - Size of proof in bytes
|
|
394
|
+
* @param useBuffer - Force buffer mode (default: true for proofs > MAX_DATA_PER_WRITE)
|
|
395
|
+
*/
|
|
396
|
+
export function calculateUploadTransactions(proofSize, useBuffer = true) {
|
|
397
|
+
if (!useBuffer) {
|
|
398
|
+
return 0; // Inline mode
|
|
399
|
+
}
|
|
400
|
+
// TX 1: CreateAccount
|
|
401
|
+
// TX 2: Init with first chunk
|
|
402
|
+
// TX 3+: Write remaining chunks
|
|
403
|
+
const chunks = Math.ceil(proofSize / MAX_DATA_PER_WRITE);
|
|
404
|
+
return 1 + chunks; // CreateAccount + Init + (chunks-1) writes = 1 + chunks
|
|
405
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
* Encode a seed phrase for use in claim links.
|
|
11
|
+
* URL-encodes the seed — much shorter than encoding nullifier+secret.
|
|
12
|
+
*
|
|
13
|
+
* @param seed - Seed phrase (user's secret note)
|
|
14
|
+
* @returns URL-safe encoded string
|
|
15
|
+
*/
|
|
16
|
+
export declare function encodeClaimLink(seed: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Decode a claim link seed.
|
|
19
|
+
*
|
|
20
|
+
* @param encoded - URL-encoded seed string
|
|
21
|
+
* @returns Decoded seed string, or null if invalid
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeClaimLink(encoded: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Parse claim URL — reads seed from URL fragment (#note=...).
|
|
26
|
+
*
|
|
27
|
+
* @param url - URL string
|
|
28
|
+
* @returns Seed string or null if invalid
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseClaimUrl(url: string): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
* Encode a seed phrase for use in claim links.
|
|
11
|
+
* URL-encodes the seed — much shorter than encoding nullifier+secret.
|
|
12
|
+
*
|
|
13
|
+
* @param seed - Seed phrase (user's secret note)
|
|
14
|
+
* @returns URL-safe encoded string
|
|
15
|
+
*/
|
|
16
|
+
export function encodeClaimLink(seed) {
|
|
17
|
+
return encodeURIComponent(seed);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decode a claim link seed.
|
|
21
|
+
*
|
|
22
|
+
* @param encoded - URL-encoded seed string
|
|
23
|
+
* @returns Decoded seed string, or null if invalid
|
|
24
|
+
*/
|
|
25
|
+
export function decodeClaimLink(encoded) {
|
|
26
|
+
try {
|
|
27
|
+
const decoded = decodeURIComponent(encoded);
|
|
28
|
+
if (/^[a-zA-Z0-9]/.test(decoded) && decoded.length >= 8) {
|
|
29
|
+
return decoded;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not URL-encoded
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse claim URL — reads seed from URL fragment (#note=...).
|
|
39
|
+
*
|
|
40
|
+
* @param url - URL string
|
|
41
|
+
* @returns Seed string or null if invalid
|
|
42
|
+
*/
|
|
43
|
+
export function parseClaimUrl(url) {
|
|
44
|
+
if (url.includes("#note=")) {
|
|
45
|
+
const encoded = url.split("#note=")[1].split("&")[0];
|
|
46
|
+
if (encoded)
|
|
47
|
+
return decodeClaimLink(encoded);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|