@ckbfs/api 1.3.0 → 1.4.0

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.
@@ -0,0 +1,580 @@
1
+ import {
2
+ CKBFS,
3
+ NetworkType,
4
+ ProtocolVersion,
5
+ ProtocolVersionType,
6
+ CKBFSDataType,
7
+ CKBFSData,
8
+ decodeWitnessContent,
9
+ decodeMultipleWitnessContents,
10
+ extractFileFromWitnesses,
11
+ decodeFileFromWitnessData,
12
+ saveFileFromWitnessData,
13
+ getFileContentFromChain,
14
+ saveFileFromChain,
15
+ getFileContentFromChainByTypeId,
16
+ saveFileFromChainByTypeId,
17
+ decodeFileFromChainByTypeId,
18
+ getFileContentFromChainByIdentifier,
19
+ saveFileFromChainByIdentifier,
20
+ decodeFileFromChainByIdentifier,
21
+ parseIdentifier,
22
+ IdentifierType,
23
+ } from "../src/index";
24
+ import { Script, ClientPublicTestnet, Transaction, ccc } from "@ckb-ccc/core";
25
+
26
+ // Replace with your actual private key (optional for this example)
27
+ const privateKey = process.env.CKB_PRIVATE_KEY || "your-private-key-here";
28
+
29
+ // Parse command line arguments for transaction hash
30
+ const txHashArg = process.argv.find((arg) => arg.startsWith("--txhash="));
31
+ const targetTxHash = txHashArg
32
+ ? txHashArg.split("=")[1]
33
+ : process.env.TARGET_TX_HASH ||
34
+ "0x0000000000000000000000000000000000000000000000000000000000000000";
35
+
36
+ /**
37
+ * Ensures a string is prefixed with '0x'
38
+ * @param value The string to ensure is hex prefixed
39
+ * @returns A hex prefixed string
40
+ */
41
+ function ensureHexPrefix(value: string): `0x${string}` {
42
+ if (value.startsWith("0x")) {
43
+ return value as `0x${string}`;
44
+ }
45
+ return `0x${value}` as `0x${string}`;
46
+ }
47
+
48
+ // Initialize CKB client for testnet
49
+ const client = new ClientPublicTestnet();
50
+
51
+ /**
52
+ * Example of retrieving and decoding file content using traditional blockchain method
53
+ */
54
+ async function traditionalRetrieveExample() {
55
+ console.log("=== Traditional Blockchain Retrieval Method ===");
56
+
57
+ try {
58
+ // Get transaction from blockchain
59
+ const txWithStatus = await client.getTransaction(targetTxHash);
60
+ if (!txWithStatus || !txWithStatus.transaction) {
61
+ throw new Error(`Transaction ${targetTxHash} not found`);
62
+ }
63
+
64
+ const tx = Transaction.from(txWithStatus.transaction);
65
+ console.log(`Transaction found with ${tx.outputs.length} outputs`);
66
+
67
+ // Find CKBFS cell (first output with type script)
68
+ const ckbfsCellIndex = 0;
69
+ const output = tx.outputs[ckbfsCellIndex];
70
+ if (!output || !output.type) {
71
+ throw new Error("No CKBFS cell found in the transaction");
72
+ }
73
+
74
+ // Get and parse output data
75
+ const outputData = tx.outputsData[ckbfsCellIndex];
76
+ if (!outputData) {
77
+ throw new Error("Output data not found");
78
+ }
79
+
80
+ const rawData = outputData.startsWith("0x")
81
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
82
+ : Buffer.from(outputData, "hex");
83
+
84
+ // Try both protocol versions for unpacking
85
+ let ckbfsData: CKBFSDataType;
86
+ let version: ProtocolVersionType;
87
+
88
+ try {
89
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
90
+ version = ProtocolVersion.V2;
91
+ } catch (error) {
92
+ console.log("Failed to unpack as V2, trying V1...");
93
+ try {
94
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
95
+ version = ProtocolVersion.V1;
96
+ } catch (v1Error) {
97
+ throw new Error(
98
+ `Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
99
+ );
100
+ }
101
+ }
102
+
103
+ console.log(`Successfully unpacked CKBFS data using ${version}:`);
104
+ console.log(`- Filename: ${ckbfsData.filename}`);
105
+ console.log(`- Content Type: ${ckbfsData.contentType}`);
106
+ console.log(`- Checksum: ${ckbfsData.checksum}`);
107
+ console.log(`- Backlinks: ${ckbfsData.backLinks?.length || 0}`);
108
+
109
+ // Retrieve complete file content using traditional method
110
+ const outPoint = { txHash: targetTxHash, index: ckbfsCellIndex };
111
+ const content = await getFileContentFromChain(client, outPoint, ckbfsData);
112
+
113
+ // Save file using traditional method
114
+ const savedPath = saveFileFromChain(
115
+ content,
116
+ ckbfsData,
117
+ `./traditional_${ckbfsData.filename}`,
118
+ );
119
+
120
+ console.log(`Traditional method completed. File saved to: ${savedPath}`);
121
+ console.log(`File size: ${content.length} bytes`);
122
+
123
+ return { ckbfsData, tx, content };
124
+ } catch (error) {
125
+ console.error("Traditional retrieval failed:", error);
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Example of decoding file content directly from witnesses (new method)
132
+ */
133
+ async function directWitnessDecodingExample() {
134
+ console.log("\n=== Direct Witness Decoding Method (New) ===");
135
+
136
+ try {
137
+ // Get transaction from blockchain
138
+ const txWithStatus = await client.getTransaction(targetTxHash);
139
+ if (!txWithStatus || !txWithStatus.transaction) {
140
+ throw new Error(`Transaction ${targetTxHash} not found`);
141
+ }
142
+
143
+ const tx = Transaction.from(txWithStatus.transaction);
144
+
145
+ // Get CKBFS data to know which witnesses contain content
146
+ const outputData = tx.outputsData[0];
147
+ if (!outputData) {
148
+ throw new Error("Output data not found");
149
+ }
150
+
151
+ const rawData = outputData.startsWith("0x")
152
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
153
+ : Buffer.from(outputData, "hex");
154
+
155
+ let ckbfsData: CKBFSDataType;
156
+ let version: ProtocolVersionType;
157
+
158
+ try {
159
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
160
+ version = ProtocolVersion.V2;
161
+ } catch (error) {
162
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
163
+ version = ProtocolVersion.V1;
164
+ }
165
+
166
+ console.log(`Using protocol ${version} for witness decoding`);
167
+
168
+ // Get witness indexes from CKBFS data
169
+ const indexes =
170
+ ckbfsData.indexes ||
171
+ (ckbfsData.index !== undefined ? [ckbfsData.index] : []);
172
+ console.log(`Content witness indexes: ${indexes.join(", ")}`);
173
+
174
+ // Example 1: Decode individual witnesses
175
+ console.log("\n--- Example 1: Decode Individual Witnesses ---");
176
+ for (const idx of indexes) {
177
+ if (idx < tx.witnesses.length) {
178
+ const decoded = decodeWitnessContent(tx.witnesses[idx]);
179
+ if (decoded) {
180
+ console.log(
181
+ `Witness ${idx}: ${decoded.content.length} bytes, valid: ${decoded.isValid}`,
182
+ );
183
+ } else {
184
+ console.log(`Witness ${idx}: Not a valid CKBFS witness`);
185
+ }
186
+ }
187
+ }
188
+
189
+ // Example 2: Decode multiple witnesses at once
190
+ console.log("\n--- Example 2: Decode Multiple Witnesses ---");
191
+ const relevantWitnesses = indexes
192
+ .map((idx) => tx.witnesses[idx])
193
+ .filter(Boolean);
194
+ const combinedContent = decodeMultipleWitnessContents(
195
+ relevantWitnesses,
196
+ true,
197
+ );
198
+ console.log(
199
+ `Combined content from ${relevantWitnesses.length} witnesses: ${combinedContent.length} bytes`,
200
+ );
201
+
202
+ // Example 3: Extract file using witness indexes
203
+ console.log("\n--- Example 3: Extract File Using Indexes ---");
204
+ const extractedContent = extractFileFromWitnesses(tx.witnesses, indexes);
205
+ console.log(`Extracted content: ${extractedContent.length} bytes`);
206
+
207
+ // Example 4: High-level decode with metadata
208
+ console.log("\n--- Example 4: High-level Decode with Metadata ---");
209
+ const decodedFile = decodeFileFromWitnessData({
210
+ witnesses: tx.witnesses,
211
+ indexes: indexes,
212
+ filename: ckbfsData.filename,
213
+ contentType: ckbfsData.contentType,
214
+ });
215
+
216
+ console.log(`Decoded file:`);
217
+ console.log(`- Filename: ${decodedFile.filename}`);
218
+ console.log(`- Content Type: ${decodedFile.contentType}`);
219
+ console.log(`- Size: ${decodedFile.size} bytes`);
220
+
221
+ // Example 5: Save file directly from witness data
222
+ console.log("\n--- Example 5: Save File from Witness Data ---");
223
+ const savedPath = saveFileFromWitnessData(
224
+ {
225
+ witnesses: tx.witnesses,
226
+ indexes: indexes,
227
+ filename: ckbfsData.filename,
228
+ contentType: ckbfsData.contentType,
229
+ },
230
+ `./direct_${ckbfsData.filename}`,
231
+ );
232
+
233
+ console.log(
234
+ `Direct witness decoding completed. File saved to: ${savedPath}`,
235
+ );
236
+
237
+ return { decodedFile, extractedContent };
238
+ } catch (error) {
239
+ console.error("Direct witness decoding failed:", error);
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Example of handling backlinks with direct witness decoding
246
+ */
247
+ async function backlinksDecodingExample() {
248
+ console.log("\n=== Backlinks Decoding Example ===");
249
+
250
+ try {
251
+ // Get main transaction
252
+ const txWithStatus = await client.getTransaction(targetTxHash);
253
+ if (!txWithStatus || !txWithStatus.transaction) {
254
+ throw new Error(`Transaction ${targetTxHash} not found`);
255
+ }
256
+
257
+ const tx = Transaction.from(txWithStatus.transaction);
258
+ const outputData = tx.outputsData[0];
259
+ const rawData = outputData.startsWith("0x")
260
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
261
+ : Buffer.from(outputData, "hex");
262
+
263
+ let ckbfsData: CKBFSDataType;
264
+ try {
265
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
266
+ } catch (error) {
267
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
268
+ }
269
+
270
+ console.log(`Found ${ckbfsData.backLinks?.length || 0} backlinks`);
271
+
272
+ if (ckbfsData.backLinks && ckbfsData.backLinks.length > 0) {
273
+ // Process each backlink
274
+ for (let i = 0; i < ckbfsData.backLinks.length; i++) {
275
+ const backlink = ckbfsData.backLinks[i];
276
+ console.log(`\nProcessing backlink ${i + 1}:`);
277
+ console.log(`- Transaction: ${backlink.txHash}`);
278
+ console.log(`- Checksum: ${backlink.checksum}`);
279
+
280
+ try {
281
+ // Get backlink transaction
282
+ const backTxWithStatus = await client.getTransaction(backlink.txHash);
283
+ if (backTxWithStatus && backTxWithStatus.transaction) {
284
+ const backTx = Transaction.from(backTxWithStatus.transaction);
285
+
286
+ // Get backlink indexes
287
+ const backIndexes =
288
+ backlink.indexes ||
289
+ (backlink.index !== undefined ? [backlink.index] : []);
290
+ console.log(`- Witness indexes: ${backIndexes.join(", ")}`);
291
+
292
+ // Decode content from backlink witnesses
293
+ const backlinkContent = extractFileFromWitnesses(
294
+ backTx.witnesses,
295
+ backIndexes,
296
+ );
297
+ console.log(`- Content size: ${backlinkContent.length} bytes`);
298
+
299
+ // Save backlink content
300
+ const backlinkPath = `./backlink_${i + 1}_content.bin`;
301
+ require("fs").writeFileSync(backlinkPath, backlinkContent);
302
+ console.log(`- Saved to: ${backlinkPath}`);
303
+ }
304
+ } catch (error) {
305
+ console.warn(`Failed to process backlink ${i + 1}: ${error}`);
306
+ }
307
+ }
308
+ } else {
309
+ console.log("No backlinks found in this file");
310
+ }
311
+ } catch (error) {
312
+ console.error("Backlinks decoding failed:", error);
313
+ throw error;
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Example of retrieving files using generic identifiers (new method)
319
+ */
320
+ async function genericIdentifierRetrievalExample() {
321
+ console.log("\n=== Generic Identifier Retrieval Example ===");
322
+
323
+ try {
324
+ // Get transaction to extract TypeID from it
325
+ const txWithStatus = await client.getTransaction(targetTxHash);
326
+ if (!txWithStatus || !txWithStatus.transaction) {
327
+ throw new Error(`Transaction ${targetTxHash} not found`);
328
+ }
329
+
330
+ const tx = Transaction.from(txWithStatus.transaction);
331
+
332
+ // Find CKBFS cell (first output with type script)
333
+ const ckbfsCellIndex = 0;
334
+ const output = tx.outputs[ckbfsCellIndex];
335
+ if (!output || !output.type) {
336
+ throw new Error("No CKBFS cell found in the transaction");
337
+ }
338
+
339
+ // Extract TypeID from the type script args
340
+ const typeId = output.type.args;
341
+ console.log(`Extracted TypeID: ${typeId}`);
342
+
343
+ if (!typeId || typeId === "0x") {
344
+ console.log(
345
+ "No TypeID found in this transaction, skipping identifier examples",
346
+ );
347
+ return;
348
+ }
349
+
350
+ // Create different identifier formats for demonstration
351
+ const identifiers = [
352
+ typeId, // Pure TypeID hex string
353
+ `ckbfs://${typeId.slice(2)}`, // CKBFS URI with TypeID
354
+ `ckbfs://${targetTxHash.slice(2)}i${ckbfsCellIndex}`, // CKBFS URI with outPoint
355
+ ];
356
+
357
+ console.log("\nTesting different identifier formats:");
358
+ identifiers.forEach((id, index) => {
359
+ const parsed = parseIdentifier(id);
360
+ console.log(`${index + 1}. ${id}`);
361
+ console.log(` Type: ${parsed.type}`);
362
+ if (parsed.typeId) console.log(` TypeID: ${parsed.typeId}`);
363
+ if (parsed.txHash)
364
+ console.log(` TxHash: ${parsed.txHash}, Index: ${parsed.index}`);
365
+ });
366
+
367
+ // Example 1: Get file content using generic identifier (traditional method with backlinks)
368
+ console.log(
369
+ "\n--- Example 1: Get File Content by Generic Identifier (Traditional) ---",
370
+ );
371
+
372
+ // Test with TypeID format
373
+ const fileData = await getFileContentFromChainByIdentifier(
374
+ client,
375
+ identifiers[0],
376
+ {
377
+ network: "testnet",
378
+ version: ProtocolVersion.V2,
379
+ useTypeID: false,
380
+ },
381
+ );
382
+
383
+ if (fileData) {
384
+ console.log(`Retrieved file: ${fileData.filename}`);
385
+ console.log(`Content type: ${fileData.contentType}`);
386
+ console.log(`Size: ${fileData.size} bytes`);
387
+ console.log(`Checksum: ${fileData.checksum}`);
388
+ console.log(`Backlinks: ${fileData.backLinks.length}`);
389
+ console.log(`Parsed identifier type: ${fileData.parsedId.type}`);
390
+ } else {
391
+ console.log("File not found by identifier");
392
+ return;
393
+ }
394
+
395
+ // Example 2: Test all identifier formats
396
+ console.log("\n--- Example 2: Test All Identifier Formats ---");
397
+ for (let i = 0; i < identifiers.length; i++) {
398
+ const identifier = identifiers[i];
399
+ console.log(`\nTesting identifier ${i + 1}: ${identifier}`);
400
+
401
+ try {
402
+ const testData = await getFileContentFromChainByIdentifier(
403
+ client,
404
+ identifier,
405
+ {
406
+ network: "testnet",
407
+ version: ProtocolVersion.V2,
408
+ useTypeID: false,
409
+ },
410
+ );
411
+
412
+ if (testData) {
413
+ console.log(
414
+ `✓ Success: ${testData.filename} (${testData.size} bytes)`,
415
+ );
416
+ console.log(` Parsed as: ${testData.parsedId.type}`);
417
+ } else {
418
+ console.log(`✗ Failed: File not found`);
419
+ }
420
+ } catch (error) {
421
+ console.log(`✗ Error: ${error}`);
422
+ }
423
+ }
424
+
425
+ // Example 3: Save file using CKBFS URI format
426
+ console.log("\n--- Example 3: Save File using CKBFS URI ---");
427
+ const savedPath = await saveFileFromChainByIdentifier(
428
+ client,
429
+ identifiers[1], // Use CKBFS URI format
430
+ `./identifier_${fileData.filename}`,
431
+ {
432
+ network: "testnet",
433
+ version: ProtocolVersion.V2,
434
+ useTypeID: false,
435
+ },
436
+ );
437
+
438
+ if (savedPath) {
439
+ console.log(`File saved via identifier to: ${savedPath}`);
440
+ }
441
+
442
+ // Example 4: Decode file using outPoint format with direct witness method
443
+ console.log(
444
+ "\n--- Example 4: Decode File using OutPoint Format (Direct Witness) ---",
445
+ );
446
+ const decodedData = await decodeFileFromChainByIdentifier(
447
+ client,
448
+ identifiers[2],
449
+ {
450
+ network: "testnet",
451
+ version: ProtocolVersion.V2,
452
+ useTypeID: false,
453
+ },
454
+ );
455
+
456
+ if (decodedData) {
457
+ console.log(`Decoded file: ${decodedData.filename}`);
458
+ console.log(`Content type: ${decodedData.contentType}`);
459
+ console.log(`Size: ${decodedData.size} bytes`);
460
+ console.log(`Checksum: ${decodedData.checksum}`);
461
+ console.log(`Parsed identifier type: ${decodedData.parsedId.type}`);
462
+
463
+ // Verify content matches
464
+ const contentMatches =
465
+ Buffer.compare(fileData.content, decodedData.content) === 0;
466
+ console.log(
467
+ `Content verification: ${contentMatches ? "PASSED" : "FAILED"}`,
468
+ );
469
+ }
470
+
471
+ console.log("\nGeneric identifier retrieval completed successfully!");
472
+ return { fileData, decodedData, identifiers };
473
+ } catch (error) {
474
+ console.error("Generic identifier retrieval failed:", error);
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Performance comparison between traditional and direct methods
481
+ */
482
+ async function performanceComparisonExample() {
483
+ console.log("\n=== Performance Comparison ===");
484
+
485
+ try {
486
+ // Traditional method timing
487
+ const traditionalStart = Date.now();
488
+ const traditionalResult = await traditionalRetrieveExample();
489
+ const traditionalTime = Date.now() - traditionalStart;
490
+
491
+ // Direct method timing
492
+ const directStart = Date.now();
493
+ const directResult = await directWitnessDecodingExample();
494
+ const directTime = Date.now() - directStart;
495
+
496
+ console.log("\n--- Performance Results ---");
497
+ console.log(`Traditional method: ${traditionalTime}ms`);
498
+ console.log(`Direct witness method: ${directTime}ms`);
499
+ console.log(
500
+ `Performance improvement: ${(((traditionalTime - directTime) / traditionalTime) * 100).toFixed(1)}%`,
501
+ );
502
+
503
+ // Verify content matches
504
+ const contentMatches =
505
+ Buffer.compare(
506
+ traditionalResult.content,
507
+ directResult.extractedContent,
508
+ ) === 0;
509
+ console.log(
510
+ `Content verification: ${contentMatches ? "PASSED" : "FAILED"}`,
511
+ );
512
+ } catch (error) {
513
+ console.error("Performance comparison failed:", error);
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Main function to run all examples
519
+ */
520
+ async function main() {
521
+ console.log("Running CKBFS retrieve and witness decoding examples...");
522
+ console.log("=========================================================");
523
+ console.log(`Target transaction: ${targetTxHash}`);
524
+
525
+ // Validate transaction hash
526
+ if (
527
+ targetTxHash ===
528
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
529
+ ) {
530
+ console.error(
531
+ "Please provide a valid transaction hash by setting the TARGET_TX_HASH environment variable or using --txhash=0x... argument",
532
+ );
533
+ console.log("\nExample usage:");
534
+ console.log(" npm run example:retrieve -- --txhash=0x123456...");
535
+ console.log(" TARGET_TX_HASH=0x123456... npm run example:retrieve");
536
+ process.exit(1);
537
+ }
538
+
539
+ try {
540
+ // Run traditional method
541
+ await traditionalRetrieveExample();
542
+
543
+ // Run new direct witness decoding methods
544
+ await directWitnessDecodingExample();
545
+
546
+ // Handle backlinks if present
547
+ await backlinksDecodingExample();
548
+
549
+ // Run generic identifier-based retrieval examples
550
+ await genericIdentifierRetrievalExample();
551
+
552
+ console.log("\n=== Summary ===");
553
+ console.log("All examples completed successfully!");
554
+ console.log("\nKey advantages of direct witness decoding:");
555
+ console.log("- No need for recursive blockchain queries");
556
+ console.log("- Faster performance for simple cases");
557
+ console.log("- More control over witness processing");
558
+ console.log("- Useful for offline processing with cached data");
559
+ console.log("\nTraditional method advantages:");
560
+ console.log("- Automatic backlink following");
561
+ console.log("- Complete file reconstruction");
562
+ console.log("- Built-in error handling for complex cases");
563
+ console.log("\nGeneric identifier method advantages:");
564
+ console.log("- Flexible input formats (TypeID, CKBFS URIs, outPoint)");
565
+ console.log("- Simple interface - works with any identifier format");
566
+ console.log("- No need to know specific format beforehand");
567
+ console.log("- Automatic format detection and parsing");
568
+ console.log("- Works with both traditional and direct witness decoding");
569
+
570
+ process.exit(0);
571
+ } catch (error) {
572
+ console.error("Examples failed:", error);
573
+ process.exit(1);
574
+ }
575
+ }
576
+
577
+ // Run the example if this file is executed directly
578
+ if (require.main === module) {
579
+ main().catch(console.error);
580
+ }