@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.
- package/README.md +31 -6
- package/RFC.v3.md +210 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +437 -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 -115
- package/dist/utils/transaction.js +45 -622
- 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/dist/utils/checksum.js
CHANGED
|
@@ -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
|
-
//
|
|
46
|
-
const
|
|
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: "
|
|
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;
|
package/dist/utils/constants.js
CHANGED
|
@@ -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 -
|
|
86
|
-
exports.DEFAULT_VERSION = exports.ProtocolVersion.
|
|
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
|
package/dist/utils/file.d.ts
CHANGED
|
@@ -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>;
|
package/dist/utils/file.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 (
|
|
640
|
+
catch (v3Error) {
|
|
618
641
|
try {
|
|
619
|
-
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.
|
|
620
|
-
protocolVersion =
|
|
642
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
|
643
|
+
protocolVersion = ProtocolVersion.V2;
|
|
621
644
|
}
|
|
622
|
-
catch (
|
|
623
|
-
|
|
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
|
|
630
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 (
|
|
791
|
+
catch (v3Error) {
|
|
751
792
|
try {
|
|
752
|
-
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.
|
|
753
|
-
protocolVersion =
|
|
793
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
|
794
|
+
protocolVersion = ProtocolVersion.V2;
|
|
754
795
|
}
|
|
755
|
-
catch (
|
|
756
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
}
|