@ckbfs/api 1.5.0 → 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 +440 -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 -105
  16. package/dist/utils/transaction.js +45 -565
  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
@@ -43,15 +43,9 @@ export async function updateChecksum(previousChecksum: number, newData: Uint8Arr
43
43
  adlerB = (adlerB + adlerA) % MOD_ADLER;
44
44
  }
45
45
 
46
- // Combine a and b to get the final checksum
47
- // Use a Uint32Array to ensure we get a proper unsigned 32-bit integer
48
- const buffer = new ArrayBuffer(4);
49
- const view = new DataView(buffer);
50
- view.setUint16(0, adlerA, true); // Set lower 16 bits (little endian)
51
- view.setUint16(2, adlerB, true); // Set upper 16 bits (little endian)
52
-
53
- // Read as an unsigned 32-bit integer
54
- 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
55
49
 
56
50
  console.log(`Updated checksum from ${previousChecksum} to ${updatedChecksum} for appended content`);
57
51
 
@@ -93,4 +87,91 @@ export async function verifyWitnessChecksum(
93
87
 
94
88
  // Otherwise, calculate checksum from scratch
95
89
  return verifyChecksum(contentBytes, expectedChecksum);
90
+ }
91
+
92
+ /**
93
+ * Verifies the checksum of a CKBFS v3 witness
94
+ * @param witness The v3 witness bytes
95
+ * @param expectedChecksum The expected checksum
96
+ * @param previousChecksum Optional previous checksum for chained verification
97
+ * @returns Promise resolving to a boolean indicating whether the checksum is valid
98
+ */
99
+ export async function verifyV3WitnessChecksum(
100
+ witness: Uint8Array,
101
+ expectedChecksum: number,
102
+ previousChecksum?: number
103
+ ): Promise<boolean> {
104
+ // Check if this is a head witness (contains CKBFS header)
105
+ const isHeadWitness = witness.length >= 50 &&
106
+ new TextDecoder().decode(witness.slice(0, 5)) === 'CKBFS' &&
107
+ witness[5] === 0x03;
108
+
109
+ let contentBytes: Uint8Array;
110
+ let extractedPreviousChecksum: number | undefined = previousChecksum;
111
+
112
+ if (isHeadWitness) {
113
+ // Head witness: extract content after backlink structure
114
+ // Format: CKBFS(5) + version(1) + prevTxHash(32) + prevWitnessIndex(4) + prevChecksum(4) + nextIndex(4) + content
115
+ contentBytes = witness.slice(50);
116
+
117
+ // Extract previous checksum from head witness if not provided externally
118
+ if (previousChecksum === undefined) {
119
+ const prevChecksumBytes = witness.slice(42, 46);
120
+ extractedPreviousChecksum = new DataView(prevChecksumBytes.buffer).getUint32(0, true); // little-endian
121
+ }
122
+ } else {
123
+ // Continuation witness: extract content after next index
124
+ // Format: nextIndex(4) + content
125
+ contentBytes = witness.slice(4);
126
+ }
127
+
128
+ // If previous checksum is available (either provided or extracted), use it for rolling calculation
129
+ if (extractedPreviousChecksum !== undefined && extractedPreviousChecksum !== 0) {
130
+ const updatedChecksum = await updateChecksum(extractedPreviousChecksum, contentBytes);
131
+ return updatedChecksum === expectedChecksum;
132
+ }
133
+
134
+ // Otherwise, calculate checksum from scratch
135
+ return verifyChecksum(contentBytes, expectedChecksum);
136
+ }
137
+
138
+ /**
139
+ * Verifies the checksum chain for multiple v3 witnesses
140
+ * @param witnesses Array of v3 witness bytes
141
+ * @param finalExpectedChecksum The expected final checksum
142
+ * @param initialChecksum Optional initial checksum (for append operations)
143
+ * @returns Promise resolving to a boolean indicating whether the checksum chain is valid
144
+ */
145
+ export async function verifyV3WitnessChain(
146
+ witnesses: Uint8Array[],
147
+ finalExpectedChecksum: number,
148
+ initialChecksum: number = 1 // Adler32 starts with 1
149
+ ): Promise<boolean> {
150
+ if (witnesses.length === 0) {
151
+ return finalExpectedChecksum === initialChecksum;
152
+ }
153
+
154
+ let currentChecksum = initialChecksum;
155
+
156
+ // Process each witness in the chain
157
+ for (const witness of witnesses) {
158
+ const isHeadWitness = witness.length >= 50 &&
159
+ new TextDecoder().decode(witness.slice(0, 5)) === 'CKBFS' &&
160
+ witness[5] === 0x03;
161
+
162
+ let contentBytes: Uint8Array;
163
+
164
+ if (isHeadWitness) {
165
+ // Head witness: extract content after backlink structure
166
+ contentBytes = witness.slice(50);
167
+ } else {
168
+ // Continuation witness: extract content after next index
169
+ contentBytes = witness.slice(4);
170
+ }
171
+
172
+ // Update checksum with this witness's content
173
+ currentChecksum = await updateChecksum(currentChecksum, contentBytes);
174
+ }
175
+
176
+ return currentChecksum === finalExpectedChecksum;
96
177
  }
@@ -11,6 +11,7 @@ export enum NetworkType {
11
11
  export const ProtocolVersion = {
12
12
  V1: "20240906.ce6724722cf6", // Original version, compact and simple, suitable for small files
13
13
  V2: "20241025.db973a8e8032", // New version, more features and can do complex operations
14
+ V3: "20250821.4ee6689bf7ec", // Witnesses-based storage, no backlinks in cell data, more affordable
14
15
  } as const;
15
16
 
16
17
  export type ProtocolVersionType =
@@ -28,6 +29,8 @@ export const CKBFS_CODE_HASH: Record<NetworkType, Record<string, string>> = {
28
29
  "0xe8905ad29a02cf8befa9c258f4f941773839a618d75a64afc22059de9413f712",
29
30
  [ProtocolVersion.V2]:
30
31
  "0x31e6376287d223b8c0410d562fb422f04d1d617b2947596a14c3d2efb7218d3a",
32
+ [ProtocolVersion.V3]:
33
+ "0xb5d13ffe0547c78021c01fe24dce2e959a1ed8edbca3cb93dd2e9f57fb56d695",
31
34
  },
32
35
  };
33
36
 
@@ -41,6 +44,8 @@ export const CKBFS_TYPE_ID: Record<NetworkType, Record<string, string>> = {
41
44
  "0x88ef4d436af35684a27edda0d44dd8771318330285f90f02d13606e095aea86f",
42
45
  [ProtocolVersion.V2]:
43
46
  "0x7c6dcab8268201f064dc8676b5eafa60ca2569e5c6209dcbab0eb64a9cb3aaa3",
47
+ [ProtocolVersion.V3]:
48
+ "0xaebf5a7b541da9603c2066a9768d3d18fea2e7f3c1943821611545155fecc671",
44
49
  },
45
50
  };
46
51
 
@@ -55,6 +60,8 @@ export const ADLER32_CODE_HASH: Record<NetworkType, Record<string, string>> = {
55
60
  "0x8af42cd329cf1bcffb4c73b48252e99cb32346fdbc1cdaa5ae1d000232d47e84",
56
61
  [ProtocolVersion.V2]:
57
62
  "0x2138683f76944437c0c643664120d620bdb5858dd6c9d1d156805e279c2c536f",
63
+ [ProtocolVersion.V3]:
64
+ "0xbd944c8c5aa127270b591d50ab899c9a2a3e4429300db4ea3d7523aa592c1db1",
58
65
  },
59
66
  };
60
67
 
@@ -68,6 +75,8 @@ export const ADLER32_TYPE_ID: Record<NetworkType, Record<string, string>> = {
68
75
  "0xccf29a0d8e860044a3d2f6a6e709f6572f77e4fe245fadd212fc342337048d60",
69
76
  [ProtocolVersion.V2]:
70
77
  "0x5f73f128be76e397f5a3b56c94ca16883a8ee91b498bc0ee80473818318c05ac",
78
+ [ProtocolVersion.V3]:
79
+ "0x552e2a5e679f45bca7834b03a1f8613f2a910b64a7bafb51986cfc6f1b6cb31c",
71
80
  },
72
81
  };
73
82
 
@@ -82,6 +91,8 @@ export const DEP_GROUP_TX_HASH: Record<NetworkType, Record<string, string>> = {
82
91
  "0xc8fd44aba36f0c4b37536b6c7ea3b88df65fa97e02f77cd33b9bf20bf241a09b",
83
92
  [ProtocolVersion.V2]:
84
93
  "0x469af0d961dcaaedd872968a9388b546717a6ccfa47b3165b3f9c981e9d66aaa",
94
+ [ProtocolVersion.V3]:
95
+ "0x47cfa8d554cccffe7796f93b58437269de1f98f029d0a52b6b146381f3e95e61",
85
96
  },
86
97
  };
87
98
 
@@ -111,11 +122,17 @@ export const DEPLOY_TX_HASH: Record<
111
122
  adler32:
112
123
  "0x2c8c9ad3134743368b5a79977648f96c5bd0aba187021a72fb624301064d3616",
113
124
  },
125
+ [ProtocolVersion.V3]: {
126
+ ckbfs:
127
+ "0x1488b592b0946589730c906c6d9a46fb82c1181156fc1a4251adce14002a9cfb",
128
+ adler32:
129
+ "0x8d6bd7ea704f9b19af5b83b81544c34982515a825e6185d88faf47583a542671",
130
+ },
114
131
  },
115
132
  };
116
133
 
117
- // Default values - V2 is now the default
118
- export const DEFAULT_VERSION = ProtocolVersion.V2;
134
+ // Default values - V3 is now the default
135
+ export const DEFAULT_VERSION = ProtocolVersion.V3;
119
136
  export const DEFAULT_NETWORK = NetworkType.Testnet;
120
137
 
121
138
  // Helper function to get CKBFS script configuration
package/src/utils/file.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { ProtocolVersion } from "./constants";
4
+ import { extractCKBFSV3WitnessContent, isCKBFSV3Witness } from "./witness";
3
5
 
4
6
  /**
5
7
  * Utility functions for file operations
@@ -524,7 +526,7 @@ async function resolveCKBFSCell(
524
526
  } | null> {
525
527
  const {
526
528
  network = "testnet",
527
- version = "20241025.db973a8e8032",
529
+ version = ProtocolVersion.V3,
528
530
  useTypeID = false,
529
531
  } = options;
530
532
 
@@ -628,7 +630,7 @@ async function findCKBFSCellByTypeId(
628
630
  client: any,
629
631
  typeId: string,
630
632
  network: string = "testnet",
631
- version: string = "20241025.db973a8e8032",
633
+ version: string = ProtocolVersion.V3,
632
634
  useTypeID: boolean = false,
633
635
  ): Promise<{
634
636
  cell: any;
@@ -644,10 +646,20 @@ async function findCKBFSCellByTypeId(
644
646
  // Get CKBFS script config
645
647
  const networkType =
646
648
  network === "mainnet" ? NetworkType.Mainnet : NetworkType.Testnet;
647
- const protocolVersion =
648
- version === "20240906.ce6724722cf6"
649
- ? ProtocolVersion.V1
650
- : ProtocolVersion.V2;
649
+
650
+ // Map version strings to protocol versions
651
+ let protocolVersion: string;
652
+ if (version === "20240906.ce6724722cf6") {
653
+ protocolVersion = ProtocolVersion.V1;
654
+ } else if (version === "20241025.db973a8e8032") {
655
+ protocolVersion = ProtocolVersion.V2;
656
+ } else if (version === "20250821.4ee6689bf7ec" || version === ProtocolVersion.V3) {
657
+ protocolVersion = ProtocolVersion.V3;
658
+ } else {
659
+ // Default to the version passed in if it doesn't match known patterns
660
+ protocolVersion = version;
661
+ }
662
+
651
663
  const config = getCKBFSScriptConfig(
652
664
  networkType,
653
665
  protocolVersion,
@@ -716,7 +728,7 @@ export async function getFileContentFromChainByIdentifier(
716
728
  } | null> {
717
729
  const {
718
730
  network = "testnet",
719
- version = "20241025.db973a8e8032",
731
+ version = ProtocolVersion.V3,
720
732
  useTypeID = false,
721
733
  } = options;
722
734
 
@@ -757,20 +769,30 @@ export async function getFileContentFromChainByIdentifier(
757
769
  ? ccc.bytesFrom(outputData.slice(2), "hex")
758
770
  : Buffer.from(outputData, "hex");
759
771
 
760
- // Try to unpack CKBFS data with both protocol versions
772
+ // Try to unpack CKBFS data with all protocol versions (V3, V2, V1)
761
773
  let ckbfsData: any;
762
774
  let protocolVersion = version;
763
775
 
764
776
  try {
765
- ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
766
- } catch (error) {
777
+ // Try V3 first if the version suggests it
778
+ if (version === ProtocolVersion.V3 || version === "20250821.4ee6689bf7ec") {
779
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
780
+ } else {
781
+ throw new Error("Not V3 version");
782
+ }
783
+ } catch (v3Error) {
767
784
  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
- );
785
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
786
+ protocolVersion = ProtocolVersion.V2;
787
+ } catch (v2Error) {
788
+ try {
789
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
790
+ protocolVersion = ProtocolVersion.V1;
791
+ } catch (v1Error) {
792
+ throw new Error(
793
+ `Failed to unpack CKBFS data with all versions: V3(${v3Error}), V2(${v2Error}), V1(${v1Error})`,
794
+ );
795
+ }
774
796
  }
775
797
  }
776
798
 
@@ -778,8 +800,13 @@ export async function getFileContentFromChainByIdentifier(
778
800
  console.log(`Content type: ${ckbfsData.contentType}`);
779
801
  console.log(`Protocol version: ${protocolVersion}`);
780
802
 
781
- // Use existing function to get complete file content
782
- const content = await getFileContentFromChain(client, outPoint, ckbfsData);
803
+ // Use appropriate function to get complete file content based on protocol version
804
+ let content: Uint8Array;
805
+ if (protocolVersion === ProtocolVersion.V3) {
806
+ content = await getFileContentFromChainV3(client, outPoint, ckbfsData);
807
+ } else {
808
+ content = await getFileContentFromChain(client, outPoint, ckbfsData);
809
+ }
783
810
 
784
811
  return {
785
812
  content,
@@ -937,7 +964,7 @@ export async function decodeFileFromChainByIdentifier(
937
964
  } | null> {
938
965
  const {
939
966
  network = "testnet",
940
- version = "20241025.db973a8e8032",
967
+ version = ProtocolVersion.V3,
941
968
  useTypeID = false,
942
969
  } = options;
943
970
 
@@ -976,20 +1003,30 @@ export async function decodeFileFromChainByIdentifier(
976
1003
  ? ccc.bytesFrom(outputData.slice(2), "hex")
977
1004
  : Buffer.from(outputData, "hex");
978
1005
 
979
- // Try to unpack CKBFS data with both protocol versions
1006
+ // Try to unpack CKBFS data with all protocol versions (V3, V2, V1)
980
1007
  let ckbfsData: any;
981
1008
  let protocolVersion = version;
982
1009
 
983
1010
  try {
984
- ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
985
- } catch (error) {
1011
+ // Try V3 first if the version suggests it
1012
+ if (version === ProtocolVersion.V3 || version === "20250821.4ee6689bf7ec") {
1013
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
1014
+ } else {
1015
+ throw new Error("Not V3 version");
1016
+ }
1017
+ } catch (v3Error) {
986
1018
  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
- );
1019
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
1020
+ protocolVersion = ProtocolVersion.V2;
1021
+ } catch (v2Error) {
1022
+ try {
1023
+ ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
1024
+ protocolVersion = ProtocolVersion.V1;
1025
+ } catch (v1Error) {
1026
+ throw new Error(
1027
+ `Failed to unpack CKBFS data with all versions: V3(${v3Error}), V2(${v2Error}), V1(${v1Error})`,
1028
+ );
1029
+ }
993
1030
  }
994
1031
  }
995
1032
 
@@ -1002,13 +1039,24 @@ export async function decodeFileFromChainByIdentifier(
1002
1039
  ckbfsData.indexes ||
1003
1040
  (ckbfsData.index !== undefined ? [ckbfsData.index] : []);
1004
1041
 
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
- });
1042
+ // Use direct witness decoding method for V1/V2, or appropriate method for V3
1043
+ let content: any;
1044
+ if (protocolVersion === ProtocolVersion.V3) {
1045
+ // For V3, we should use the V3-specific content retrieval which handles witness chains
1046
+ const fullContent = await getFileContentFromChainV3(client, outPoint, ckbfsData);
1047
+ content = {
1048
+ content: fullContent,
1049
+ size: fullContent.length,
1050
+ };
1051
+ } else {
1052
+ // For V1/V2, use the direct witness decoding method
1053
+ content = decodeFileFromWitnessData({
1054
+ witnesses: tx.witnesses,
1055
+ indexes: indexes,
1056
+ filename: ckbfsData.filename,
1057
+ contentType: ckbfsData.contentType,
1058
+ });
1059
+ }
1012
1060
 
1013
1061
  return {
1014
1062
  content: content.content,
@@ -1055,3 +1103,306 @@ export async function decodeFileFromChainByTypeId(
1055
1103
  }
1056
1104
  return null;
1057
1105
  }
1106
+
1107
+ /**
1108
+ * Retrieves complete file content from the blockchain for CKBFS v3
1109
+ * V3 stores backlinks in witnesses instead of cell data
1110
+ * @param client The CKB client to use for blockchain queries
1111
+ * @param outPoint The output point of the latest CKBFS v3 cell
1112
+ * @param ckbfsData The data from the latest CKBFS v3 cell
1113
+ * @returns Promise resolving to the complete file content
1114
+ */
1115
+ export async function getFileContentFromChainV3(
1116
+ client: any,
1117
+ outPoint: { txHash: string; index: number },
1118
+ ckbfsData: any,
1119
+ ): Promise<Uint8Array> {
1120
+ console.log(`Retrieving v3 file: ${safelyDecode(ckbfsData.filename)}`);
1121
+ console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
1122
+
1123
+ // Follow the CKBFS cell chain backwards to build the complete transaction sequence
1124
+ const transactionChain: Array<{
1125
+ txHash: string;
1126
+ cellIndex: number;
1127
+ witnessIndex: number;
1128
+ content: Uint8Array[];
1129
+ isPublish: boolean;
1130
+ }> = [];
1131
+
1132
+ let currentTxHash = outPoint.txHash;
1133
+ let currentCellIndex = outPoint.index;
1134
+ let currentWitnessIndex = ckbfsData.index;
1135
+
1136
+
1137
+
1138
+ // Follow the chain backwards to build the complete transaction sequence
1139
+ while (currentTxHash && currentWitnessIndex !== undefined) {
1140
+ const tx = await client.getTransaction(currentTxHash);
1141
+ if (!tx || !tx.transaction) {
1142
+ console.warn(`Transaction ${currentTxHash} not found`);
1143
+ break;
1144
+ }
1145
+
1146
+ if (currentWitnessIndex >= tx.transaction.witnesses.length) {
1147
+ console.warn(`Witness index ${currentWitnessIndex} out of range in transaction ${currentTxHash}`);
1148
+ break;
1149
+ }
1150
+
1151
+ const witnessHex = tx.transaction.witnesses[currentWitnessIndex];
1152
+ const witness = new Uint8Array(Buffer.from(witnessHex.slice(2), "hex"));
1153
+
1154
+ // Check if this is a v3 head witness
1155
+ if (isCKBFSV3Witness(witness)) {
1156
+ const witnessData = extractCKBFSV3WitnessContent(witness, true);
1157
+
1158
+ // Collect content from this transaction (head + continuation witnesses)
1159
+ const txContentPieces: Uint8Array[] = [witnessData.content];
1160
+
1161
+ // Follow continuation witnesses in this transaction
1162
+ let nextIndex = witnessData.nextIndex;
1163
+ while (nextIndex > 0 && nextIndex < tx.transaction.witnesses.length) {
1164
+ const nextWitnessHex = tx.transaction.witnesses[nextIndex];
1165
+ const nextWitness = new Uint8Array(Buffer.from(nextWitnessHex.slice(2), "hex"));
1166
+
1167
+ const nextWitnessData = extractCKBFSV3WitnessContent(nextWitness, false);
1168
+ txContentPieces.push(nextWitnessData.content);
1169
+
1170
+ nextIndex = nextWitnessData.nextIndex;
1171
+ }
1172
+
1173
+ // Check if this is a publish operation (all zeros for previous position)
1174
+ const isPublish = witnessData.previousTxHash === '0x' + '00'.repeat(32) &&
1175
+ witnessData.previousWitnessIndex === 0;
1176
+
1177
+ // Add this transaction's content to the beginning of the chain (since we're going backwards)
1178
+ transactionChain.unshift({
1179
+ txHash: currentTxHash,
1180
+ cellIndex: currentCellIndex,
1181
+ witnessIndex: currentWitnessIndex,
1182
+ content: txContentPieces,
1183
+ isPublish,
1184
+ });
1185
+
1186
+ // If this is the original publish operation, we're done
1187
+ if (isPublish) {
1188
+ break;
1189
+ }
1190
+
1191
+ // For append operations, we need to find the previous CKBFS cell
1192
+ const previousTxHash = witnessData.previousTxHash;
1193
+
1194
+ if (!previousTxHash || previousTxHash === '0x' + '00'.repeat(32)) {
1195
+ break;
1196
+ }
1197
+
1198
+ // Get the previous transaction to find the CKBFS cell and its witness index
1199
+ const prevTx = await client.getTransaction(previousTxHash);
1200
+ if (!prevTx || !prevTx.transaction) {
1201
+ console.warn(`Previous transaction ${previousTxHash} not found`);
1202
+ break;
1203
+ }
1204
+
1205
+ // Import Transaction class to parse the previous transaction
1206
+ const { Transaction } = await import("@ckb-ccc/core");
1207
+ const prevTxObj = Transaction.from(prevTx.transaction);
1208
+
1209
+ // Find the CKBFS cell in the previous transaction (it should have a type script)
1210
+ let prevCkbfsCellIndex = -1;
1211
+ let prevCkbfsData: any = null;
1212
+
1213
+ for (let i = 0; i < prevTxObj.outputs.length; i++) {
1214
+ const output = prevTxObj.outputs[i];
1215
+ const outputData = prevTxObj.outputsData[i];
1216
+
1217
+ // Check if this output has a type script (CKBFS cells have type scripts)
1218
+ if (output.type && outputData && outputData !== '0x') {
1219
+ try {
1220
+ // Try to parse as CKBFS data
1221
+ const { ccc } = await import("@ckb-ccc/core");
1222
+ const { CKBFSData } = await import("./molecule");
1223
+ const { ProtocolVersion } = await import("./constants");
1224
+
1225
+ const rawData = outputData.startsWith("0x")
1226
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
1227
+ : Buffer.from(outputData, "hex");
1228
+
1229
+ const ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
1230
+
1231
+ prevCkbfsCellIndex = i;
1232
+ prevCkbfsData = ckbfsData;
1233
+ break;
1234
+ } catch (error) {
1235
+ // Not a CKBFS cell, continue searching
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ if (prevCkbfsCellIndex === -1 || !prevCkbfsData) {
1241
+ console.warn(`No CKBFS cell found in previous transaction ${previousTxHash}`);
1242
+ break;
1243
+ }
1244
+
1245
+ const previousWitnessIndex = prevCkbfsData.index;
1246
+
1247
+ // Move to the previous CKBFS cell
1248
+ currentTxHash = previousTxHash;
1249
+ currentCellIndex = prevCkbfsCellIndex;
1250
+ currentWitnessIndex = previousWitnessIndex;
1251
+ } else {
1252
+ console.warn(`Witness at index ${currentWitnessIndex} in transaction ${currentTxHash} is not a valid CKBFS v3 witness`);
1253
+ break;
1254
+ }
1255
+ }
1256
+
1257
+ // Now assemble all content pieces in chronological order (first creation to latest)
1258
+ const allContentPieces: Uint8Array[] = [];
1259
+
1260
+ for (const txEntry of transactionChain) {
1261
+ allContentPieces.push(...txEntry.content);
1262
+ }
1263
+
1264
+ // Combine all content pieces
1265
+ return Buffer.concat(allContentPieces);
1266
+ }
1267
+
1268
+ /**
1269
+ * Retrieves complete file content from the blockchain using identifier for CKBFS v3
1270
+ * @param client The CKB client to use for blockchain queries
1271
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
1272
+ * @param options Optional configuration for network, version, and useTypeID
1273
+ * @returns Promise resolving to the complete file content and metadata
1274
+ */
1275
+ export async function getFileContentFromChainByIdentifierV3(
1276
+ client: any,
1277
+ identifier: string,
1278
+ options: {
1279
+ network?: "mainnet" | "testnet";
1280
+ version?: string;
1281
+ useTypeID?: boolean;
1282
+ } = {},
1283
+ ): Promise<{
1284
+ content: Uint8Array;
1285
+ filename: string;
1286
+ contentType: string;
1287
+ checksum: number;
1288
+ size: number;
1289
+ parsedId: ParsedIdentifier;
1290
+ } | null> {
1291
+ const {
1292
+ network = "testnet",
1293
+ version = ProtocolVersion.V3,
1294
+ useTypeID = false,
1295
+ } = options;
1296
+
1297
+ try {
1298
+ // Resolve the CKBFS cell using any supported identifier format
1299
+ const cellInfo = await resolveCKBFSCell(client, identifier, {
1300
+ network,
1301
+ version,
1302
+ useTypeID,
1303
+ });
1304
+
1305
+ if (!cellInfo) {
1306
+ console.warn(`CKBFS v3 cell with identifier ${identifier} not found`);
1307
+ return null;
1308
+ }
1309
+
1310
+ const { cell, transaction, outPoint, parsedId } = cellInfo;
1311
+
1312
+ // Import Transaction class dynamically
1313
+ const { Transaction } = await import("@ckb-ccc/core");
1314
+ const tx = Transaction.from(transaction);
1315
+
1316
+ // Get output data from the cell
1317
+ const outputIndex = outPoint.index;
1318
+ const outputData = tx.outputsData[outputIndex];
1319
+
1320
+ if (!outputData) {
1321
+ throw new Error(`Output data not found for cell at index ${outputIndex}`);
1322
+ }
1323
+
1324
+ // Import required modules dynamically
1325
+ const { ccc } = await import("@ckb-ccc/core");
1326
+ const { CKBFSData } = await import("./molecule");
1327
+
1328
+ // Parse the output data
1329
+ const rawData = outputData.startsWith("0x")
1330
+ ? ccc.bytesFrom(outputData.slice(2), "hex")
1331
+ : Buffer.from(outputData, "hex");
1332
+
1333
+ // Unpack CKBFS v3 data
1334
+ const ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V3);
1335
+
1336
+ console.log(`Found CKBFS v3 file: ${ckbfsData.filename}`);
1337
+ console.log(`Content type: ${ckbfsData.contentType}`);
1338
+
1339
+ // Use v3-specific function to get complete file content
1340
+ const content = await getFileContentFromChainV3(client, outPoint, ckbfsData);
1341
+
1342
+ return {
1343
+ content,
1344
+ filename: ckbfsData.filename,
1345
+ contentType: ckbfsData.contentType,
1346
+ checksum: ckbfsData.checksum,
1347
+ size: content.length,
1348
+ parsedId,
1349
+ };
1350
+ } catch (error) {
1351
+ console.error(`Error retrieving v3 file by identifier ${identifier}:`, error);
1352
+ throw error;
1353
+ }
1354
+ }
1355
+
1356
+ /**
1357
+ * Saves CKBFS v3 file content retrieved from blockchain by identifier to disk
1358
+ * @param client The CKB client to use for blockchain queries
1359
+ * @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
1360
+ * @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
1361
+ * @param options Optional configuration for network, version, and useTypeID
1362
+ * @returns Promise resolving to the path where the file was saved, or null if file not found
1363
+ */
1364
+ export async function saveFileFromChainByIdentifierV3(
1365
+ client: any,
1366
+ identifier: string,
1367
+ outputPath?: string,
1368
+ options: {
1369
+ network?: "mainnet" | "testnet";
1370
+ version?: string;
1371
+ useTypeID?: boolean;
1372
+ } = {},
1373
+ ): Promise<string | null> {
1374
+ try {
1375
+ // Get file content by identifier using v3 method
1376
+ const fileData = await getFileContentFromChainByIdentifierV3(
1377
+ client,
1378
+ identifier,
1379
+ { ...options, version: ProtocolVersion.V3 },
1380
+ );
1381
+
1382
+ if (!fileData) {
1383
+ console.warn(`CKBFS v3 file with identifier ${identifier} not found`);
1384
+ return null;
1385
+ }
1386
+
1387
+ // Determine output path
1388
+ const filePath = outputPath || fileData.filename;
1389
+
1390
+ // Ensure directory exists
1391
+ const directory = path.dirname(filePath);
1392
+ if (!fs.existsSync(directory)) {
1393
+ fs.mkdirSync(directory, { recursive: true });
1394
+ }
1395
+
1396
+ // Write file
1397
+ fs.writeFileSync(filePath, fileData.content);
1398
+ console.log(`CKBFS v3 file saved to: ${filePath}`);
1399
+ console.log(`Size: ${fileData.size} bytes`);
1400
+ console.log(`Content type: ${fileData.contentType}`);
1401
+ console.log(`Checksum: ${fileData.checksum}`);
1402
+
1403
+ return filePath;
1404
+ } catch (error) {
1405
+ console.error(`Error saving v3 file by identifier ${identifier}:`, error);
1406
+ throw error;
1407
+ }
1408
+ }