@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.
- package/README.md +31 -6
- package/RFC.v3.md +210 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +440 -75
- package/dist/utils/checksum.d.ts +16 -0
- package/dist/utils/checksum.js +74 -8
- package/dist/utils/constants.d.ts +2 -1
- package/dist/utils/constants.js +12 -2
- package/dist/utils/file.d.ts +44 -0
- package/dist/utils/file.js +303 -30
- package/dist/utils/molecule.d.ts +13 -1
- package/dist/utils/molecule.js +32 -5
- package/dist/utils/transaction-backup.d.ts +117 -0
- package/dist/utils/transaction-backup.js +624 -0
- package/dist/utils/transaction.d.ts +7 -105
- package/dist/utils/transaction.js +45 -565
- package/dist/utils/transactions/index.d.ts +8 -0
- package/dist/utils/transactions/index.js +31 -0
- package/dist/utils/transactions/shared.d.ts +57 -0
- package/dist/utils/transactions/shared.js +17 -0
- package/dist/utils/transactions/v1v2.d.ts +80 -0
- package/dist/utils/transactions/v1v2.js +592 -0
- package/dist/utils/transactions/v3.d.ts +124 -0
- package/dist/utils/transactions/v3.js +369 -0
- package/dist/utils/witness.d.ts +45 -0
- package/dist/utils/witness.js +145 -3
- package/examples/append-v3.ts +310 -0
- package/examples/chunked-publish.ts +307 -0
- package/examples/publish-v3.ts +152 -0
- package/examples/publish.ts +4 -4
- package/examples/retrieve-v3.ts +222 -0
- package/package.json +6 -2
- package/small-example.txt +1 -0
- package/src/index.ts +568 -87
- package/src/utils/checksum.ts +90 -9
- package/src/utils/constants.ts +19 -2
- package/src/utils/file.ts +386 -35
- package/src/utils/molecule.ts +43 -6
- package/src/utils/transaction-backup.ts +849 -0
- package/src/utils/transaction.ts +39 -848
- package/src/utils/transactions/index.ts +16 -0
- package/src/utils/transactions/shared.ts +64 -0
- package/src/utils/transactions/v1v2.ts +791 -0
- package/src/utils/transactions/v3.ts +564 -0
- package/src/utils/witness.ts +193 -0
package/src/utils/checksum.ts
CHANGED
|
@@ -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
|
-
//
|
|
48
|
-
const
|
|
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
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -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 -
|
|
118
|
-
export const DEFAULT_VERSION = ProtocolVersion.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
766
|
-
|
|
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.
|
|
769
|
-
protocolVersion =
|
|
770
|
-
} catch (
|
|
771
|
-
|
|
772
|
-
|
|
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
|
|
782
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
985
|
-
|
|
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.
|
|
988
|
-
protocolVersion =
|
|
989
|
-
} catch (
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
}
|