@ckbfs/api 1.5.1 → 2.0.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.
Files changed (45) hide show
  1. package/README.md +31 -6
  2. package/RFC.v3.md +210 -0
  3. package/dist/index.d.ts +72 -7
  4. package/dist/index.js +437 -75
  5. package/dist/utils/checksum.d.ts +16 -0
  6. package/dist/utils/checksum.js +74 -8
  7. package/dist/utils/constants.d.ts +2 -1
  8. package/dist/utils/constants.js +12 -2
  9. package/dist/utils/file.d.ts +44 -0
  10. package/dist/utils/file.js +303 -30
  11. package/dist/utils/molecule.d.ts +13 -1
  12. package/dist/utils/molecule.js +32 -5
  13. package/dist/utils/transaction-backup.d.ts +117 -0
  14. package/dist/utils/transaction-backup.js +624 -0
  15. package/dist/utils/transaction.d.ts +7 -115
  16. package/dist/utils/transaction.js +45 -622
  17. package/dist/utils/transactions/index.d.ts +8 -0
  18. package/dist/utils/transactions/index.js +31 -0
  19. package/dist/utils/transactions/shared.d.ts +57 -0
  20. package/dist/utils/transactions/shared.js +17 -0
  21. package/dist/utils/transactions/v1v2.d.ts +80 -0
  22. package/dist/utils/transactions/v1v2.js +592 -0
  23. package/dist/utils/transactions/v3.d.ts +124 -0
  24. package/dist/utils/transactions/v3.js +369 -0
  25. package/dist/utils/witness.d.ts +45 -0
  26. package/dist/utils/witness.js +145 -3
  27. package/examples/append-v3.ts +310 -0
  28. package/examples/chunked-publish.ts +307 -0
  29. package/examples/publish-v3.ts +152 -0
  30. package/examples/publish.ts +4 -4
  31. package/examples/retrieve-v3.ts +222 -0
  32. package/package.json +6 -2
  33. package/small-example.txt +1 -0
  34. package/src/index.ts +568 -87
  35. package/src/utils/checksum.ts +90 -9
  36. package/src/utils/constants.ts +19 -2
  37. package/src/utils/file.ts +386 -35
  38. package/src/utils/molecule.ts +43 -6
  39. package/src/utils/transaction-backup.ts +849 -0
  40. package/src/utils/transaction.ts +39 -848
  41. package/src/utils/transactions/index.ts +16 -0
  42. package/src/utils/transactions/shared.ts +64 -0
  43. package/src/utils/transactions/v1v2.ts +791 -0
  44. package/src/utils/transactions/v3.ts +564 -0
  45. package/src/utils/witness.ts +193 -0
@@ -4,6 +4,8 @@ exports.calculateChecksum = calculateChecksum;
4
4
  exports.updateChecksum = updateChecksum;
5
5
  exports.verifyChecksum = verifyChecksum;
6
6
  exports.verifyWitnessChecksum = verifyWitnessChecksum;
7
+ exports.verifyV3WitnessChecksum = verifyV3WitnessChecksum;
8
+ exports.verifyV3WitnessChain = verifyV3WitnessChain;
7
9
  const hash_wasm_1 = require("hash-wasm");
8
10
  /**
9
11
  * Utility functions for Adler32 checksum generation and verification
@@ -41,14 +43,9 @@ async function updateChecksum(previousChecksum, newData) {
41
43
  adlerA = (adlerA + newData[i]) % MOD_ADLER;
42
44
  adlerB = (adlerB + adlerA) % MOD_ADLER;
43
45
  }
44
- // Combine a and b to get the final checksum
45
- // Use a Uint32Array to ensure we get a proper unsigned 32-bit integer
46
- const buffer = new ArrayBuffer(4);
47
- const view = new DataView(buffer);
48
- view.setUint16(0, adlerA, true); // Set lower 16 bits (little endian)
49
- view.setUint16(2, adlerB, true); // Set upper 16 bits (little endian)
50
- // Read as an unsigned 32-bit integer
51
- const updatedChecksum = view.getUint32(0, true);
46
+ // Combine a and b to get the final checksum using standard Adler32 format
47
+ // In Adler32, checksum = (b << 16) | a
48
+ const updatedChecksum = ((adlerB << 16) | adlerA) >>> 0; // Use unsigned right shift to ensure uint32
52
49
  console.log(`Updated checksum from ${previousChecksum} to ${updatedChecksum} for appended content`);
53
50
  return updatedChecksum;
54
51
  }
@@ -81,3 +78,72 @@ async function verifyWitnessChecksum(witness, expectedChecksum, backlinks = [])
81
78
  // Otherwise, calculate checksum from scratch
82
79
  return verifyChecksum(contentBytes, expectedChecksum);
83
80
  }
81
+ /**
82
+ * Verifies the checksum of a CKBFS v3 witness
83
+ * @param witness The v3 witness bytes
84
+ * @param expectedChecksum The expected checksum
85
+ * @param previousChecksum Optional previous checksum for chained verification
86
+ * @returns Promise resolving to a boolean indicating whether the checksum is valid
87
+ */
88
+ async function verifyV3WitnessChecksum(witness, expectedChecksum, previousChecksum) {
89
+ // Check if this is a head witness (contains CKBFS header)
90
+ const isHeadWitness = witness.length >= 50 &&
91
+ new TextDecoder().decode(witness.slice(0, 5)) === 'CKBFS' &&
92
+ witness[5] === 0x03;
93
+ let contentBytes;
94
+ let extractedPreviousChecksum = previousChecksum;
95
+ if (isHeadWitness) {
96
+ // Head witness: extract content after backlink structure
97
+ // Format: CKBFS(5) + version(1) + prevTxHash(32) + prevWitnessIndex(4) + prevChecksum(4) + nextIndex(4) + content
98
+ contentBytes = witness.slice(50);
99
+ // Extract previous checksum from head witness if not provided externally
100
+ if (previousChecksum === undefined) {
101
+ const prevChecksumBytes = witness.slice(42, 46);
102
+ extractedPreviousChecksum = new DataView(prevChecksumBytes.buffer).getUint32(0, true); // little-endian
103
+ }
104
+ }
105
+ else {
106
+ // Continuation witness: extract content after next index
107
+ // Format: nextIndex(4) + content
108
+ contentBytes = witness.slice(4);
109
+ }
110
+ // If previous checksum is available (either provided or extracted), use it for rolling calculation
111
+ if (extractedPreviousChecksum !== undefined && extractedPreviousChecksum !== 0) {
112
+ const updatedChecksum = await updateChecksum(extractedPreviousChecksum, contentBytes);
113
+ return updatedChecksum === expectedChecksum;
114
+ }
115
+ // Otherwise, calculate checksum from scratch
116
+ return verifyChecksum(contentBytes, expectedChecksum);
117
+ }
118
+ /**
119
+ * Verifies the checksum chain for multiple v3 witnesses
120
+ * @param witnesses Array of v3 witness bytes
121
+ * @param finalExpectedChecksum The expected final checksum
122
+ * @param initialChecksum Optional initial checksum (for append operations)
123
+ * @returns Promise resolving to a boolean indicating whether the checksum chain is valid
124
+ */
125
+ async function verifyV3WitnessChain(witnesses, finalExpectedChecksum, initialChecksum = 1 // Adler32 starts with 1
126
+ ) {
127
+ if (witnesses.length === 0) {
128
+ return finalExpectedChecksum === initialChecksum;
129
+ }
130
+ let currentChecksum = initialChecksum;
131
+ // Process each witness in the chain
132
+ for (const witness of witnesses) {
133
+ const isHeadWitness = witness.length >= 50 &&
134
+ new TextDecoder().decode(witness.slice(0, 5)) === 'CKBFS' &&
135
+ witness[5] === 0x03;
136
+ let contentBytes;
137
+ if (isHeadWitness) {
138
+ // Head witness: extract content after backlink structure
139
+ contentBytes = witness.slice(50);
140
+ }
141
+ else {
142
+ // Continuation witness: extract content after next index
143
+ contentBytes = witness.slice(4);
144
+ }
145
+ // Update checksum with this witness's content
146
+ currentChecksum = await updateChecksum(currentChecksum, contentBytes);
147
+ }
148
+ return currentChecksum === finalExpectedChecksum;
149
+ }
@@ -8,6 +8,7 @@ export declare enum NetworkType {
8
8
  export declare const ProtocolVersion: {
9
9
  readonly V1: "20240906.ce6724722cf6";
10
10
  readonly V2: "20241025.db973a8e8032";
11
+ readonly V3: "20250821.4ee6689bf7ec";
11
12
  };
12
13
  export type ProtocolVersionType = (typeof ProtocolVersion)[keyof typeof ProtocolVersion] | string;
13
14
  export declare const CKBFS_CODE_HASH: Record<NetworkType, Record<string, string>>;
@@ -19,7 +20,7 @@ export declare const DEPLOY_TX_HASH: Record<NetworkType, Record<string, {
19
20
  ckbfs: string;
20
21
  adler32: string;
21
22
  }>>;
22
- export declare const DEFAULT_VERSION: "20241025.db973a8e8032";
23
+ export declare const DEFAULT_VERSION: "20250821.4ee6689bf7ec";
23
24
  export declare const DEFAULT_NETWORK = NetworkType.Testnet;
24
25
  export interface CKBFSScriptConfig {
25
26
  codeHash: string;
@@ -14,6 +14,7 @@ var NetworkType;
14
14
  exports.ProtocolVersion = {
15
15
  V1: "20240906.ce6724722cf6", // Original version, compact and simple, suitable for small files
16
16
  V2: "20241025.db973a8e8032", // New version, more features and can do complex operations
17
+ V3: "20250821.4ee6689bf7ec", // Witnesses-based storage, no backlinks in cell data, more affordable
17
18
  };
18
19
  // CKBFS Type Script Constants
19
20
  exports.CKBFS_CODE_HASH = {
@@ -23,6 +24,7 @@ exports.CKBFS_CODE_HASH = {
23
24
  [NetworkType.Testnet]: {
24
25
  [exports.ProtocolVersion.V1]: "0xe8905ad29a02cf8befa9c258f4f941773839a618d75a64afc22059de9413f712",
25
26
  [exports.ProtocolVersion.V2]: "0x31e6376287d223b8c0410d562fb422f04d1d617b2947596a14c3d2efb7218d3a",
27
+ [exports.ProtocolVersion.V3]: "0xb5d13ffe0547c78021c01fe24dce2e959a1ed8edbca3cb93dd2e9f57fb56d695",
26
28
  },
27
29
  };
28
30
  exports.CKBFS_TYPE_ID = {
@@ -32,6 +34,7 @@ exports.CKBFS_TYPE_ID = {
32
34
  [NetworkType.Testnet]: {
33
35
  [exports.ProtocolVersion.V1]: "0x88ef4d436af35684a27edda0d44dd8771318330285f90f02d13606e095aea86f",
34
36
  [exports.ProtocolVersion.V2]: "0x7c6dcab8268201f064dc8676b5eafa60ca2569e5c6209dcbab0eb64a9cb3aaa3",
37
+ [exports.ProtocolVersion.V3]: "0xaebf5a7b541da9603c2066a9768d3d18fea2e7f3c1943821611545155fecc671",
35
38
  },
36
39
  };
37
40
  // Adler32 Hasher Constants
@@ -42,6 +45,7 @@ exports.ADLER32_CODE_HASH = {
42
45
  [NetworkType.Testnet]: {
43
46
  [exports.ProtocolVersion.V1]: "0x8af42cd329cf1bcffb4c73b48252e99cb32346fdbc1cdaa5ae1d000232d47e84",
44
47
  [exports.ProtocolVersion.V2]: "0x2138683f76944437c0c643664120d620bdb5858dd6c9d1d156805e279c2c536f",
48
+ [exports.ProtocolVersion.V3]: "0xbd944c8c5aa127270b591d50ab899c9a2a3e4429300db4ea3d7523aa592c1db1",
45
49
  },
46
50
  };
47
51
  exports.ADLER32_TYPE_ID = {
@@ -51,6 +55,7 @@ exports.ADLER32_TYPE_ID = {
51
55
  [NetworkType.Testnet]: {
52
56
  [exports.ProtocolVersion.V1]: "0xccf29a0d8e860044a3d2f6a6e709f6572f77e4fe245fadd212fc342337048d60",
53
57
  [exports.ProtocolVersion.V2]: "0x5f73f128be76e397f5a3b56c94ca16883a8ee91b498bc0ee80473818318c05ac",
58
+ [exports.ProtocolVersion.V3]: "0x552e2a5e679f45bca7834b03a1f8613f2a910b64a7bafb51986cfc6f1b6cb31c",
54
59
  },
55
60
  };
56
61
  // Dep Group Transaction Constants
@@ -61,6 +66,7 @@ exports.DEP_GROUP_TX_HASH = {
61
66
  [NetworkType.Testnet]: {
62
67
  [exports.ProtocolVersion.V1]: "0xc8fd44aba36f0c4b37536b6c7ea3b88df65fa97e02f77cd33b9bf20bf241a09b",
63
68
  [exports.ProtocolVersion.V2]: "0x469af0d961dcaaedd872968a9388b546717a6ccfa47b3165b3f9c981e9d66aaa",
69
+ [exports.ProtocolVersion.V3]: "0x47cfa8d554cccffe7796f93b58437269de1f98f029d0a52b6b146381f3e95e61",
64
70
  },
65
71
  };
66
72
  // Deploy Transaction Constants
@@ -80,10 +86,14 @@ exports.DEPLOY_TX_HASH = {
80
86
  ckbfs: "0x2c8c9ad3134743368b5a79977648f96c5bd0aba187021a72fb624301064d3616",
81
87
  adler32: "0x2c8c9ad3134743368b5a79977648f96c5bd0aba187021a72fb624301064d3616",
82
88
  },
89
+ [exports.ProtocolVersion.V3]: {
90
+ ckbfs: "0x1488b592b0946589730c906c6d9a46fb82c1181156fc1a4251adce14002a9cfb",
91
+ adler32: "0x8d6bd7ea704f9b19af5b83b81544c34982515a825e6185d88faf47583a542671",
92
+ },
83
93
  },
84
94
  };
85
- // Default values - V2 is now the default
86
- exports.DEFAULT_VERSION = exports.ProtocolVersion.V2;
95
+ // Default values - V3 is now the default
96
+ exports.DEFAULT_VERSION = exports.ProtocolVersion.V3;
87
97
  exports.DEFAULT_NETWORK = NetworkType.Testnet;
88
98
  /**
89
99
  * Get CKBFS script configuration for a specific network and version
@@ -242,3 +242,47 @@ export declare function decodeFileFromChainByTypeId(client: any, typeId: string,
242
242
  size: number;
243
243
  backLinks: any[];
244
244
  } | null>;
245
+ /**
246
+ * Retrieves complete file content from the blockchain for CKBFS v3
247
+ * V3 stores backlinks in witnesses instead of cell data
248
+ * @param client The CKB client to use for blockchain queries
249
+ * @param outPoint The output point of the latest CKBFS v3 cell
250
+ * @param ckbfsData The data from the latest CKBFS v3 cell
251
+ * @returns Promise resolving to the complete file content
252
+ */
253
+ export declare function getFileContentFromChainV3(client: any, outPoint: {
254
+ txHash: string;
255
+ index: number;
256
+ }, ckbfsData: any): Promise<Uint8Array>;
257
+ /**
258
+ * Retrieves complete file content from the blockchain using identifier for CKBFS v3
259
+ * @param client The CKB client to use for blockchain queries
260
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
261
+ * @param options Optional configuration for network, version, and useTypeID
262
+ * @returns Promise resolving to the complete file content and metadata
263
+ */
264
+ export declare function getFileContentFromChainByIdentifierV3(client: any, identifier: string, options?: {
265
+ network?: "mainnet" | "testnet";
266
+ version?: string;
267
+ useTypeID?: boolean;
268
+ }): Promise<{
269
+ content: Uint8Array;
270
+ filename: string;
271
+ contentType: string;
272
+ checksum: number;
273
+ size: number;
274
+ parsedId: ParsedIdentifier;
275
+ } | null>;
276
+ /**
277
+ * Saves CKBFS v3 file content retrieved from blockchain by identifier to disk
278
+ * @param client The CKB client to use for blockchain queries
279
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
280
+ * @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
281
+ * @param options Optional configuration for network, version, and useTypeID
282
+ * @returns Promise resolving to the path where the file was saved, or null if file not found
283
+ */
284
+ export declare function saveFileFromChainByIdentifierV3(client: any, identifier: string, outputPath?: string, options?: {
285
+ network?: "mainnet" | "testnet";
286
+ version?: string;
287
+ useTypeID?: boolean;
288
+ }): Promise<string | null>;
@@ -58,8 +58,13 @@ exports.saveFileFromChainByIdentifier = saveFileFromChainByIdentifier;
58
58
  exports.saveFileFromChainByTypeId = saveFileFromChainByTypeId;
59
59
  exports.decodeFileFromChainByIdentifier = decodeFileFromChainByIdentifier;
60
60
  exports.decodeFileFromChainByTypeId = decodeFileFromChainByTypeId;
61
+ exports.getFileContentFromChainV3 = getFileContentFromChainV3;
62
+ exports.getFileContentFromChainByIdentifierV3 = getFileContentFromChainByIdentifierV3;
63
+ exports.saveFileFromChainByIdentifierV3 = saveFileFromChainByIdentifierV3;
61
64
  const fs_1 = __importDefault(require("fs"));
62
65
  const path_1 = __importDefault(require("path"));
66
+ const constants_1 = require("./constants");
67
+ const witness_1 = require("./witness");
63
68
  /**
64
69
  * Utility functions for file operations
65
70
  */
@@ -458,7 +463,7 @@ function parseIdentifier(identifier) {
458
463
  * @returns Promise resolving to the found cell and transaction info, or null if not found
459
464
  */
460
465
  async function resolveCKBFSCell(client, identifier, options = {}) {
461
- const { network = "testnet", version = "20241025.db973a8e8032", useTypeID = false, } = options;
466
+ const { network = "testnet", version = constants_1.ProtocolVersion.V3, useTypeID = false, } = options;
462
467
  const parsedId = parseIdentifier(identifier);
463
468
  try {
464
469
  if (parsedId.type === IdentifierType.TypeID && parsedId.typeId) {
@@ -529,15 +534,27 @@ async function resolveCKBFSCell(client, identifier, options = {}) {
529
534
  * @param useTypeID Whether to use type ID instead of code hash for script matching
530
535
  * @returns Promise resolving to the found cell and transaction info, or null if not found
531
536
  */
532
- async function findCKBFSCellByTypeId(client, typeId, network = "testnet", version = "20241025.db973a8e8032", useTypeID = false) {
537
+ async function findCKBFSCellByTypeId(client, typeId, network = "testnet", version = constants_1.ProtocolVersion.V3, useTypeID = false) {
533
538
  try {
534
539
  // Import constants dynamically to avoid circular dependencies
535
540
  const { getCKBFSScriptConfig, NetworkType, ProtocolVersion } = await Promise.resolve().then(() => __importStar(require("./constants")));
536
541
  // Get CKBFS script config
537
542
  const networkType = network === "mainnet" ? NetworkType.Mainnet : NetworkType.Testnet;
538
- const protocolVersion = version === "20240906.ce6724722cf6"
539
- ? ProtocolVersion.V1
540
- : ProtocolVersion.V2;
543
+ // Map version strings to protocol versions
544
+ let protocolVersion;
545
+ if (version === "20240906.ce6724722cf6") {
546
+ protocolVersion = ProtocolVersion.V1;
547
+ }
548
+ else if (version === "20241025.db973a8e8032") {
549
+ protocolVersion = ProtocolVersion.V2;
550
+ }
551
+ else if (version === "20250821.4ee6689bf7ec" || version === ProtocolVersion.V3) {
552
+ protocolVersion = ProtocolVersion.V3;
553
+ }
554
+ else {
555
+ // Default to the version passed in if it doesn't match known patterns
556
+ protocolVersion = version;
557
+ }
541
558
  const config = getCKBFSScriptConfig(networkType, protocolVersion, useTypeID);
542
559
  // Create the script to search for
543
560
  const script = {
@@ -578,7 +595,7 @@ async function findCKBFSCellByTypeId(client, typeId, network = "testnet", versio
578
595
  * @returns Promise resolving to the complete file content and metadata
579
596
  */
580
597
  async function getFileContentFromChainByIdentifier(client, identifier, options = {}) {
581
- const { network = "testnet", version = "20241025.db973a8e8032", useTypeID = false, } = options;
598
+ const { network = "testnet", version = constants_1.ProtocolVersion.V3, useTypeID = false, } = options;
582
599
  try {
583
600
  // Resolve the CKBFS cell using any supported identifier format
584
601
  const cellInfo = await resolveCKBFSCell(client, identifier, {
@@ -608,26 +625,44 @@ async function getFileContentFromChainByIdentifier(client, identifier, options =
608
625
  const rawData = outputData.startsWith("0x")
609
626
  ? ccc.bytesFrom(outputData.slice(2), "hex")
610
627
  : Buffer.from(outputData, "hex");
611
- // Try to unpack CKBFS data with both protocol versions
628
+ // Try to unpack CKBFS data with all protocol versions (V3, V2, V1)
612
629
  let ckbfsData;
613
630
  let protocolVersion = version;
614
631
  try {
615
- ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
632
+ // Try V3 first if the version suggests it
633
+ if (version === ProtocolVersion.V3 || version === "20250821.4ee6689bf7ec") {
634
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
635
+ }
636
+ else {
637
+ throw new Error("Not V3 version");
638
+ }
616
639
  }
617
- catch (error) {
640
+ catch (v3Error) {
618
641
  try {
619
- ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
620
- protocolVersion = "20240906.ce6724722cf6";
642
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
643
+ protocolVersion = ProtocolVersion.V2;
621
644
  }
622
- catch (v1Error) {
623
- throw new Error(`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`);
645
+ catch (v2Error) {
646
+ try {
647
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
648
+ protocolVersion = ProtocolVersion.V1;
649
+ }
650
+ catch (v1Error) {
651
+ throw new Error(`Failed to unpack CKBFS data with all versions: V3(${v3Error}), V2(${v2Error}), V1(${v1Error})`);
652
+ }
624
653
  }
625
654
  }
626
655
  console.log(`Found CKBFS file: ${ckbfsData.filename}`);
627
656
  console.log(`Content type: ${ckbfsData.contentType}`);
628
657
  console.log(`Protocol version: ${protocolVersion}`);
629
- // Use existing function to get complete file content
630
- const content = await getFileContentFromChain(client, outPoint, ckbfsData);
658
+ // Use appropriate function to get complete file content based on protocol version
659
+ let content;
660
+ if (protocolVersion === ProtocolVersion.V3) {
661
+ content = await getFileContentFromChainV3(client, outPoint, ckbfsData);
662
+ }
663
+ else {
664
+ content = await getFileContentFromChain(client, outPoint, ckbfsData);
665
+ }
631
666
  return {
632
667
  content,
633
668
  filename: ckbfsData.filename,
@@ -713,7 +748,7 @@ async function saveFileFromChainByTypeId(client, typeId, outputPath, options = {
713
748
  * @returns Promise resolving to the decoded file content and metadata, or null if not found
714
749
  */
715
750
  async function decodeFileFromChainByIdentifier(client, identifier, options = {}) {
716
- const { network = "testnet", version = "20241025.db973a8e8032", useTypeID = false, } = options;
751
+ const { network = "testnet", version = constants_1.ProtocolVersion.V3, useTypeID = false, } = options;
717
752
  try {
718
753
  // Resolve the CKBFS cell using any supported identifier format
719
754
  const cellInfo = await resolveCKBFSCell(client, identifier, {
@@ -741,19 +776,31 @@ async function decodeFileFromChainByIdentifier(client, identifier, options = {})
741
776
  const rawData = outputData.startsWith("0x")
742
777
  ? ccc.bytesFrom(outputData.slice(2), "hex")
743
778
  : Buffer.from(outputData, "hex");
744
- // Try to unpack CKBFS data with both protocol versions
779
+ // Try to unpack CKBFS data with all protocol versions (V3, V2, V1)
745
780
  let ckbfsData;
746
781
  let protocolVersion = version;
747
782
  try {
748
- ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
783
+ // Try V3 first if the version suggests it
784
+ if (version === ProtocolVersion.V3 || version === "20250821.4ee6689bf7ec") {
785
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
786
+ }
787
+ else {
788
+ throw new Error("Not V3 version");
789
+ }
749
790
  }
750
- catch (error) {
791
+ catch (v3Error) {
751
792
  try {
752
- ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
753
- protocolVersion = "20240906.ce6724722cf6";
793
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
794
+ protocolVersion = ProtocolVersion.V2;
754
795
  }
755
- catch (v1Error) {
756
- throw new Error(`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`);
796
+ catch (v2Error) {
797
+ try {
798
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
799
+ protocolVersion = ProtocolVersion.V1;
800
+ }
801
+ catch (v1Error) {
802
+ throw new Error(`Failed to unpack CKBFS data with all versions: V3(${v3Error}), V2(${v2Error}), V1(${v1Error})`);
803
+ }
757
804
  }
758
805
  }
759
806
  console.log(`Found CKBFS file: ${ckbfsData.filename}`);
@@ -762,13 +809,25 @@ async function decodeFileFromChainByIdentifier(client, identifier, options = {})
762
809
  // Get witness indexes from CKBFS data
763
810
  const indexes = ckbfsData.indexes ||
764
811
  (ckbfsData.index !== undefined ? [ckbfsData.index] : []);
765
- // Use direct witness decoding method
766
- const content = decodeFileFromWitnessData({
767
- witnesses: tx.witnesses,
768
- indexes: indexes,
769
- filename: ckbfsData.filename,
770
- contentType: ckbfsData.contentType,
771
- });
812
+ // Use direct witness decoding method for V1/V2, or appropriate method for V3
813
+ let content;
814
+ if (protocolVersion === ProtocolVersion.V3) {
815
+ // For V3, we should use the V3-specific content retrieval which handles witness chains
816
+ const fullContent = await getFileContentFromChainV3(client, outPoint, ckbfsData);
817
+ content = {
818
+ content: fullContent,
819
+ size: fullContent.length,
820
+ };
821
+ }
822
+ else {
823
+ // For V1/V2, use the direct witness decoding method
824
+ content = decodeFileFromWitnessData({
825
+ witnesses: tx.witnesses,
826
+ indexes: indexes,
827
+ filename: ckbfsData.filename,
828
+ contentType: ckbfsData.contentType,
829
+ });
830
+ }
772
831
  return {
773
832
  content: content.content,
774
833
  filename: ckbfsData.filename,
@@ -799,3 +858,217 @@ async function decodeFileFromChainByTypeId(client, typeId, options = {}) {
799
858
  }
800
859
  return null;
801
860
  }
861
+ /**
862
+ * Retrieves complete file content from the blockchain for CKBFS v3
863
+ * V3 stores backlinks in witnesses instead of cell data
864
+ * @param client The CKB client to use for blockchain queries
865
+ * @param outPoint The output point of the latest CKBFS v3 cell
866
+ * @param ckbfsData The data from the latest CKBFS v3 cell
867
+ * @returns Promise resolving to the complete file content
868
+ */
869
+ async function getFileContentFromChainV3(client, outPoint, ckbfsData) {
870
+ console.log(`Retrieving v3 file: ${safelyDecode(ckbfsData.filename)}`);
871
+ console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
872
+ // Follow the CKBFS cell chain backwards to build the complete transaction sequence
873
+ const transactionChain = [];
874
+ let currentTxHash = outPoint.txHash;
875
+ let currentCellIndex = outPoint.index;
876
+ let currentWitnessIndex = ckbfsData.index;
877
+ // Follow the chain backwards to build the complete transaction sequence
878
+ while (currentTxHash && currentWitnessIndex !== undefined) {
879
+ const tx = await client.getTransaction(currentTxHash);
880
+ if (!tx || !tx.transaction) {
881
+ console.warn(`Transaction ${currentTxHash} not found`);
882
+ break;
883
+ }
884
+ if (currentWitnessIndex >= tx.transaction.witnesses.length) {
885
+ console.warn(`Witness index ${currentWitnessIndex} out of range in transaction ${currentTxHash}`);
886
+ break;
887
+ }
888
+ const witnessHex = tx.transaction.witnesses[currentWitnessIndex];
889
+ const witness = new Uint8Array(Buffer.from(witnessHex.slice(2), "hex"));
890
+ // Check if this is a v3 head witness
891
+ if ((0, witness_1.isCKBFSV3Witness)(witness)) {
892
+ const witnessData = (0, witness_1.extractCKBFSV3WitnessContent)(witness, true);
893
+ // Collect content from this transaction (head + continuation witnesses)
894
+ const txContentPieces = [witnessData.content];
895
+ // Follow continuation witnesses in this transaction
896
+ let nextIndex = witnessData.nextIndex;
897
+ while (nextIndex > 0 && nextIndex < tx.transaction.witnesses.length) {
898
+ const nextWitnessHex = tx.transaction.witnesses[nextIndex];
899
+ const nextWitness = new Uint8Array(Buffer.from(nextWitnessHex.slice(2), "hex"));
900
+ const nextWitnessData = (0, witness_1.extractCKBFSV3WitnessContent)(nextWitness, false);
901
+ txContentPieces.push(nextWitnessData.content);
902
+ nextIndex = nextWitnessData.nextIndex;
903
+ }
904
+ // Check if this is a publish operation (all zeros for previous position)
905
+ const isPublish = witnessData.previousTxHash === '0x' + '00'.repeat(32) &&
906
+ witnessData.previousWitnessIndex === 0;
907
+ // Add this transaction's content to the beginning of the chain (since we're going backwards)
908
+ transactionChain.unshift({
909
+ txHash: currentTxHash,
910
+ cellIndex: currentCellIndex,
911
+ witnessIndex: currentWitnessIndex,
912
+ content: txContentPieces,
913
+ isPublish,
914
+ });
915
+ // If this is the original publish operation, we're done
916
+ if (isPublish) {
917
+ break;
918
+ }
919
+ // For append operations, we need to find the previous CKBFS cell
920
+ const previousTxHash = witnessData.previousTxHash;
921
+ if (!previousTxHash || previousTxHash === '0x' + '00'.repeat(32)) {
922
+ break;
923
+ }
924
+ // Get the previous transaction to find the CKBFS cell and its witness index
925
+ const prevTx = await client.getTransaction(previousTxHash);
926
+ if (!prevTx || !prevTx.transaction) {
927
+ console.warn(`Previous transaction ${previousTxHash} not found`);
928
+ break;
929
+ }
930
+ // Import Transaction class to parse the previous transaction
931
+ const { Transaction } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
932
+ const prevTxObj = Transaction.from(prevTx.transaction);
933
+ // Find the CKBFS cell in the previous transaction (it should have a type script)
934
+ let prevCkbfsCellIndex = -1;
935
+ let prevCkbfsData = null;
936
+ for (let i = 0; i < prevTxObj.outputs.length; i++) {
937
+ const output = prevTxObj.outputs[i];
938
+ const outputData = prevTxObj.outputsData[i];
939
+ // Check if this output has a type script (CKBFS cells have type scripts)
940
+ if (output.type && outputData && outputData !== '0x') {
941
+ try {
942
+ // Try to parse as CKBFS data
943
+ const { ccc } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
944
+ const { CKBFSData } = await Promise.resolve().then(() => __importStar(require("./molecule")));
945
+ const { ProtocolVersion } = await Promise.resolve().then(() => __importStar(require("./constants")));
946
+ const rawData = outputData.startsWith("0x")
947
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
948
+ : Buffer.from(outputData, "hex");
949
+ const ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
950
+ prevCkbfsCellIndex = i;
951
+ prevCkbfsData = ckbfsData;
952
+ break;
953
+ }
954
+ catch (error) {
955
+ // Not a CKBFS cell, continue searching
956
+ }
957
+ }
958
+ }
959
+ if (prevCkbfsCellIndex === -1 || !prevCkbfsData) {
960
+ console.warn(`No CKBFS cell found in previous transaction ${previousTxHash}`);
961
+ break;
962
+ }
963
+ const previousWitnessIndex = prevCkbfsData.index;
964
+ // Move to the previous CKBFS cell
965
+ currentTxHash = previousTxHash;
966
+ currentCellIndex = prevCkbfsCellIndex;
967
+ currentWitnessIndex = previousWitnessIndex;
968
+ }
969
+ else {
970
+ console.warn(`Witness at index ${currentWitnessIndex} in transaction ${currentTxHash} is not a valid CKBFS v3 witness`);
971
+ break;
972
+ }
973
+ }
974
+ // Now assemble all content pieces in chronological order (first creation to latest)
975
+ const allContentPieces = [];
976
+ for (const txEntry of transactionChain) {
977
+ allContentPieces.push(...txEntry.content);
978
+ }
979
+ // Combine all content pieces
980
+ return Buffer.concat(allContentPieces);
981
+ }
982
+ /**
983
+ * Retrieves complete file content from the blockchain using identifier for CKBFS v3
984
+ * @param client The CKB client to use for blockchain queries
985
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
986
+ * @param options Optional configuration for network, version, and useTypeID
987
+ * @returns Promise resolving to the complete file content and metadata
988
+ */
989
+ async function getFileContentFromChainByIdentifierV3(client, identifier, options = {}) {
990
+ const { network = "testnet", version = constants_1.ProtocolVersion.V3, useTypeID = false, } = options;
991
+ try {
992
+ // Resolve the CKBFS cell using any supported identifier format
993
+ const cellInfo = await resolveCKBFSCell(client, identifier, {
994
+ network,
995
+ version,
996
+ useTypeID,
997
+ });
998
+ if (!cellInfo) {
999
+ console.warn(`CKBFS v3 cell with identifier ${identifier} not found`);
1000
+ return null;
1001
+ }
1002
+ const { cell, transaction, outPoint, parsedId } = cellInfo;
1003
+ // Import Transaction class dynamically
1004
+ const { Transaction } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
1005
+ const tx = Transaction.from(transaction);
1006
+ // Get output data from the cell
1007
+ const outputIndex = outPoint.index;
1008
+ const outputData = tx.outputsData[outputIndex];
1009
+ if (!outputData) {
1010
+ throw new Error(`Output data not found for cell at index ${outputIndex}`);
1011
+ }
1012
+ // Import required modules dynamically
1013
+ const { ccc } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
1014
+ const { CKBFSData } = await Promise.resolve().then(() => __importStar(require("./molecule")));
1015
+ // Parse the output data
1016
+ const rawData = outputData.startsWith("0x")
1017
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
1018
+ : Buffer.from(outputData, "hex");
1019
+ // Unpack CKBFS v3 data
1020
+ const ckbfsData = CKBFSData.unpack(rawData, constants_1.ProtocolVersion.V3);
1021
+ console.log(`Found CKBFS v3 file: ${ckbfsData.filename}`);
1022
+ console.log(`Content type: ${ckbfsData.contentType}`);
1023
+ // Use v3-specific function to get complete file content
1024
+ const content = await getFileContentFromChainV3(client, outPoint, ckbfsData);
1025
+ return {
1026
+ content,
1027
+ filename: ckbfsData.filename,
1028
+ contentType: ckbfsData.contentType,
1029
+ checksum: ckbfsData.checksum,
1030
+ size: content.length,
1031
+ parsedId,
1032
+ };
1033
+ }
1034
+ catch (error) {
1035
+ console.error(`Error retrieving v3 file by identifier ${identifier}:`, error);
1036
+ throw error;
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Saves CKBFS v3 file content retrieved from blockchain by identifier to disk
1041
+ * @param client The CKB client to use for blockchain queries
1042
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
1043
+ * @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
1044
+ * @param options Optional configuration for network, version, and useTypeID
1045
+ * @returns Promise resolving to the path where the file was saved, or null if file not found
1046
+ */
1047
+ async function saveFileFromChainByIdentifierV3(client, identifier, outputPath, options = {}) {
1048
+ try {
1049
+ // Get file content by identifier using v3 method
1050
+ const fileData = await getFileContentFromChainByIdentifierV3(client, identifier, { ...options, version: constants_1.ProtocolVersion.V3 });
1051
+ if (!fileData) {
1052
+ console.warn(`CKBFS v3 file with identifier ${identifier} not found`);
1053
+ return null;
1054
+ }
1055
+ // Determine output path
1056
+ const filePath = outputPath || fileData.filename;
1057
+ // Ensure directory exists
1058
+ const directory = path_1.default.dirname(filePath);
1059
+ if (!fs_1.default.existsSync(directory)) {
1060
+ fs_1.default.mkdirSync(directory, { recursive: true });
1061
+ }
1062
+ // Write file
1063
+ fs_1.default.writeFileSync(filePath, fileData.content);
1064
+ console.log(`CKBFS v3 file saved to: ${filePath}`);
1065
+ console.log(`Size: ${fileData.size} bytes`);
1066
+ console.log(`Content type: ${fileData.contentType}`);
1067
+ console.log(`Checksum: ${fileData.checksum}`);
1068
+ return filePath;
1069
+ }
1070
+ catch (error) {
1071
+ console.error(`Error saving v3 file by identifier ${identifier}:`, error);
1072
+ throw error;
1073
+ }
1074
+ }