@ckbfs/api 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +506 -74
- package/code.png +0 -0
- package/demo-output.txt +1 -0
- package/direct_direct_content_example.txt +1 -0
- package/dist/index.d.ts +55 -13
- package/dist/index.js +143 -18
- package/dist/utils/constants.d.ts +7 -3
- package/dist/utils/constants.js +41 -34
- package/dist/utils/file.d.ts +179 -0
- package/dist/utils/file.js +599 -31
- package/dist/utils/molecule.d.ts +3 -2
- package/dist/utils/molecule.js +22 -24
- package/dist/utils/transaction.d.ts +10 -4
- package/dist/utils/transaction.js +34 -32
- package/examples/example.txt +1 -0
- package/examples/identifier-test.ts +178 -0
- package/examples/index.ts +36 -24
- package/examples/publish.ts +39 -5
- package/examples/retrieve.ts +542 -77
- package/examples/witness-decode-demo.ts +190 -0
- package/identifier_direct_content_example.txt +1 -0
- package/package-lock.json +4978 -0
- package/package.json +3 -2
- package/src/index.ts +312 -96
- package/src/utils/constants.ts +77 -43
- package/src/utils/file.ts +864 -59
- package/src/utils/molecule.ts +41 -36
- package/src/utils/transaction.ts +177 -151
- package/traditional_direct_content_example.txt +1 -0
- package/typeid_direct_content_example.txt +1 -0
- package/.cursor/rules/typescript.mdc +0 -49
- package/ABC.LOGS +0 -1
- package/RFC.v2.md +0 -341
- package/append.txt +0 -1
- package/example.txt +0 -1
- package/publish-tx-hash-v1.txt +0 -1
- package/src/utils/createPublishTransaction +0 -24
- package/test-download.txt +0 -2
package/dist/utils/file.js
CHANGED
@@ -1,8 +1,42 @@
|
|
1
1
|
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
37
|
};
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
39
|
+
exports.IdentifierType = void 0;
|
6
40
|
exports.readFile = readFile;
|
7
41
|
exports.readFileAsText = readFileAsText;
|
8
42
|
exports.readFileAsUint8Array = readFileAsUint8Array;
|
@@ -12,6 +46,18 @@ exports.splitFileIntoChunks = splitFileIntoChunks;
|
|
12
46
|
exports.combineChunksToFile = combineChunksToFile;
|
13
47
|
exports.getFileContentFromChain = getFileContentFromChain;
|
14
48
|
exports.saveFileFromChain = saveFileFromChain;
|
49
|
+
exports.decodeWitnessContent = decodeWitnessContent;
|
50
|
+
exports.decodeMultipleWitnessContents = decodeMultipleWitnessContents;
|
51
|
+
exports.extractFileFromWitnesses = extractFileFromWitnesses;
|
52
|
+
exports.decodeFileFromWitnessData = decodeFileFromWitnessData;
|
53
|
+
exports.saveFileFromWitnessData = saveFileFromWitnessData;
|
54
|
+
exports.parseIdentifier = parseIdentifier;
|
55
|
+
exports.getFileContentFromChainByIdentifier = getFileContentFromChainByIdentifier;
|
56
|
+
exports.getFileContentFromChainByTypeId = getFileContentFromChainByTypeId;
|
57
|
+
exports.saveFileFromChainByIdentifier = saveFileFromChainByIdentifier;
|
58
|
+
exports.saveFileFromChainByTypeId = saveFileFromChainByTypeId;
|
59
|
+
exports.decodeFileFromChainByIdentifier = decodeFileFromChainByIdentifier;
|
60
|
+
exports.decodeFileFromChainByTypeId = decodeFileFromChainByTypeId;
|
15
61
|
const fs_1 = __importDefault(require("fs"));
|
16
62
|
const path_1 = __importDefault(require("path"));
|
17
63
|
/**
|
@@ -31,7 +77,7 @@ function readFile(filePath) {
|
|
31
77
|
* @returns String containing the file contents
|
32
78
|
*/
|
33
79
|
function readFileAsText(filePath) {
|
34
|
-
return fs_1.default.readFileSync(filePath,
|
80
|
+
return fs_1.default.readFileSync(filePath, "utf-8");
|
35
81
|
}
|
36
82
|
/**
|
37
83
|
* Reads a file as Uint8Array from the file system
|
@@ -63,27 +109,27 @@ function writeFile(filePath, data) {
|
|
63
109
|
function getContentType(filePath) {
|
64
110
|
const extension = path_1.default.extname(filePath).toLowerCase();
|
65
111
|
const mimeTypes = {
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
112
|
+
".txt": "text/plain",
|
113
|
+
".html": "text/html",
|
114
|
+
".htm": "text/html",
|
115
|
+
".css": "text/css",
|
116
|
+
".js": "application/javascript",
|
117
|
+
".json": "application/json",
|
118
|
+
".jpg": "image/jpeg",
|
119
|
+
".jpeg": "image/jpeg",
|
120
|
+
".png": "image/png",
|
121
|
+
".gif": "image/gif",
|
122
|
+
".svg": "image/svg+xml",
|
123
|
+
".pdf": "application/pdf",
|
124
|
+
".mp3": "audio/mpeg",
|
125
|
+
".mp4": "video/mp4",
|
126
|
+
".wav": "audio/wav",
|
127
|
+
".xml": "application/xml",
|
128
|
+
".zip": "application/zip",
|
129
|
+
".md": "text/markdown",
|
130
|
+
".markdown": "text/markdown",
|
85
131
|
};
|
86
|
-
return mimeTypes[extension] ||
|
132
|
+
return mimeTypes[extension] || "application/octet-stream";
|
87
133
|
}
|
88
134
|
/**
|
89
135
|
* Splits a file into chunks of a specific size
|
@@ -105,7 +151,7 @@ function splitFileIntoChunks(filePath, chunkSize) {
|
|
105
151
|
* @param outputPath The path to write the combined file to
|
106
152
|
*/
|
107
153
|
function combineChunksToFile(chunks, outputPath) {
|
108
|
-
const combinedBuffer = Buffer.concat(chunks.map(chunk => Buffer.from(chunk)));
|
154
|
+
const combinedBuffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
109
155
|
writeFile(outputPath, combinedBuffer);
|
110
156
|
}
|
111
157
|
/**
|
@@ -115,12 +161,12 @@ function combineChunksToFile(chunks, outputPath) {
|
|
115
161
|
*/
|
116
162
|
function safelyDecode(buffer) {
|
117
163
|
if (!buffer)
|
118
|
-
return
|
164
|
+
return "[Unknown]";
|
119
165
|
try {
|
120
166
|
if (buffer instanceof Uint8Array) {
|
121
167
|
return new TextDecoder().decode(buffer);
|
122
168
|
}
|
123
|
-
else if (typeof buffer ===
|
169
|
+
else if (typeof buffer === "string") {
|
124
170
|
return buffer;
|
125
171
|
}
|
126
172
|
else {
|
@@ -128,7 +174,7 @@ function safelyDecode(buffer) {
|
|
128
174
|
}
|
129
175
|
}
|
130
176
|
catch (e) {
|
131
|
-
return
|
177
|
+
return "[Decode Error]";
|
132
178
|
}
|
133
179
|
}
|
134
180
|
/**
|
@@ -151,7 +197,8 @@ async function getFileContentFromChain(client, outPoint, ckbfsData) {
|
|
151
197
|
throw new Error(`Transaction ${currentOutPoint.txHash} not found`);
|
152
198
|
}
|
153
199
|
// Get content from witnesses
|
154
|
-
const indexes = currentData.indexes ||
|
200
|
+
const indexes = currentData.indexes ||
|
201
|
+
(currentData.index !== undefined ? [currentData.index] : []);
|
155
202
|
if (indexes.length > 0) {
|
156
203
|
// Get content from each witness index
|
157
204
|
for (const idx of indexes) {
|
@@ -160,9 +207,9 @@ async function getFileContentFromChain(client, outPoint, ckbfsData) {
|
|
160
207
|
continue;
|
161
208
|
}
|
162
209
|
const witnessHex = tx.transaction.witnesses[idx];
|
163
|
-
const witness = Buffer.from(witnessHex.slice(2),
|
210
|
+
const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
|
164
211
|
// Extract content (skip CKBFS header + version byte)
|
165
|
-
if (witness.length >= 6 && witness.slice(0, 5).toString() ===
|
212
|
+
if (witness.length >= 6 && witness.slice(0, 5).toString() === "CKBFS") {
|
166
213
|
const content = witness.slice(6);
|
167
214
|
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
168
215
|
}
|
@@ -183,7 +230,8 @@ async function getFileContentFromChain(client, outPoint, ckbfsData) {
|
|
183
230
|
continue;
|
184
231
|
}
|
185
232
|
// Get content from backlink witnesses
|
186
|
-
const backIndexes = backlink.indexes ||
|
233
|
+
const backIndexes = backlink.indexes ||
|
234
|
+
(backlink.index !== undefined ? [backlink.index] : []);
|
187
235
|
if (backIndexes.length > 0) {
|
188
236
|
// Get content from each witness index
|
189
237
|
for (const idx of backIndexes) {
|
@@ -192,9 +240,10 @@ async function getFileContentFromChain(client, outPoint, ckbfsData) {
|
|
192
240
|
continue;
|
193
241
|
}
|
194
242
|
const witnessHex = backTx.transaction.witnesses[idx];
|
195
|
-
const witness = Buffer.from(witnessHex.slice(2),
|
243
|
+
const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
|
196
244
|
// Extract content (skip CKBFS header + version byte)
|
197
|
-
if (witness.length >= 6 &&
|
245
|
+
if (witness.length >= 6 &&
|
246
|
+
witness.slice(0, 5).toString() === "CKBFS") {
|
198
247
|
const content = witness.slice(6);
|
199
248
|
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
200
249
|
}
|
@@ -231,3 +280,522 @@ function saveFileFromChain(content, ckbfsData, outputPath) {
|
|
231
280
|
console.log(`Size: ${content.length} bytes`);
|
232
281
|
return filePath;
|
233
282
|
}
|
283
|
+
/**
|
284
|
+
* Decodes content from a single CKBFS witness
|
285
|
+
* @param witnessHex The witness data in hex format (with or without 0x prefix)
|
286
|
+
* @returns Object containing the decoded content and metadata, or null if not a valid CKBFS witness
|
287
|
+
*/
|
288
|
+
function decodeWitnessContent(witnessHex) {
|
289
|
+
try {
|
290
|
+
// Remove 0x prefix if present
|
291
|
+
const hexData = witnessHex.startsWith("0x")
|
292
|
+
? witnessHex.slice(2)
|
293
|
+
: witnessHex;
|
294
|
+
const witness = Buffer.from(hexData, "hex");
|
295
|
+
// Check if it's a valid CKBFS witness
|
296
|
+
if (witness.length < 6) {
|
297
|
+
return null;
|
298
|
+
}
|
299
|
+
// Check CKBFS header
|
300
|
+
const header = witness.slice(0, 5).toString();
|
301
|
+
if (header !== "CKBFS") {
|
302
|
+
return null;
|
303
|
+
}
|
304
|
+
// Extract content (skip CKBFS header + version byte)
|
305
|
+
const content = witness.slice(6);
|
306
|
+
return {
|
307
|
+
content,
|
308
|
+
isValid: true,
|
309
|
+
};
|
310
|
+
}
|
311
|
+
catch (error) {
|
312
|
+
console.warn("Error decoding witness content:", error);
|
313
|
+
return null;
|
314
|
+
}
|
315
|
+
}
|
316
|
+
/**
|
317
|
+
* Decodes and combines content from multiple CKBFS witnesses
|
318
|
+
* @param witnessHexArray Array of witness data in hex format
|
319
|
+
* @param preserveOrder Whether to preserve the order of witnesses (default: true)
|
320
|
+
* @returns Combined content from all valid CKBFS witnesses
|
321
|
+
*/
|
322
|
+
function decodeMultipleWitnessContents(witnessHexArray, preserveOrder = true) {
|
323
|
+
const contentPieces = [];
|
324
|
+
for (let i = 0; i < witnessHexArray.length; i++) {
|
325
|
+
const witnessHex = witnessHexArray[i];
|
326
|
+
const decoded = decodeWitnessContent(witnessHex);
|
327
|
+
if (decoded && decoded.isValid) {
|
328
|
+
if (preserveOrder) {
|
329
|
+
contentPieces.push(decoded.content);
|
330
|
+
}
|
331
|
+
else {
|
332
|
+
contentPieces.unshift(decoded.content);
|
333
|
+
}
|
334
|
+
}
|
335
|
+
else {
|
336
|
+
console.warn(`Witness at index ${i} is not a valid CKBFS witness`);
|
337
|
+
}
|
338
|
+
}
|
339
|
+
return Buffer.concat(contentPieces);
|
340
|
+
}
|
341
|
+
/**
|
342
|
+
* Extracts complete file content from witnesses using specified indexes
|
343
|
+
* @param witnesses Array of all witnesses from a transaction
|
344
|
+
* @param indexes Array of witness indexes that contain CKBFS content
|
345
|
+
* @returns Combined content from the specified witness indexes
|
346
|
+
*/
|
347
|
+
function extractFileFromWitnesses(witnesses, indexes) {
|
348
|
+
const relevantWitnesses = [];
|
349
|
+
for (const idx of indexes) {
|
350
|
+
if (idx >= witnesses.length) {
|
351
|
+
console.warn(`Witness index ${idx} out of range (total witnesses: ${witnesses.length})`);
|
352
|
+
continue;
|
353
|
+
}
|
354
|
+
relevantWitnesses.push(witnesses[idx]);
|
355
|
+
}
|
356
|
+
return decodeMultipleWitnessContents(relevantWitnesses, true);
|
357
|
+
}
|
358
|
+
/**
|
359
|
+
* Decodes file content directly from witness data without blockchain queries
|
360
|
+
* @param witnessData Object containing witness information
|
361
|
+
* @returns Object containing the decoded file content and metadata
|
362
|
+
*/
|
363
|
+
function decodeFileFromWitnessData(witnessData) {
|
364
|
+
const { witnesses, indexes, filename, contentType } = witnessData;
|
365
|
+
// Normalize indexes to array
|
366
|
+
const indexArray = Array.isArray(indexes) ? indexes : [indexes];
|
367
|
+
// Extract content from witnesses
|
368
|
+
const content = extractFileFromWitnesses(witnesses, indexArray);
|
369
|
+
return {
|
370
|
+
content,
|
371
|
+
filename,
|
372
|
+
contentType,
|
373
|
+
size: content.length,
|
374
|
+
};
|
375
|
+
}
|
376
|
+
/**
|
377
|
+
* Saves decoded file content directly from witness data
|
378
|
+
* @param witnessData Object containing witness information
|
379
|
+
* @param outputPath Optional path to save the file
|
380
|
+
* @returns The path where the file was saved
|
381
|
+
*/
|
382
|
+
function saveFileFromWitnessData(witnessData, outputPath) {
|
383
|
+
const decoded = decodeFileFromWitnessData(witnessData);
|
384
|
+
// Determine output path
|
385
|
+
const filename = decoded.filename || "decoded_file";
|
386
|
+
const filePath = outputPath || filename;
|
387
|
+
// Ensure directory exists
|
388
|
+
const directory = path_1.default.dirname(filePath);
|
389
|
+
if (!fs_1.default.existsSync(directory)) {
|
390
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
391
|
+
}
|
392
|
+
// Write file
|
393
|
+
fs_1.default.writeFileSync(filePath, decoded.content);
|
394
|
+
console.log(`File saved to: ${filePath}`);
|
395
|
+
console.log(`Size: ${decoded.size} bytes`);
|
396
|
+
console.log(`Content type: ${decoded.contentType || "unknown"}`);
|
397
|
+
return filePath;
|
398
|
+
}
|
399
|
+
/**
|
400
|
+
* Identifier types for CKBFS cells
|
401
|
+
*/
|
402
|
+
var IdentifierType;
|
403
|
+
(function (IdentifierType) {
|
404
|
+
IdentifierType["TypeID"] = "typeId";
|
405
|
+
IdentifierType["OutPoint"] = "outPoint";
|
406
|
+
IdentifierType["Unknown"] = "unknown";
|
407
|
+
})(IdentifierType || (exports.IdentifierType = IdentifierType = {}));
|
408
|
+
/**
|
409
|
+
* Detects and parses different CKBFS identifier formats
|
410
|
+
* @param identifier The identifier string to parse
|
411
|
+
* @returns Parsed identifier information
|
412
|
+
*/
|
413
|
+
function parseIdentifier(identifier) {
|
414
|
+
const trimmed = identifier.trim();
|
415
|
+
// Type 1: Pure TypeID hex string (with or without 0x prefix)
|
416
|
+
if (trimmed.match(/^(0x)?[a-fA-F0-9]{64}$/)) {
|
417
|
+
const typeId = trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
|
418
|
+
return {
|
419
|
+
type: IdentifierType.TypeID,
|
420
|
+
typeId,
|
421
|
+
original: identifier,
|
422
|
+
};
|
423
|
+
}
|
424
|
+
// Type 2: CKBFS URI with TypeID
|
425
|
+
if (trimmed.startsWith("ckbfs://")) {
|
426
|
+
const content = trimmed.slice(8); // Remove "ckbfs://"
|
427
|
+
// Check if it's TypeID format (64 hex characters)
|
428
|
+
if (content.match(/^[a-fA-F0-9]{64}$/)) {
|
429
|
+
return {
|
430
|
+
type: IdentifierType.TypeID,
|
431
|
+
typeId: `0x${content}`,
|
432
|
+
original: identifier,
|
433
|
+
};
|
434
|
+
}
|
435
|
+
// Type 3: CKBFS URI with transaction hash and index (txhash + 'i' + index)
|
436
|
+
const outPointMatch = content.match(/^([a-fA-F0-9]{64})i(\d+)$/);
|
437
|
+
if (outPointMatch) {
|
438
|
+
const [, txHash, indexStr] = outPointMatch;
|
439
|
+
return {
|
440
|
+
type: IdentifierType.OutPoint,
|
441
|
+
txHash: `0x${txHash}`,
|
442
|
+
index: parseInt(indexStr, 10),
|
443
|
+
original: identifier,
|
444
|
+
};
|
445
|
+
}
|
446
|
+
}
|
447
|
+
// Unknown format
|
448
|
+
return {
|
449
|
+
type: IdentifierType.Unknown,
|
450
|
+
original: identifier,
|
451
|
+
};
|
452
|
+
}
|
453
|
+
/**
|
454
|
+
* Resolves a CKBFS cell using any supported identifier format
|
455
|
+
* @param client The CKB client to use for blockchain queries
|
456
|
+
* @param identifier The identifier (TypeID, CKBFS URI, or outPoint URI)
|
457
|
+
* @param options Optional configuration for network, version, and useTypeID
|
458
|
+
* @returns Promise resolving to the found cell and transaction info, or null if not found
|
459
|
+
*/
|
460
|
+
async function resolveCKBFSCell(client, identifier, options = {}) {
|
461
|
+
const { network = "testnet", version = "20241025.db973a8e8032", useTypeID = false, } = options;
|
462
|
+
const parsedId = parseIdentifier(identifier);
|
463
|
+
try {
|
464
|
+
if (parsedId.type === IdentifierType.TypeID && parsedId.typeId) {
|
465
|
+
// Use existing TypeID resolution logic
|
466
|
+
const cellInfo = await findCKBFSCellByTypeId(client, parsedId.typeId, network, version, useTypeID);
|
467
|
+
if (cellInfo) {
|
468
|
+
return {
|
469
|
+
...cellInfo,
|
470
|
+
parsedId,
|
471
|
+
};
|
472
|
+
}
|
473
|
+
}
|
474
|
+
else if (parsedId.type === IdentifierType.OutPoint &&
|
475
|
+
parsedId.txHash &&
|
476
|
+
parsedId.index !== undefined) {
|
477
|
+
// Resolve using transaction hash and index
|
478
|
+
const txWithStatus = await client.getTransaction(parsedId.txHash);
|
479
|
+
if (!txWithStatus || !txWithStatus.transaction) {
|
480
|
+
console.warn(`Transaction ${parsedId.txHash} not found`);
|
481
|
+
return null;
|
482
|
+
}
|
483
|
+
// Import Transaction class dynamically
|
484
|
+
const { Transaction } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
|
485
|
+
const tx = Transaction.from(txWithStatus.transaction);
|
486
|
+
// Check if the index is valid
|
487
|
+
if (parsedId.index >= tx.outputs.length) {
|
488
|
+
console.warn(`Output index ${parsedId.index} out of range for transaction ${parsedId.txHash}`);
|
489
|
+
return null;
|
490
|
+
}
|
491
|
+
const output = tx.outputs[parsedId.index];
|
492
|
+
// Verify it's a CKBFS cell by checking if it has a type script
|
493
|
+
if (!output.type) {
|
494
|
+
console.warn(`Output at index ${parsedId.index} is not a CKBFS cell (no type script)`);
|
495
|
+
return null;
|
496
|
+
}
|
497
|
+
// Create a mock cell object similar to what findSingletonCellByType returns
|
498
|
+
const cell = {
|
499
|
+
outPoint: {
|
500
|
+
txHash: parsedId.txHash,
|
501
|
+
index: parsedId.index,
|
502
|
+
},
|
503
|
+
output,
|
504
|
+
};
|
505
|
+
return {
|
506
|
+
cell,
|
507
|
+
transaction: txWithStatus.transaction,
|
508
|
+
outPoint: {
|
509
|
+
txHash: parsedId.txHash,
|
510
|
+
index: parsedId.index,
|
511
|
+
},
|
512
|
+
parsedId,
|
513
|
+
};
|
514
|
+
}
|
515
|
+
console.warn(`Unable to resolve identifier: ${identifier} (type: ${parsedId.type})`);
|
516
|
+
return null;
|
517
|
+
}
|
518
|
+
catch (error) {
|
519
|
+
console.error(`Error resolving CKBFS cell for identifier ${identifier}:`, error);
|
520
|
+
return null;
|
521
|
+
}
|
522
|
+
}
|
523
|
+
/**
|
524
|
+
* Finds a CKBFS cell by TypeID
|
525
|
+
* @param client The CKB client to use for blockchain queries
|
526
|
+
* @param typeId The TypeID (args) of the CKBFS cell to find
|
527
|
+
* @param network The network type (mainnet or testnet)
|
528
|
+
* @param version The protocol version to use
|
529
|
+
* @param useTypeID Whether to use type ID instead of code hash for script matching
|
530
|
+
* @returns Promise resolving to the found cell and transaction info, or null if not found
|
531
|
+
*/
|
532
|
+
async function findCKBFSCellByTypeId(client, typeId, network = "testnet", version = "20241025.db973a8e8032", useTypeID = false) {
|
533
|
+
try {
|
534
|
+
// Import constants dynamically to avoid circular dependencies
|
535
|
+
const { getCKBFSScriptConfig, NetworkType, ProtocolVersion } = await Promise.resolve().then(() => __importStar(require("./constants")));
|
536
|
+
// Get CKBFS script config
|
537
|
+
const networkType = network === "mainnet" ? NetworkType.Mainnet : NetworkType.Testnet;
|
538
|
+
const protocolVersion = version === "20240906.ce6724722cf6"
|
539
|
+
? ProtocolVersion.V1
|
540
|
+
: ProtocolVersion.V2;
|
541
|
+
const config = getCKBFSScriptConfig(networkType, protocolVersion, useTypeID);
|
542
|
+
// Create the script to search for
|
543
|
+
const script = {
|
544
|
+
codeHash: config.codeHash,
|
545
|
+
hashType: config.hashType,
|
546
|
+
args: typeId.startsWith("0x") ? typeId : `0x${typeId}`,
|
547
|
+
};
|
548
|
+
// Find the cell by type script
|
549
|
+
const cell = await client.findSingletonCellByType(script, true);
|
550
|
+
if (!cell) {
|
551
|
+
return null;
|
552
|
+
}
|
553
|
+
// Get the transaction that contains this cell
|
554
|
+
const txHash = cell.outPoint.txHash;
|
555
|
+
const txWithStatus = await client.getTransaction(txHash);
|
556
|
+
if (!txWithStatus || !txWithStatus.transaction) {
|
557
|
+
throw new Error(`Transaction ${txHash} not found`);
|
558
|
+
}
|
559
|
+
return {
|
560
|
+
cell,
|
561
|
+
transaction: txWithStatus.transaction,
|
562
|
+
outPoint: {
|
563
|
+
txHash: cell.outPoint.txHash,
|
564
|
+
index: cell.outPoint.index,
|
565
|
+
},
|
566
|
+
};
|
567
|
+
}
|
568
|
+
catch (error) {
|
569
|
+
console.warn(`Error finding CKBFS cell by TypeID ${typeId}:`, error);
|
570
|
+
return null;
|
571
|
+
}
|
572
|
+
}
|
573
|
+
/**
|
574
|
+
* Retrieves complete file content from the blockchain using any supported identifier
|
575
|
+
* @param client The CKB client to use for blockchain queries
|
576
|
+
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
|
577
|
+
* @param options Optional configuration for network, version, and useTypeID
|
578
|
+
* @returns Promise resolving to the complete file content and metadata
|
579
|
+
*/
|
580
|
+
async function getFileContentFromChainByIdentifier(client, identifier, options = {}) {
|
581
|
+
const { network = "testnet", version = "20241025.db973a8e8032", useTypeID = false, } = options;
|
582
|
+
try {
|
583
|
+
// Resolve the CKBFS cell using any supported identifier format
|
584
|
+
const cellInfo = await resolveCKBFSCell(client, identifier, {
|
585
|
+
network,
|
586
|
+
version,
|
587
|
+
useTypeID,
|
588
|
+
});
|
589
|
+
if (!cellInfo) {
|
590
|
+
console.warn(`CKBFS cell with identifier ${identifier} not found`);
|
591
|
+
return null;
|
592
|
+
}
|
593
|
+
const { cell, transaction, outPoint, parsedId } = cellInfo;
|
594
|
+
// Import Transaction class dynamically
|
595
|
+
const { Transaction } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
|
596
|
+
const tx = Transaction.from(transaction);
|
597
|
+
// Get output data from the cell
|
598
|
+
const outputIndex = outPoint.index;
|
599
|
+
const outputData = tx.outputsData[outputIndex];
|
600
|
+
if (!outputData) {
|
601
|
+
throw new Error(`Output data not found for cell at index ${outputIndex}`);
|
602
|
+
}
|
603
|
+
// Import required modules dynamically
|
604
|
+
const { ccc } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
|
605
|
+
const { CKBFSData } = await Promise.resolve().then(() => __importStar(require("./molecule")));
|
606
|
+
const { ProtocolVersion } = await Promise.resolve().then(() => __importStar(require("./constants")));
|
607
|
+
// Parse the output data
|
608
|
+
const rawData = outputData.startsWith("0x")
|
609
|
+
? ccc.bytesFrom(outputData.slice(2), "hex")
|
610
|
+
: Buffer.from(outputData, "hex");
|
611
|
+
// Try to unpack CKBFS data with both protocol versions
|
612
|
+
let ckbfsData;
|
613
|
+
let protocolVersion = version;
|
614
|
+
try {
|
615
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
616
|
+
}
|
617
|
+
catch (error) {
|
618
|
+
try {
|
619
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
|
620
|
+
protocolVersion = "20240906.ce6724722cf6";
|
621
|
+
}
|
622
|
+
catch (v1Error) {
|
623
|
+
throw new Error(`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`);
|
624
|
+
}
|
625
|
+
}
|
626
|
+
console.log(`Found CKBFS file: ${ckbfsData.filename}`);
|
627
|
+
console.log(`Content type: ${ckbfsData.contentType}`);
|
628
|
+
console.log(`Protocol version: ${protocolVersion}`);
|
629
|
+
// Use existing function to get complete file content
|
630
|
+
const content = await getFileContentFromChain(client, outPoint, ckbfsData);
|
631
|
+
return {
|
632
|
+
content,
|
633
|
+
filename: ckbfsData.filename,
|
634
|
+
contentType: ckbfsData.contentType,
|
635
|
+
checksum: ckbfsData.checksum,
|
636
|
+
size: content.length,
|
637
|
+
backLinks: ckbfsData.backLinks || [],
|
638
|
+
parsedId,
|
639
|
+
};
|
640
|
+
}
|
641
|
+
catch (error) {
|
642
|
+
console.error(`Error retrieving file by identifier ${identifier}:`, error);
|
643
|
+
throw error;
|
644
|
+
}
|
645
|
+
}
|
646
|
+
/**
|
647
|
+
* Retrieves complete file content from the blockchain using TypeID (legacy function)
|
648
|
+
* @param client The CKB client to use for blockchain queries
|
649
|
+
* @param typeId The TypeID (args) of the CKBFS cell
|
650
|
+
* @param options Optional configuration for network, version, and useTypeID
|
651
|
+
* @returns Promise resolving to the complete file content and metadata
|
652
|
+
*/
|
653
|
+
async function getFileContentFromChainByTypeId(client, typeId, options = {}) {
|
654
|
+
const result = await getFileContentFromChainByIdentifier(client, typeId, options);
|
655
|
+
if (result) {
|
656
|
+
const { parsedId, ...fileData } = result;
|
657
|
+
return fileData;
|
658
|
+
}
|
659
|
+
return null;
|
660
|
+
}
|
661
|
+
/**
|
662
|
+
* Saves file content retrieved from blockchain by identifier to disk
|
663
|
+
* @param client The CKB client to use for blockchain queries
|
664
|
+
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
|
665
|
+
* @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
|
666
|
+
* @param options Optional configuration for network, version, and useTypeID
|
667
|
+
* @returns Promise resolving to the path where the file was saved, or null if file not found
|
668
|
+
*/
|
669
|
+
async function saveFileFromChainByIdentifier(client, identifier, outputPath, options = {}) {
|
670
|
+
try {
|
671
|
+
// Get file content by identifier
|
672
|
+
const fileData = await getFileContentFromChainByIdentifier(client, identifier, options);
|
673
|
+
if (!fileData) {
|
674
|
+
console.warn(`File with identifier ${identifier} not found`);
|
675
|
+
return null;
|
676
|
+
}
|
677
|
+
// Determine output path
|
678
|
+
const filePath = outputPath || fileData.filename;
|
679
|
+
// Ensure directory exists
|
680
|
+
const directory = path_1.default.dirname(filePath);
|
681
|
+
if (!fs_1.default.existsSync(directory)) {
|
682
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
683
|
+
}
|
684
|
+
// Write file
|
685
|
+
fs_1.default.writeFileSync(filePath, fileData.content);
|
686
|
+
console.log(`File saved to: ${filePath}`);
|
687
|
+
console.log(`Size: ${fileData.size} bytes`);
|
688
|
+
console.log(`Content type: ${fileData.contentType}`);
|
689
|
+
console.log(`Checksum: ${fileData.checksum}`);
|
690
|
+
return filePath;
|
691
|
+
}
|
692
|
+
catch (error) {
|
693
|
+
console.error(`Error saving file by identifier ${identifier}:`, error);
|
694
|
+
throw error;
|
695
|
+
}
|
696
|
+
}
|
697
|
+
/**
|
698
|
+
* Saves file content retrieved from blockchain by TypeID to disk (legacy function)
|
699
|
+
* @param client The CKB client to use for blockchain queries
|
700
|
+
* @param typeId The TypeID (args) of the CKBFS cell
|
701
|
+
* @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
|
702
|
+
* @param options Optional configuration for network, version, and useTypeID
|
703
|
+
* @returns Promise resolving to the path where the file was saved, or null if file not found
|
704
|
+
*/
|
705
|
+
async function saveFileFromChainByTypeId(client, typeId, outputPath, options = {}) {
|
706
|
+
return await saveFileFromChainByIdentifier(client, typeId, outputPath, options);
|
707
|
+
}
|
708
|
+
/**
|
709
|
+
* Decodes file content directly from identifier using witness decoding (new method)
|
710
|
+
* @param client The CKB client to use for blockchain queries
|
711
|
+
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
|
712
|
+
* @param options Optional configuration for network, version, and useTypeID
|
713
|
+
* @returns Promise resolving to the decoded file content and metadata, or null if not found
|
714
|
+
*/
|
715
|
+
async function decodeFileFromChainByIdentifier(client, identifier, options = {}) {
|
716
|
+
const { network = "testnet", version = "20241025.db973a8e8032", useTypeID = false, } = options;
|
717
|
+
try {
|
718
|
+
// Resolve the CKBFS cell using any supported identifier format
|
719
|
+
const cellInfo = await resolveCKBFSCell(client, identifier, {
|
720
|
+
network,
|
721
|
+
version,
|
722
|
+
useTypeID,
|
723
|
+
});
|
724
|
+
if (!cellInfo) {
|
725
|
+
console.warn(`CKBFS cell with identifier ${identifier} not found`);
|
726
|
+
return null;
|
727
|
+
}
|
728
|
+
const { cell, transaction, outPoint, parsedId } = cellInfo;
|
729
|
+
// Import required modules dynamically
|
730
|
+
const { Transaction, ccc } = await Promise.resolve().then(() => __importStar(require("@ckb-ccc/core")));
|
731
|
+
const { CKBFSData } = await Promise.resolve().then(() => __importStar(require("./molecule")));
|
732
|
+
const { ProtocolVersion } = await Promise.resolve().then(() => __importStar(require("./constants")));
|
733
|
+
const tx = Transaction.from(transaction);
|
734
|
+
// Get output data from the cell
|
735
|
+
const outputIndex = outPoint.index;
|
736
|
+
const outputData = tx.outputsData[outputIndex];
|
737
|
+
if (!outputData) {
|
738
|
+
throw new Error(`Output data not found for cell at index ${outputIndex}`);
|
739
|
+
}
|
740
|
+
// Parse the output data
|
741
|
+
const rawData = outputData.startsWith("0x")
|
742
|
+
? ccc.bytesFrom(outputData.slice(2), "hex")
|
743
|
+
: Buffer.from(outputData, "hex");
|
744
|
+
// Try to unpack CKBFS data with both protocol versions
|
745
|
+
let ckbfsData;
|
746
|
+
let protocolVersion = version;
|
747
|
+
try {
|
748
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
749
|
+
}
|
750
|
+
catch (error) {
|
751
|
+
try {
|
752
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
|
753
|
+
protocolVersion = "20240906.ce6724722cf6";
|
754
|
+
}
|
755
|
+
catch (v1Error) {
|
756
|
+
throw new Error(`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`);
|
757
|
+
}
|
758
|
+
}
|
759
|
+
console.log(`Found CKBFS file: ${ckbfsData.filename}`);
|
760
|
+
console.log(`Content type: ${ckbfsData.contentType}`);
|
761
|
+
console.log(`Using direct witness decoding method`);
|
762
|
+
// Get witness indexes from CKBFS data
|
763
|
+
const indexes = ckbfsData.indexes ||
|
764
|
+
(ckbfsData.index !== undefined ? [ckbfsData.index] : []);
|
765
|
+
// Use direct witness decoding method
|
766
|
+
const content = decodeFileFromWitnessData({
|
767
|
+
witnesses: tx.witnesses,
|
768
|
+
indexes: indexes,
|
769
|
+
filename: ckbfsData.filename,
|
770
|
+
contentType: ckbfsData.contentType,
|
771
|
+
});
|
772
|
+
return {
|
773
|
+
content: content.content,
|
774
|
+
filename: ckbfsData.filename,
|
775
|
+
contentType: ckbfsData.contentType,
|
776
|
+
checksum: ckbfsData.checksum,
|
777
|
+
size: content.size,
|
778
|
+
backLinks: ckbfsData.backLinks || [],
|
779
|
+
parsedId,
|
780
|
+
};
|
781
|
+
}
|
782
|
+
catch (error) {
|
783
|
+
console.error(`Error decoding file by identifier ${identifier}:`, error);
|
784
|
+
throw error;
|
785
|
+
}
|
786
|
+
}
|
787
|
+
/**
|
788
|
+
* Decodes file content directly from TypeID using witness decoding (legacy function)
|
789
|
+
* @param client The CKB client to use for blockchain queries
|
790
|
+
* @param typeId The TypeID (args) of the CKBFS cell
|
791
|
+
* @param options Optional configuration for network, version, and useTypeID
|
792
|
+
* @returns Promise resolving to the decoded file content and metadata, or null if not found
|
793
|
+
*/
|
794
|
+
async function decodeFileFromChainByTypeId(client, typeId, options = {}) {
|
795
|
+
const result = await decodeFileFromChainByIdentifier(client, typeId, options);
|
796
|
+
if (result) {
|
797
|
+
const { parsedId, ...fileData } = result;
|
798
|
+
return fileData;
|
799
|
+
}
|
800
|
+
return null;
|
801
|
+
}
|