@ckbfs/api 1.2.3 → 1.2.5

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/src/utils/file.ts CHANGED
@@ -1,5 +1,5 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import fs from "fs";
2
+ import path from "path";
3
3
 
4
4
  /**
5
5
  * Utility functions for file operations
@@ -20,7 +20,7 @@ export function readFile(filePath: string): Buffer {
20
20
  * @returns String containing the file contents
21
21
  */
22
22
  export function readFileAsText(filePath: string): string {
23
- return fs.readFileSync(filePath, 'utf-8');
23
+ return fs.readFileSync(filePath, "utf-8");
24
24
  }
25
25
 
26
26
  /**
@@ -44,7 +44,7 @@ export function writeFile(filePath: string, data: Buffer | string): void {
44
44
  if (!fs.existsSync(dirPath)) {
45
45
  fs.mkdirSync(dirPath, { recursive: true });
46
46
  }
47
-
47
+
48
48
  fs.writeFileSync(filePath, data);
49
49
  }
50
50
 
@@ -55,30 +55,30 @@ export function writeFile(filePath: string, data: Buffer | string): void {
55
55
  */
56
56
  export function getContentType(filePath: string): string {
57
57
  const extension = path.extname(filePath).toLowerCase();
58
-
58
+
59
59
  const mimeTypes: { [key: string]: string } = {
60
- '.txt': 'text/plain',
61
- '.html': 'text/html',
62
- '.htm': 'text/html',
63
- '.css': 'text/css',
64
- '.js': 'application/javascript',
65
- '.json': 'application/json',
66
- '.jpg': 'image/jpeg',
67
- '.jpeg': 'image/jpeg',
68
- '.png': 'image/png',
69
- '.gif': 'image/gif',
70
- '.svg': 'image/svg+xml',
71
- '.pdf': 'application/pdf',
72
- '.mp3': 'audio/mpeg',
73
- '.mp4': 'video/mp4',
74
- '.wav': 'audio/wav',
75
- '.xml': 'application/xml',
76
- '.zip': 'application/zip',
77
- '.md': 'text/markdown',
78
- '.markdown': 'text/markdown',
60
+ ".txt": "text/plain",
61
+ ".html": "text/html",
62
+ ".htm": "text/html",
63
+ ".css": "text/css",
64
+ ".js": "application/javascript",
65
+ ".json": "application/json",
66
+ ".jpg": "image/jpeg",
67
+ ".jpeg": "image/jpeg",
68
+ ".png": "image/png",
69
+ ".gif": "image/gif",
70
+ ".svg": "image/svg+xml",
71
+ ".pdf": "application/pdf",
72
+ ".mp3": "audio/mpeg",
73
+ ".mp4": "video/mp4",
74
+ ".wav": "audio/wav",
75
+ ".xml": "application/xml",
76
+ ".zip": "application/zip",
77
+ ".md": "text/markdown",
78
+ ".markdown": "text/markdown",
79
79
  };
80
-
81
- return mimeTypes[extension] || 'application/octet-stream';
80
+
81
+ return mimeTypes[extension] || "application/octet-stream";
82
82
  }
83
83
 
84
84
  /**
@@ -87,14 +87,17 @@ export function getContentType(filePath: string): string {
87
87
  * @param chunkSize The maximum size of each chunk in bytes
88
88
  * @returns Array of Uint8Array chunks
89
89
  */
90
- export function splitFileIntoChunks(filePath: string, chunkSize: number): Uint8Array[] {
90
+ export function splitFileIntoChunks(
91
+ filePath: string,
92
+ chunkSize: number,
93
+ ): Uint8Array[] {
91
94
  const fileBuffer = fs.readFileSync(filePath);
92
95
  const chunks: Uint8Array[] = [];
93
-
96
+
94
97
  for (let i = 0; i < fileBuffer.length; i += chunkSize) {
95
98
  chunks.push(new Uint8Array(fileBuffer.slice(i, i + chunkSize)));
96
99
  }
97
-
100
+
98
101
  return chunks;
99
102
  }
100
103
 
@@ -103,8 +106,13 @@ export function splitFileIntoChunks(filePath: string, chunkSize: number): Uint8A
103
106
  * @param chunks Array of chunks to combine
104
107
  * @param outputPath The path to write the combined file to
105
108
  */
106
- export function combineChunksToFile(chunks: Uint8Array[], outputPath: string): void {
107
- const combinedBuffer = Buffer.concat(chunks.map(chunk => Buffer.from(chunk)));
109
+ export function combineChunksToFile(
110
+ chunks: Uint8Array[],
111
+ outputPath: string,
112
+ ): void {
113
+ const combinedBuffer = Buffer.concat(
114
+ chunks.map((chunk) => Buffer.from(chunk)),
115
+ );
108
116
  writeFile(outputPath, combinedBuffer);
109
117
  }
110
118
 
@@ -114,17 +122,17 @@ export function combineChunksToFile(chunks: Uint8Array[], outputPath: string): v
114
122
  * @returns Decoded string or placeholder on error
115
123
  */
116
124
  function safelyDecode(buffer: any): string {
117
- if (!buffer) return '[Unknown]';
125
+ if (!buffer) return "[Unknown]";
118
126
  try {
119
127
  if (buffer instanceof Uint8Array) {
120
128
  return new TextDecoder().decode(buffer);
121
- } else if (typeof buffer === 'string') {
129
+ } else if (typeof buffer === "string") {
122
130
  return buffer;
123
131
  } else {
124
132
  return `[Buffer: ${buffer.toString()}]`;
125
133
  }
126
134
  } catch (e) {
127
- return '[Decode Error]';
135
+ return "[Decode Error]";
128
136
  }
129
137
  }
130
138
 
@@ -138,24 +146,26 @@ function safelyDecode(buffer: any): string {
138
146
  export async function getFileContentFromChain(
139
147
  client: any,
140
148
  outPoint: { txHash: string; index: number },
141
- ckbfsData: any
149
+ ckbfsData: any,
142
150
  ): Promise<Uint8Array> {
143
151
  console.log(`Retrieving file: ${safelyDecode(ckbfsData.filename)}`);
144
152
  console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
145
-
153
+
146
154
  // Prepare to collect all content pieces
147
155
  const contentPieces: Uint8Array[] = [];
148
156
  let currentData = ckbfsData;
149
157
  let currentOutPoint = outPoint;
150
-
158
+
151
159
  // Process the current transaction first
152
160
  const tx = await client.getTransaction(currentOutPoint.txHash);
153
161
  if (!tx || !tx.transaction) {
154
162
  throw new Error(`Transaction ${currentOutPoint.txHash} not found`);
155
163
  }
156
-
164
+
157
165
  // Get content from witnesses
158
- const indexes = currentData.indexes || (currentData.index !== undefined ? [currentData.index] : []);
166
+ const indexes =
167
+ currentData.indexes ||
168
+ (currentData.index !== undefined ? [currentData.index] : []);
159
169
  if (indexes.length > 0) {
160
170
  // Get content from each witness index
161
171
  for (const idx of indexes) {
@@ -163,12 +173,12 @@ export async function getFileContentFromChain(
163
173
  console.warn(`Witness index ${idx} out of range`);
164
174
  continue;
165
175
  }
166
-
176
+
167
177
  const witnessHex = tx.transaction.witnesses[idx];
168
- const witness = Buffer.from(witnessHex.slice(2), 'hex'); // Remove 0x prefix
169
-
178
+ const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
179
+
170
180
  // Extract content (skip CKBFS header + version byte)
171
- if (witness.length >= 6 && witness.slice(0, 5).toString() === 'CKBFS') {
181
+ if (witness.length >= 6 && witness.slice(0, 5).toString() === "CKBFS") {
172
182
  const content = witness.slice(6);
173
183
  contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
174
184
  } else {
@@ -176,22 +186,24 @@ export async function getFileContentFromChain(
176
186
  }
177
187
  }
178
188
  }
179
-
189
+
180
190
  // Follow backlinks recursively
181
191
  if (currentData.backLinks && currentData.backLinks.length > 0) {
182
192
  // Process each backlink, from most recent to oldest
183
193
  for (let i = currentData.backLinks.length - 1; i >= 0; i--) {
184
194
  const backlink = currentData.backLinks[i];
185
-
195
+
186
196
  // Get the transaction for this backlink
187
197
  const backTx = await client.getTransaction(backlink.txHash);
188
198
  if (!backTx || !backTx.transaction) {
189
199
  console.warn(`Backlink transaction ${backlink.txHash} not found`);
190
200
  continue;
191
201
  }
192
-
202
+
193
203
  // Get content from backlink witnesses
194
- const backIndexes = backlink.indexes || (backlink.index !== undefined ? [backlink.index] : []);
204
+ const backIndexes =
205
+ backlink.indexes ||
206
+ (backlink.index !== undefined ? [backlink.index] : []);
195
207
  if (backIndexes.length > 0) {
196
208
  // Get content from each witness index
197
209
  for (const idx of backIndexes) {
@@ -199,22 +211,27 @@ export async function getFileContentFromChain(
199
211
  console.warn(`Backlink witness index ${idx} out of range`);
200
212
  continue;
201
213
  }
202
-
214
+
203
215
  const witnessHex = backTx.transaction.witnesses[idx];
204
- const witness = Buffer.from(witnessHex.slice(2), 'hex'); // Remove 0x prefix
205
-
216
+ const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
217
+
206
218
  // Extract content (skip CKBFS header + version byte)
207
- if (witness.length >= 6 && witness.slice(0, 5).toString() === 'CKBFS') {
219
+ if (
220
+ witness.length >= 6 &&
221
+ witness.slice(0, 5).toString() === "CKBFS"
222
+ ) {
208
223
  const content = witness.slice(6);
209
224
  contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
210
225
  } else {
211
- console.warn(`Backlink witness at index ${idx} is not a valid CKBFS witness`);
226
+ console.warn(
227
+ `Backlink witness at index ${idx} is not a valid CKBFS witness`,
228
+ );
212
229
  }
213
230
  }
214
231
  }
215
232
  }
216
233
  }
217
-
234
+
218
235
  // Combine all content pieces
219
236
  return Buffer.concat(contentPieces);
220
237
  }
@@ -229,24 +246,812 @@ export async function getFileContentFromChain(
229
246
  export function saveFileFromChain(
230
247
  content: Uint8Array,
231
248
  ckbfsData: any,
232
- outputPath?: string
249
+ outputPath?: string,
233
250
  ): string {
234
251
  // Get filename from CKBFS data
235
252
  const filename = safelyDecode(ckbfsData.filename);
236
-
253
+
237
254
  // Determine output path
238
255
  const filePath = outputPath || filename;
239
-
256
+
240
257
  // Ensure directory exists
241
258
  const directory = path.dirname(filePath);
242
259
  if (!fs.existsSync(directory)) {
243
260
  fs.mkdirSync(directory, { recursive: true });
244
261
  }
245
-
262
+
246
263
  // Write file
247
264
  fs.writeFileSync(filePath, content);
248
265
  console.log(`File saved to: ${filePath}`);
249
266
  console.log(`Size: ${content.length} bytes`);
250
-
267
+
251
268
  return filePath;
252
- }
269
+ }
270
+
271
+ /**
272
+ * Decodes content from a single CKBFS witness
273
+ * @param witnessHex The witness data in hex format (with or without 0x prefix)
274
+ * @returns Object containing the decoded content and metadata, or null if not a valid CKBFS witness
275
+ */
276
+ export function decodeWitnessContent(
277
+ witnessHex: string,
278
+ ): { content: Uint8Array; isValid: boolean } | null {
279
+ try {
280
+ // Remove 0x prefix if present
281
+ const hexData = witnessHex.startsWith("0x")
282
+ ? witnessHex.slice(2)
283
+ : witnessHex;
284
+ const witness = Buffer.from(hexData, "hex");
285
+
286
+ // Check if it's a valid CKBFS witness
287
+ if (witness.length < 6) {
288
+ return null;
289
+ }
290
+
291
+ // Check CKBFS header
292
+ const header = witness.slice(0, 5).toString();
293
+ if (header !== "CKBFS") {
294
+ return null;
295
+ }
296
+
297
+ // Extract content (skip CKBFS header + version byte)
298
+ const content = witness.slice(6);
299
+
300
+ return {
301
+ content,
302
+ isValid: true,
303
+ };
304
+ } catch (error) {
305
+ console.warn("Error decoding witness content:", error);
306
+ return null;
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Decodes and combines content from multiple CKBFS witnesses
312
+ * @param witnessHexArray Array of witness data in hex format
313
+ * @param preserveOrder Whether to preserve the order of witnesses (default: true)
314
+ * @returns Combined content from all valid CKBFS witnesses
315
+ */
316
+ export function decodeMultipleWitnessContents(
317
+ witnessHexArray: string[],
318
+ preserveOrder: boolean = true,
319
+ ): Uint8Array {
320
+ const contentPieces: Uint8Array[] = [];
321
+
322
+ for (let i = 0; i < witnessHexArray.length; i++) {
323
+ const witnessHex = witnessHexArray[i];
324
+ const decoded = decodeWitnessContent(witnessHex);
325
+
326
+ if (decoded && decoded.isValid) {
327
+ if (preserveOrder) {
328
+ contentPieces.push(decoded.content);
329
+ } else {
330
+ contentPieces.unshift(decoded.content);
331
+ }
332
+ } else {
333
+ console.warn(`Witness at index ${i} is not a valid CKBFS witness`);
334
+ }
335
+ }
336
+
337
+ return Buffer.concat(contentPieces);
338
+ }
339
+
340
+ /**
341
+ * Extracts complete file content from witnesses using specified indexes
342
+ * @param witnesses Array of all witnesses from a transaction
343
+ * @param indexes Array of witness indexes that contain CKBFS content
344
+ * @returns Combined content from the specified witness indexes
345
+ */
346
+ export function extractFileFromWitnesses(
347
+ witnesses: string[],
348
+ indexes: number[],
349
+ ): Uint8Array {
350
+ const relevantWitnesses: string[] = [];
351
+
352
+ for (const idx of indexes) {
353
+ if (idx >= witnesses.length) {
354
+ console.warn(
355
+ `Witness index ${idx} out of range (total witnesses: ${witnesses.length})`,
356
+ );
357
+ continue;
358
+ }
359
+ relevantWitnesses.push(witnesses[idx]);
360
+ }
361
+
362
+ return decodeMultipleWitnessContents(relevantWitnesses, true);
363
+ }
364
+
365
+ /**
366
+ * Decodes file content directly from witness data without blockchain queries
367
+ * @param witnessData Object containing witness information
368
+ * @returns Object containing the decoded file content and metadata
369
+ */
370
+ export function decodeFileFromWitnessData(witnessData: {
371
+ witnesses: string[];
372
+ indexes: number[] | number;
373
+ filename?: string;
374
+ contentType?: string;
375
+ }): {
376
+ content: Uint8Array;
377
+ filename?: string;
378
+ contentType?: string;
379
+ size: number;
380
+ } {
381
+ const { witnesses, indexes, filename, contentType } = witnessData;
382
+
383
+ // Normalize indexes to array
384
+ const indexArray = Array.isArray(indexes) ? indexes : [indexes];
385
+
386
+ // Extract content from witnesses
387
+ const content = extractFileFromWitnesses(witnesses, indexArray);
388
+
389
+ return {
390
+ content,
391
+ filename,
392
+ contentType,
393
+ size: content.length,
394
+ };
395
+ }
396
+
397
+ /**
398
+ * Saves decoded file content directly from witness data
399
+ * @param witnessData Object containing witness information
400
+ * @param outputPath Optional path to save the file
401
+ * @returns The path where the file was saved
402
+ */
403
+ export function saveFileFromWitnessData(
404
+ witnessData: {
405
+ witnesses: string[];
406
+ indexes: number[] | number;
407
+ filename?: string;
408
+ contentType?: string;
409
+ },
410
+ outputPath?: string,
411
+ ): string {
412
+ const decoded = decodeFileFromWitnessData(witnessData);
413
+
414
+ // Determine output path
415
+ const filename = decoded.filename || "decoded_file";
416
+ const filePath = outputPath || filename;
417
+
418
+ // Ensure directory exists
419
+ const directory = path.dirname(filePath);
420
+ if (!fs.existsSync(directory)) {
421
+ fs.mkdirSync(directory, { recursive: true });
422
+ }
423
+
424
+ // Write file
425
+ fs.writeFileSync(filePath, decoded.content);
426
+ console.log(`File saved to: ${filePath}`);
427
+ console.log(`Size: ${decoded.size} bytes`);
428
+ console.log(`Content type: ${decoded.contentType || "unknown"}`);
429
+
430
+ return filePath;
431
+ }
432
+
433
+ /**
434
+ * Identifier types for CKBFS cells
435
+ */
436
+ export enum IdentifierType {
437
+ TypeID = "typeId",
438
+ OutPoint = "outPoint",
439
+ Unknown = "unknown",
440
+ }
441
+
442
+ /**
443
+ * Parsed identifier information
444
+ */
445
+ export interface ParsedIdentifier {
446
+ type: IdentifierType;
447
+ typeId?: string;
448
+ txHash?: string;
449
+ index?: number;
450
+ original: string;
451
+ }
452
+
453
+ /**
454
+ * Detects and parses different CKBFS identifier formats
455
+ * @param identifier The identifier string to parse
456
+ * @returns Parsed identifier information
457
+ */
458
+ export function parseIdentifier(identifier: string): ParsedIdentifier {
459
+ const trimmed = identifier.trim();
460
+
461
+ // Type 1: Pure TypeID hex string (with or without 0x prefix)
462
+ if (trimmed.match(/^(0x)?[a-fA-F0-9]{64}$/)) {
463
+ const typeId = trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
464
+ return {
465
+ type: IdentifierType.TypeID,
466
+ typeId,
467
+ original: identifier,
468
+ };
469
+ }
470
+
471
+ // Type 2: CKBFS URI with TypeID
472
+ if (trimmed.startsWith("ckbfs://")) {
473
+ const content = trimmed.slice(8); // Remove "ckbfs://"
474
+
475
+ // Check if it's TypeID format (64 hex characters)
476
+ if (content.match(/^[a-fA-F0-9]{64}$/)) {
477
+ return {
478
+ type: IdentifierType.TypeID,
479
+ typeId: `0x${content}`,
480
+ original: identifier,
481
+ };
482
+ }
483
+
484
+ // Type 3: CKBFS URI with transaction hash and index (txhash + 'i' + index)
485
+ const outPointMatch = content.match(/^([a-fA-F0-9]{64})i(\d+)$/);
486
+ if (outPointMatch) {
487
+ const [, txHash, indexStr] = outPointMatch;
488
+ return {
489
+ type: IdentifierType.OutPoint,
490
+ txHash: `0x${txHash}`,
491
+ index: parseInt(indexStr, 10),
492
+ original: identifier,
493
+ };
494
+ }
495
+ }
496
+
497
+ // Unknown format
498
+ return {
499
+ type: IdentifierType.Unknown,
500
+ original: identifier,
501
+ };
502
+ }
503
+
504
+ /**
505
+ * Resolves a CKBFS cell using any supported identifier format
506
+ * @param client The CKB client to use for blockchain queries
507
+ * @param identifier The identifier (TypeID, CKBFS URI, or outPoint URI)
508
+ * @param options Optional configuration for network, version, and useTypeID
509
+ * @returns Promise resolving to the found cell and transaction info, or null if not found
510
+ */
511
+ async function resolveCKBFSCell(
512
+ client: any,
513
+ identifier: string,
514
+ options: {
515
+ network?: "mainnet" | "testnet";
516
+ version?: string;
517
+ useTypeID?: boolean;
518
+ } = {},
519
+ ): Promise<{
520
+ cell: any;
521
+ transaction: any;
522
+ outPoint: { txHash: string; index: number };
523
+ parsedId: ParsedIdentifier;
524
+ } | null> {
525
+ const {
526
+ network = "testnet",
527
+ version = "20241025.db973a8e8032",
528
+ useTypeID = false,
529
+ } = options;
530
+
531
+ const parsedId = parseIdentifier(identifier);
532
+
533
+ try {
534
+ if (parsedId.type === IdentifierType.TypeID && parsedId.typeId) {
535
+ // Use existing TypeID resolution logic
536
+ const cellInfo = await findCKBFSCellByTypeId(
537
+ client,
538
+ parsedId.typeId,
539
+ network,
540
+ version,
541
+ useTypeID,
542
+ );
543
+
544
+ if (cellInfo) {
545
+ return {
546
+ ...cellInfo,
547
+ parsedId,
548
+ };
549
+ }
550
+ } else if (
551
+ parsedId.type === IdentifierType.OutPoint &&
552
+ parsedId.txHash &&
553
+ parsedId.index !== undefined
554
+ ) {
555
+ // Resolve using transaction hash and index
556
+ const txWithStatus = await client.getTransaction(parsedId.txHash);
557
+
558
+ if (!txWithStatus || !txWithStatus.transaction) {
559
+ console.warn(`Transaction ${parsedId.txHash} not found`);
560
+ return null;
561
+ }
562
+
563
+ // Import Transaction class dynamically
564
+ const { Transaction } = await import("@ckb-ccc/core");
565
+ const tx = Transaction.from(txWithStatus.transaction);
566
+
567
+ // Check if the index is valid
568
+ if (parsedId.index >= tx.outputs.length) {
569
+ console.warn(
570
+ `Output index ${parsedId.index} out of range for transaction ${parsedId.txHash}`,
571
+ );
572
+ return null;
573
+ }
574
+
575
+ const output = tx.outputs[parsedId.index];
576
+
577
+ // Verify it's a CKBFS cell by checking if it has a type script
578
+ if (!output.type) {
579
+ console.warn(
580
+ `Output at index ${parsedId.index} is not a CKBFS cell (no type script)`,
581
+ );
582
+ return null;
583
+ }
584
+
585
+ // Create a mock cell object similar to what findSingletonCellByType returns
586
+ const cell = {
587
+ outPoint: {
588
+ txHash: parsedId.txHash,
589
+ index: parsedId.index,
590
+ },
591
+ output,
592
+ };
593
+
594
+ return {
595
+ cell,
596
+ transaction: txWithStatus.transaction,
597
+ outPoint: {
598
+ txHash: parsedId.txHash,
599
+ index: parsedId.index,
600
+ },
601
+ parsedId,
602
+ };
603
+ }
604
+
605
+ console.warn(
606
+ `Unable to resolve identifier: ${identifier} (type: ${parsedId.type})`,
607
+ );
608
+ return null;
609
+ } catch (error) {
610
+ console.error(
611
+ `Error resolving CKBFS cell for identifier ${identifier}:`,
612
+ error,
613
+ );
614
+ return null;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Finds a CKBFS cell by TypeID
620
+ * @param client The CKB client to use for blockchain queries
621
+ * @param typeId The TypeID (args) of the CKBFS cell to find
622
+ * @param network The network type (mainnet or testnet)
623
+ * @param version The protocol version to use
624
+ * @param useTypeID Whether to use type ID instead of code hash for script matching
625
+ * @returns Promise resolving to the found cell and transaction info, or null if not found
626
+ */
627
+ async function findCKBFSCellByTypeId(
628
+ client: any,
629
+ typeId: string,
630
+ network: string = "testnet",
631
+ version: string = "20241025.db973a8e8032",
632
+ useTypeID: boolean = false,
633
+ ): Promise<{
634
+ cell: any;
635
+ transaction: any;
636
+ outPoint: { txHash: string; index: number };
637
+ } | null> {
638
+ try {
639
+ // Import constants dynamically to avoid circular dependencies
640
+ const { getCKBFSScriptConfig, NetworkType, ProtocolVersion } = await import(
641
+ "./constants"
642
+ );
643
+
644
+ // Get CKBFS script config
645
+ const networkType =
646
+ network === "mainnet" ? NetworkType.Mainnet : NetworkType.Testnet;
647
+ const protocolVersion =
648
+ version === "20240906.ce6724722cf6"
649
+ ? ProtocolVersion.V1
650
+ : ProtocolVersion.V2;
651
+ const config = getCKBFSScriptConfig(
652
+ networkType,
653
+ protocolVersion,
654
+ useTypeID,
655
+ );
656
+
657
+ // Create the script to search for
658
+ const script = {
659
+ codeHash: config.codeHash,
660
+ hashType: config.hashType,
661
+ args: typeId.startsWith("0x") ? typeId : `0x${typeId}`,
662
+ };
663
+
664
+ // Find the cell by type script
665
+ const cell = await client.findSingletonCellByType(script, true);
666
+
667
+ if (!cell) {
668
+ return null;
669
+ }
670
+
671
+ // Get the transaction that contains this cell
672
+ const txHash = cell.outPoint.txHash;
673
+ const txWithStatus = await client.getTransaction(txHash);
674
+
675
+ if (!txWithStatus || !txWithStatus.transaction) {
676
+ throw new Error(`Transaction ${txHash} not found`);
677
+ }
678
+
679
+ return {
680
+ cell,
681
+ transaction: txWithStatus.transaction,
682
+ outPoint: {
683
+ txHash: cell.outPoint.txHash,
684
+ index: cell.outPoint.index,
685
+ },
686
+ };
687
+ } catch (error) {
688
+ console.warn(`Error finding CKBFS cell by TypeID ${typeId}:`, error);
689
+ return null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Retrieves complete file content from the blockchain using any supported identifier
695
+ * @param client The CKB client to use for blockchain queries
696
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
697
+ * @param options Optional configuration for network, version, and useTypeID
698
+ * @returns Promise resolving to the complete file content and metadata
699
+ */
700
+ export async function getFileContentFromChainByIdentifier(
701
+ client: any,
702
+ identifier: string,
703
+ options: {
704
+ network?: "mainnet" | "testnet";
705
+ version?: string;
706
+ useTypeID?: boolean;
707
+ } = {},
708
+ ): Promise<{
709
+ content: Uint8Array;
710
+ filename: string;
711
+ contentType: string;
712
+ checksum: number;
713
+ size: number;
714
+ backLinks: any[];
715
+ parsedId: ParsedIdentifier;
716
+ } | null> {
717
+ const {
718
+ network = "testnet",
719
+ version = "20241025.db973a8e8032",
720
+ useTypeID = false,
721
+ } = options;
722
+
723
+ try {
724
+ // Resolve the CKBFS cell using any supported identifier format
725
+ const cellInfo = await resolveCKBFSCell(client, identifier, {
726
+ network,
727
+ version,
728
+ useTypeID,
729
+ });
730
+
731
+ if (!cellInfo) {
732
+ console.warn(`CKBFS cell with identifier ${identifier} not found`);
733
+ return null;
734
+ }
735
+
736
+ const { cell, transaction, outPoint, parsedId } = cellInfo;
737
+
738
+ // Import Transaction class dynamically
739
+ const { Transaction } = await import("@ckb-ccc/core");
740
+ const tx = Transaction.from(transaction);
741
+
742
+ // Get output data from the cell
743
+ const outputIndex = outPoint.index;
744
+ const outputData = tx.outputsData[outputIndex];
745
+
746
+ if (!outputData) {
747
+ throw new Error(`Output data not found for cell at index ${outputIndex}`);
748
+ }
749
+
750
+ // Import required modules dynamically
751
+ const { ccc } = await import("@ckb-ccc/core");
752
+ const { CKBFSData } = await import("./molecule");
753
+ const { ProtocolVersion } = await import("./constants");
754
+
755
+ // Parse the output data
756
+ const rawData = outputData.startsWith("0x")
757
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
758
+ : Buffer.from(outputData, "hex");
759
+
760
+ // Try to unpack CKBFS data with both protocol versions
761
+ let ckbfsData: any;
762
+ let protocolVersion = version;
763
+
764
+ try {
765
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
766
+ } catch (error) {
767
+ try {
768
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
769
+ protocolVersion = "20240906.ce6724722cf6";
770
+ } catch (v1Error) {
771
+ throw new Error(
772
+ `Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
773
+ );
774
+ }
775
+ }
776
+
777
+ console.log(`Found CKBFS file: ${ckbfsData.filename}`);
778
+ console.log(`Content type: ${ckbfsData.contentType}`);
779
+ console.log(`Protocol version: ${protocolVersion}`);
780
+
781
+ // Use existing function to get complete file content
782
+ const content = await getFileContentFromChain(client, outPoint, ckbfsData);
783
+
784
+ return {
785
+ content,
786
+ filename: ckbfsData.filename,
787
+ contentType: ckbfsData.contentType,
788
+ checksum: ckbfsData.checksum,
789
+ size: content.length,
790
+ backLinks: ckbfsData.backLinks || [],
791
+ parsedId,
792
+ };
793
+ } catch (error) {
794
+ console.error(`Error retrieving file by identifier ${identifier}:`, error);
795
+ throw error;
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Retrieves complete file content from the blockchain using TypeID (legacy function)
801
+ * @param client The CKB client to use for blockchain queries
802
+ * @param typeId The TypeID (args) of the CKBFS cell
803
+ * @param options Optional configuration for network, version, and useTypeID
804
+ * @returns Promise resolving to the complete file content and metadata
805
+ */
806
+ export async function getFileContentFromChainByTypeId(
807
+ client: any,
808
+ typeId: string,
809
+ options: {
810
+ network?: "mainnet" | "testnet";
811
+ version?: string;
812
+ useTypeID?: boolean;
813
+ } = {},
814
+ ): Promise<{
815
+ content: Uint8Array;
816
+ filename: string;
817
+ contentType: string;
818
+ checksum: number;
819
+ size: number;
820
+ backLinks: any[];
821
+ } | null> {
822
+ const result = await getFileContentFromChainByIdentifier(
823
+ client,
824
+ typeId,
825
+ options,
826
+ );
827
+ if (result) {
828
+ const { parsedId, ...fileData } = result;
829
+ return fileData;
830
+ }
831
+ return null;
832
+ }
833
+
834
+ /**
835
+ * Saves file content retrieved from blockchain by identifier to disk
836
+ * @param client The CKB client to use for blockchain queries
837
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
838
+ * @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
839
+ * @param options Optional configuration for network, version, and useTypeID
840
+ * @returns Promise resolving to the path where the file was saved, or null if file not found
841
+ */
842
+ export async function saveFileFromChainByIdentifier(
843
+ client: any,
844
+ identifier: string,
845
+ outputPath?: string,
846
+ options: {
847
+ network?: "mainnet" | "testnet";
848
+ version?: string;
849
+ useTypeID?: boolean;
850
+ } = {},
851
+ ): Promise<string | null> {
852
+ try {
853
+ // Get file content by identifier
854
+ const fileData = await getFileContentFromChainByIdentifier(
855
+ client,
856
+ identifier,
857
+ options,
858
+ );
859
+
860
+ if (!fileData) {
861
+ console.warn(`File with identifier ${identifier} not found`);
862
+ return null;
863
+ }
864
+
865
+ // Determine output path
866
+ const filePath = outputPath || fileData.filename;
867
+
868
+ // Ensure directory exists
869
+ const directory = path.dirname(filePath);
870
+ if (!fs.existsSync(directory)) {
871
+ fs.mkdirSync(directory, { recursive: true });
872
+ }
873
+
874
+ // Write file
875
+ fs.writeFileSync(filePath, fileData.content);
876
+ console.log(`File saved to: ${filePath}`);
877
+ console.log(`Size: ${fileData.size} bytes`);
878
+ console.log(`Content type: ${fileData.contentType}`);
879
+ console.log(`Checksum: ${fileData.checksum}`);
880
+
881
+ return filePath;
882
+ } catch (error) {
883
+ console.error(`Error saving file by identifier ${identifier}:`, error);
884
+ throw error;
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Saves file content retrieved from blockchain by TypeID to disk (legacy function)
890
+ * @param client The CKB client to use for blockchain queries
891
+ * @param typeId The TypeID (args) of the CKBFS cell
892
+ * @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
893
+ * @param options Optional configuration for network, version, and useTypeID
894
+ * @returns Promise resolving to the path where the file was saved, or null if file not found
895
+ */
896
+ export async function saveFileFromChainByTypeId(
897
+ client: any,
898
+ typeId: string,
899
+ outputPath?: string,
900
+ options: {
901
+ network?: "mainnet" | "testnet";
902
+ version?: string;
903
+ useTypeID?: boolean;
904
+ } = {},
905
+ ): Promise<string | null> {
906
+ return await saveFileFromChainByIdentifier(
907
+ client,
908
+ typeId,
909
+ outputPath,
910
+ options,
911
+ );
912
+ }
913
+
914
+ /**
915
+ * Decodes file content directly from identifier using witness decoding (new method)
916
+ * @param client The CKB client to use for blockchain queries
917
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
918
+ * @param options Optional configuration for network, version, and useTypeID
919
+ * @returns Promise resolving to the decoded file content and metadata, or null if not found
920
+ */
921
+ export async function decodeFileFromChainByIdentifier(
922
+ client: any,
923
+ identifier: string,
924
+ options: {
925
+ network?: "mainnet" | "testnet";
926
+ version?: string;
927
+ useTypeID?: boolean;
928
+ } = {},
929
+ ): Promise<{
930
+ content: Uint8Array;
931
+ filename: string;
932
+ contentType: string;
933
+ checksum: number;
934
+ size: number;
935
+ backLinks: any[];
936
+ parsedId: ParsedIdentifier;
937
+ } | null> {
938
+ const {
939
+ network = "testnet",
940
+ version = "20241025.db973a8e8032",
941
+ useTypeID = false,
942
+ } = options;
943
+
944
+ try {
945
+ // Resolve the CKBFS cell using any supported identifier format
946
+ const cellInfo = await resolveCKBFSCell(client, identifier, {
947
+ network,
948
+ version,
949
+ useTypeID,
950
+ });
951
+
952
+ if (!cellInfo) {
953
+ console.warn(`CKBFS cell with identifier ${identifier} not found`);
954
+ return null;
955
+ }
956
+
957
+ const { cell, transaction, outPoint, parsedId } = cellInfo;
958
+
959
+ // Import required modules dynamically
960
+ const { Transaction, ccc } = await import("@ckb-ccc/core");
961
+ const { CKBFSData } = await import("./molecule");
962
+ const { ProtocolVersion } = await import("./constants");
963
+
964
+ const tx = Transaction.from(transaction);
965
+
966
+ // Get output data from the cell
967
+ const outputIndex = outPoint.index;
968
+ const outputData = tx.outputsData[outputIndex];
969
+
970
+ if (!outputData) {
971
+ throw new Error(`Output data not found for cell at index ${outputIndex}`);
972
+ }
973
+
974
+ // Parse the output data
975
+ const rawData = outputData.startsWith("0x")
976
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
977
+ : Buffer.from(outputData, "hex");
978
+
979
+ // Try to unpack CKBFS data with both protocol versions
980
+ let ckbfsData: any;
981
+ let protocolVersion = version;
982
+
983
+ try {
984
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
985
+ } catch (error) {
986
+ try {
987
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
988
+ protocolVersion = "20240906.ce6724722cf6";
989
+ } catch (v1Error) {
990
+ throw new Error(
991
+ `Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
992
+ );
993
+ }
994
+ }
995
+
996
+ console.log(`Found CKBFS file: ${ckbfsData.filename}`);
997
+ console.log(`Content type: ${ckbfsData.contentType}`);
998
+ console.log(`Using direct witness decoding method`);
999
+
1000
+ // Get witness indexes from CKBFS data
1001
+ const indexes =
1002
+ ckbfsData.indexes ||
1003
+ (ckbfsData.index !== undefined ? [ckbfsData.index] : []);
1004
+
1005
+ // Use direct witness decoding method
1006
+ const content = decodeFileFromWitnessData({
1007
+ witnesses: tx.witnesses,
1008
+ indexes: indexes,
1009
+ filename: ckbfsData.filename,
1010
+ contentType: ckbfsData.contentType,
1011
+ });
1012
+
1013
+ return {
1014
+ content: content.content,
1015
+ filename: ckbfsData.filename,
1016
+ contentType: ckbfsData.contentType,
1017
+ checksum: ckbfsData.checksum,
1018
+ size: content.size,
1019
+ backLinks: ckbfsData.backLinks || [],
1020
+ parsedId,
1021
+ };
1022
+ } catch (error) {
1023
+ console.error(`Error decoding file by identifier ${identifier}:`, error);
1024
+ throw error;
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Decodes file content directly from TypeID using witness decoding (legacy function)
1030
+ * @param client The CKB client to use for blockchain queries
1031
+ * @param typeId The TypeID (args) of the CKBFS cell
1032
+ * @param options Optional configuration for network, version, and useTypeID
1033
+ * @returns Promise resolving to the decoded file content and metadata, or null if not found
1034
+ */
1035
+ export async function decodeFileFromChainByTypeId(
1036
+ client: any,
1037
+ typeId: string,
1038
+ options: {
1039
+ network?: "mainnet" | "testnet";
1040
+ version?: string;
1041
+ useTypeID?: boolean;
1042
+ } = {},
1043
+ ): Promise<{
1044
+ content: Uint8Array;
1045
+ filename: string;
1046
+ contentType: string;
1047
+ checksum: number;
1048
+ size: number;
1049
+ backLinks: any[];
1050
+ } | null> {
1051
+ const result = await decodeFileFromChainByIdentifier(client, typeId, options);
1052
+ if (result) {
1053
+ const { parsedId, ...fileData } = result;
1054
+ return fileData;
1055
+ }
1056
+ return null;
1057
+ }