@ckbfs/api 1.2.4 → 1.2.6
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 +29 -27
- package/examples/example.txt +1 -0
- package/examples/identifier-test.ts +178 -0
- package/examples/index.ts +36 -24
- package/examples/publish.ts +40 -6
- 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 +317 -97
- 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 +172 -146
- 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/src/utils/file.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
import fs from
|
2
|
-
import path from
|
1
|
+
import fs from "fs";
|
2
|
+
import path from "path";
|
3
3
|
|
4
4
|
/**
|
5
5
|
* Utility functions for file operations
|
@@ -20,7 +20,7 @@ export function readFile(filePath: string): Buffer {
|
|
20
20
|
* @returns String containing the file contents
|
21
21
|
*/
|
22
22
|
export function readFileAsText(filePath: string): string {
|
23
|
-
return fs.readFileSync(filePath,
|
23
|
+
return fs.readFileSync(filePath, "utf-8");
|
24
24
|
}
|
25
25
|
|
26
26
|
/**
|
@@ -44,7 +44,7 @@ export function writeFile(filePath: string, data: Buffer | string): void {
|
|
44
44
|
if (!fs.existsSync(dirPath)) {
|
45
45
|
fs.mkdirSync(dirPath, { recursive: true });
|
46
46
|
}
|
47
|
-
|
47
|
+
|
48
48
|
fs.writeFileSync(filePath, data);
|
49
49
|
}
|
50
50
|
|
@@ -55,30 +55,30 @@ export function writeFile(filePath: string, data: Buffer | string): void {
|
|
55
55
|
*/
|
56
56
|
export function getContentType(filePath: string): string {
|
57
57
|
const extension = path.extname(filePath).toLowerCase();
|
58
|
-
|
58
|
+
|
59
59
|
const mimeTypes: { [key: string]: string } = {
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
60
|
+
".txt": "text/plain",
|
61
|
+
".html": "text/html",
|
62
|
+
".htm": "text/html",
|
63
|
+
".css": "text/css",
|
64
|
+
".js": "application/javascript",
|
65
|
+
".json": "application/json",
|
66
|
+
".jpg": "image/jpeg",
|
67
|
+
".jpeg": "image/jpeg",
|
68
|
+
".png": "image/png",
|
69
|
+
".gif": "image/gif",
|
70
|
+
".svg": "image/svg+xml",
|
71
|
+
".pdf": "application/pdf",
|
72
|
+
".mp3": "audio/mpeg",
|
73
|
+
".mp4": "video/mp4",
|
74
|
+
".wav": "audio/wav",
|
75
|
+
".xml": "application/xml",
|
76
|
+
".zip": "application/zip",
|
77
|
+
".md": "text/markdown",
|
78
|
+
".markdown": "text/markdown",
|
79
79
|
};
|
80
|
-
|
81
|
-
return mimeTypes[extension] ||
|
80
|
+
|
81
|
+
return mimeTypes[extension] || "application/octet-stream";
|
82
82
|
}
|
83
83
|
|
84
84
|
/**
|
@@ -87,14 +87,17 @@ export function getContentType(filePath: string): string {
|
|
87
87
|
* @param chunkSize The maximum size of each chunk in bytes
|
88
88
|
* @returns Array of Uint8Array chunks
|
89
89
|
*/
|
90
|
-
export function splitFileIntoChunks(
|
90
|
+
export function splitFileIntoChunks(
|
91
|
+
filePath: string,
|
92
|
+
chunkSize: number,
|
93
|
+
): Uint8Array[] {
|
91
94
|
const fileBuffer = fs.readFileSync(filePath);
|
92
95
|
const chunks: Uint8Array[] = [];
|
93
|
-
|
96
|
+
|
94
97
|
for (let i = 0; i < fileBuffer.length; i += chunkSize) {
|
95
98
|
chunks.push(new Uint8Array(fileBuffer.slice(i, i + chunkSize)));
|
96
99
|
}
|
97
|
-
|
100
|
+
|
98
101
|
return chunks;
|
99
102
|
}
|
100
103
|
|
@@ -103,8 +106,13 @@ export function splitFileIntoChunks(filePath: string, chunkSize: number): Uint8A
|
|
103
106
|
* @param chunks Array of chunks to combine
|
104
107
|
* @param outputPath The path to write the combined file to
|
105
108
|
*/
|
106
|
-
export function combineChunksToFile(
|
107
|
-
|
109
|
+
export function combineChunksToFile(
|
110
|
+
chunks: Uint8Array[],
|
111
|
+
outputPath: string,
|
112
|
+
): void {
|
113
|
+
const combinedBuffer = Buffer.concat(
|
114
|
+
chunks.map((chunk) => Buffer.from(chunk)),
|
115
|
+
);
|
108
116
|
writeFile(outputPath, combinedBuffer);
|
109
117
|
}
|
110
118
|
|
@@ -114,17 +122,17 @@ export function combineChunksToFile(chunks: Uint8Array[], outputPath: string): v
|
|
114
122
|
* @returns Decoded string or placeholder on error
|
115
123
|
*/
|
116
124
|
function safelyDecode(buffer: any): string {
|
117
|
-
if (!buffer) return
|
125
|
+
if (!buffer) return "[Unknown]";
|
118
126
|
try {
|
119
127
|
if (buffer instanceof Uint8Array) {
|
120
128
|
return new TextDecoder().decode(buffer);
|
121
|
-
} else if (typeof buffer ===
|
129
|
+
} else if (typeof buffer === "string") {
|
122
130
|
return buffer;
|
123
131
|
} else {
|
124
132
|
return `[Buffer: ${buffer.toString()}]`;
|
125
133
|
}
|
126
134
|
} catch (e) {
|
127
|
-
return
|
135
|
+
return "[Decode Error]";
|
128
136
|
}
|
129
137
|
}
|
130
138
|
|
@@ -138,24 +146,26 @@ function safelyDecode(buffer: any): string {
|
|
138
146
|
export async function getFileContentFromChain(
|
139
147
|
client: any,
|
140
148
|
outPoint: { txHash: string; index: number },
|
141
|
-
ckbfsData: any
|
149
|
+
ckbfsData: any,
|
142
150
|
): Promise<Uint8Array> {
|
143
151
|
console.log(`Retrieving file: ${safelyDecode(ckbfsData.filename)}`);
|
144
152
|
console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
|
145
|
-
|
153
|
+
|
146
154
|
// Prepare to collect all content pieces
|
147
155
|
const contentPieces: Uint8Array[] = [];
|
148
156
|
let currentData = ckbfsData;
|
149
157
|
let currentOutPoint = outPoint;
|
150
|
-
|
158
|
+
|
151
159
|
// Process the current transaction first
|
152
160
|
const tx = await client.getTransaction(currentOutPoint.txHash);
|
153
161
|
if (!tx || !tx.transaction) {
|
154
162
|
throw new Error(`Transaction ${currentOutPoint.txHash} not found`);
|
155
163
|
}
|
156
|
-
|
164
|
+
|
157
165
|
// Get content from witnesses
|
158
|
-
const indexes =
|
166
|
+
const indexes =
|
167
|
+
currentData.indexes ||
|
168
|
+
(currentData.index !== undefined ? [currentData.index] : []);
|
159
169
|
if (indexes.length > 0) {
|
160
170
|
// Get content from each witness index
|
161
171
|
for (const idx of indexes) {
|
@@ -163,12 +173,12 @@ export async function getFileContentFromChain(
|
|
163
173
|
console.warn(`Witness index ${idx} out of range`);
|
164
174
|
continue;
|
165
175
|
}
|
166
|
-
|
176
|
+
|
167
177
|
const witnessHex = tx.transaction.witnesses[idx];
|
168
|
-
const witness = Buffer.from(witnessHex.slice(2),
|
169
|
-
|
178
|
+
const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
|
179
|
+
|
170
180
|
// Extract content (skip CKBFS header + version byte)
|
171
|
-
if (witness.length >= 6 && witness.slice(0, 5).toString() ===
|
181
|
+
if (witness.length >= 6 && witness.slice(0, 5).toString() === "CKBFS") {
|
172
182
|
const content = witness.slice(6);
|
173
183
|
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
174
184
|
} else {
|
@@ -176,22 +186,24 @@ export async function getFileContentFromChain(
|
|
176
186
|
}
|
177
187
|
}
|
178
188
|
}
|
179
|
-
|
189
|
+
|
180
190
|
// Follow backlinks recursively
|
181
191
|
if (currentData.backLinks && currentData.backLinks.length > 0) {
|
182
192
|
// Process each backlink, from most recent to oldest
|
183
193
|
for (let i = currentData.backLinks.length - 1; i >= 0; i--) {
|
184
194
|
const backlink = currentData.backLinks[i];
|
185
|
-
|
195
|
+
|
186
196
|
// Get the transaction for this backlink
|
187
197
|
const backTx = await client.getTransaction(backlink.txHash);
|
188
198
|
if (!backTx || !backTx.transaction) {
|
189
199
|
console.warn(`Backlink transaction ${backlink.txHash} not found`);
|
190
200
|
continue;
|
191
201
|
}
|
192
|
-
|
202
|
+
|
193
203
|
// Get content from backlink witnesses
|
194
|
-
const backIndexes =
|
204
|
+
const backIndexes =
|
205
|
+
backlink.indexes ||
|
206
|
+
(backlink.index !== undefined ? [backlink.index] : []);
|
195
207
|
if (backIndexes.length > 0) {
|
196
208
|
// Get content from each witness index
|
197
209
|
for (const idx of backIndexes) {
|
@@ -199,22 +211,27 @@ export async function getFileContentFromChain(
|
|
199
211
|
console.warn(`Backlink witness index ${idx} out of range`);
|
200
212
|
continue;
|
201
213
|
}
|
202
|
-
|
214
|
+
|
203
215
|
const witnessHex = backTx.transaction.witnesses[idx];
|
204
|
-
const witness = Buffer.from(witnessHex.slice(2),
|
205
|
-
|
216
|
+
const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
|
217
|
+
|
206
218
|
// Extract content (skip CKBFS header + version byte)
|
207
|
-
if (
|
219
|
+
if (
|
220
|
+
witness.length >= 6 &&
|
221
|
+
witness.slice(0, 5).toString() === "CKBFS"
|
222
|
+
) {
|
208
223
|
const content = witness.slice(6);
|
209
224
|
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
210
225
|
} else {
|
211
|
-
console.warn(
|
226
|
+
console.warn(
|
227
|
+
`Backlink witness at index ${idx} is not a valid CKBFS witness`,
|
228
|
+
);
|
212
229
|
}
|
213
230
|
}
|
214
231
|
}
|
215
232
|
}
|
216
233
|
}
|
217
|
-
|
234
|
+
|
218
235
|
// Combine all content pieces
|
219
236
|
return Buffer.concat(contentPieces);
|
220
237
|
}
|
@@ -229,24 +246,812 @@ export async function getFileContentFromChain(
|
|
229
246
|
export function saveFileFromChain(
|
230
247
|
content: Uint8Array,
|
231
248
|
ckbfsData: any,
|
232
|
-
outputPath?: string
|
249
|
+
outputPath?: string,
|
233
250
|
): string {
|
234
251
|
// Get filename from CKBFS data
|
235
252
|
const filename = safelyDecode(ckbfsData.filename);
|
236
|
-
|
253
|
+
|
237
254
|
// Determine output path
|
238
255
|
const filePath = outputPath || filename;
|
239
|
-
|
256
|
+
|
240
257
|
// Ensure directory exists
|
241
258
|
const directory = path.dirname(filePath);
|
242
259
|
if (!fs.existsSync(directory)) {
|
243
260
|
fs.mkdirSync(directory, { recursive: true });
|
244
261
|
}
|
245
|
-
|
262
|
+
|
246
263
|
// Write file
|
247
264
|
fs.writeFileSync(filePath, content);
|
248
265
|
console.log(`File saved to: ${filePath}`);
|
249
266
|
console.log(`Size: ${content.length} bytes`);
|
250
|
-
|
267
|
+
|
251
268
|
return filePath;
|
252
|
-
}
|
269
|
+
}
|
270
|
+
|
271
|
+
/**
|
272
|
+
* Decodes content from a single CKBFS witness
|
273
|
+
* @param witnessHex The witness data in hex format (with or without 0x prefix)
|
274
|
+
* @returns Object containing the decoded content and metadata, or null if not a valid CKBFS witness
|
275
|
+
*/
|
276
|
+
export function decodeWitnessContent(
|
277
|
+
witnessHex: string,
|
278
|
+
): { content: Uint8Array; isValid: boolean } | null {
|
279
|
+
try {
|
280
|
+
// Remove 0x prefix if present
|
281
|
+
const hexData = witnessHex.startsWith("0x")
|
282
|
+
? witnessHex.slice(2)
|
283
|
+
: witnessHex;
|
284
|
+
const witness = Buffer.from(hexData, "hex");
|
285
|
+
|
286
|
+
// Check if it's a valid CKBFS witness
|
287
|
+
if (witness.length < 6) {
|
288
|
+
return null;
|
289
|
+
}
|
290
|
+
|
291
|
+
// Check CKBFS header
|
292
|
+
const header = witness.slice(0, 5).toString();
|
293
|
+
if (header !== "CKBFS") {
|
294
|
+
return null;
|
295
|
+
}
|
296
|
+
|
297
|
+
// Extract content (skip CKBFS header + version byte)
|
298
|
+
const content = witness.slice(6);
|
299
|
+
|
300
|
+
return {
|
301
|
+
content,
|
302
|
+
isValid: true,
|
303
|
+
};
|
304
|
+
} catch (error) {
|
305
|
+
console.warn("Error decoding witness content:", error);
|
306
|
+
return null;
|
307
|
+
}
|
308
|
+
}
|
309
|
+
|
310
|
+
/**
|
311
|
+
* Decodes and combines content from multiple CKBFS witnesses
|
312
|
+
* @param witnessHexArray Array of witness data in hex format
|
313
|
+
* @param preserveOrder Whether to preserve the order of witnesses (default: true)
|
314
|
+
* @returns Combined content from all valid CKBFS witnesses
|
315
|
+
*/
|
316
|
+
export function decodeMultipleWitnessContents(
|
317
|
+
witnessHexArray: string[],
|
318
|
+
preserveOrder: boolean = true,
|
319
|
+
): Uint8Array {
|
320
|
+
const contentPieces: Uint8Array[] = [];
|
321
|
+
|
322
|
+
for (let i = 0; i < witnessHexArray.length; i++) {
|
323
|
+
const witnessHex = witnessHexArray[i];
|
324
|
+
const decoded = decodeWitnessContent(witnessHex);
|
325
|
+
|
326
|
+
if (decoded && decoded.isValid) {
|
327
|
+
if (preserveOrder) {
|
328
|
+
contentPieces.push(decoded.content);
|
329
|
+
} else {
|
330
|
+
contentPieces.unshift(decoded.content);
|
331
|
+
}
|
332
|
+
} else {
|
333
|
+
console.warn(`Witness at index ${i} is not a valid CKBFS witness`);
|
334
|
+
}
|
335
|
+
}
|
336
|
+
|
337
|
+
return Buffer.concat(contentPieces);
|
338
|
+
}
|
339
|
+
|
340
|
+
/**
|
341
|
+
* Extracts complete file content from witnesses using specified indexes
|
342
|
+
* @param witnesses Array of all witnesses from a transaction
|
343
|
+
* @param indexes Array of witness indexes that contain CKBFS content
|
344
|
+
* @returns Combined content from the specified witness indexes
|
345
|
+
*/
|
346
|
+
export function extractFileFromWitnesses(
|
347
|
+
witnesses: string[],
|
348
|
+
indexes: number[],
|
349
|
+
): Uint8Array {
|
350
|
+
const relevantWitnesses: string[] = [];
|
351
|
+
|
352
|
+
for (const idx of indexes) {
|
353
|
+
if (idx >= witnesses.length) {
|
354
|
+
console.warn(
|
355
|
+
`Witness index ${idx} out of range (total witnesses: ${witnesses.length})`,
|
356
|
+
);
|
357
|
+
continue;
|
358
|
+
}
|
359
|
+
relevantWitnesses.push(witnesses[idx]);
|
360
|
+
}
|
361
|
+
|
362
|
+
return decodeMultipleWitnessContents(relevantWitnesses, true);
|
363
|
+
}
|
364
|
+
|
365
|
+
/**
|
366
|
+
* Decodes file content directly from witness data without blockchain queries
|
367
|
+
* @param witnessData Object containing witness information
|
368
|
+
* @returns Object containing the decoded file content and metadata
|
369
|
+
*/
|
370
|
+
export function decodeFileFromWitnessData(witnessData: {
|
371
|
+
witnesses: string[];
|
372
|
+
indexes: number[] | number;
|
373
|
+
filename?: string;
|
374
|
+
contentType?: string;
|
375
|
+
}): {
|
376
|
+
content: Uint8Array;
|
377
|
+
filename?: string;
|
378
|
+
contentType?: string;
|
379
|
+
size: number;
|
380
|
+
} {
|
381
|
+
const { witnesses, indexes, filename, contentType } = witnessData;
|
382
|
+
|
383
|
+
// Normalize indexes to array
|
384
|
+
const indexArray = Array.isArray(indexes) ? indexes : [indexes];
|
385
|
+
|
386
|
+
// Extract content from witnesses
|
387
|
+
const content = extractFileFromWitnesses(witnesses, indexArray);
|
388
|
+
|
389
|
+
return {
|
390
|
+
content,
|
391
|
+
filename,
|
392
|
+
contentType,
|
393
|
+
size: content.length,
|
394
|
+
};
|
395
|
+
}
|
396
|
+
|
397
|
+
/**
|
398
|
+
* Saves decoded file content directly from witness data
|
399
|
+
* @param witnessData Object containing witness information
|
400
|
+
* @param outputPath Optional path to save the file
|
401
|
+
* @returns The path where the file was saved
|
402
|
+
*/
|
403
|
+
export function saveFileFromWitnessData(
|
404
|
+
witnessData: {
|
405
|
+
witnesses: string[];
|
406
|
+
indexes: number[] | number;
|
407
|
+
filename?: string;
|
408
|
+
contentType?: string;
|
409
|
+
},
|
410
|
+
outputPath?: string,
|
411
|
+
): string {
|
412
|
+
const decoded = decodeFileFromWitnessData(witnessData);
|
413
|
+
|
414
|
+
// Determine output path
|
415
|
+
const filename = decoded.filename || "decoded_file";
|
416
|
+
const filePath = outputPath || filename;
|
417
|
+
|
418
|
+
// Ensure directory exists
|
419
|
+
const directory = path.dirname(filePath);
|
420
|
+
if (!fs.existsSync(directory)) {
|
421
|
+
fs.mkdirSync(directory, { recursive: true });
|
422
|
+
}
|
423
|
+
|
424
|
+
// Write file
|
425
|
+
fs.writeFileSync(filePath, decoded.content);
|
426
|
+
console.log(`File saved to: ${filePath}`);
|
427
|
+
console.log(`Size: ${decoded.size} bytes`);
|
428
|
+
console.log(`Content type: ${decoded.contentType || "unknown"}`);
|
429
|
+
|
430
|
+
return filePath;
|
431
|
+
}
|
432
|
+
|
433
|
+
/**
|
434
|
+
* Identifier types for CKBFS cells
|
435
|
+
*/
|
436
|
+
export enum IdentifierType {
|
437
|
+
TypeID = "typeId",
|
438
|
+
OutPoint = "outPoint",
|
439
|
+
Unknown = "unknown",
|
440
|
+
}
|
441
|
+
|
442
|
+
/**
|
443
|
+
* Parsed identifier information
|
444
|
+
*/
|
445
|
+
export interface ParsedIdentifier {
|
446
|
+
type: IdentifierType;
|
447
|
+
typeId?: string;
|
448
|
+
txHash?: string;
|
449
|
+
index?: number;
|
450
|
+
original: string;
|
451
|
+
}
|
452
|
+
|
453
|
+
/**
|
454
|
+
* Detects and parses different CKBFS identifier formats
|
455
|
+
* @param identifier The identifier string to parse
|
456
|
+
* @returns Parsed identifier information
|
457
|
+
*/
|
458
|
+
export function parseIdentifier(identifier: string): ParsedIdentifier {
|
459
|
+
const trimmed = identifier.trim();
|
460
|
+
|
461
|
+
// Type 1: Pure TypeID hex string (with or without 0x prefix)
|
462
|
+
if (trimmed.match(/^(0x)?[a-fA-F0-9]{64}$/)) {
|
463
|
+
const typeId = trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
|
464
|
+
return {
|
465
|
+
type: IdentifierType.TypeID,
|
466
|
+
typeId,
|
467
|
+
original: identifier,
|
468
|
+
};
|
469
|
+
}
|
470
|
+
|
471
|
+
// Type 2: CKBFS URI with TypeID
|
472
|
+
if (trimmed.startsWith("ckbfs://")) {
|
473
|
+
const content = trimmed.slice(8); // Remove "ckbfs://"
|
474
|
+
|
475
|
+
// Check if it's TypeID format (64 hex characters)
|
476
|
+
if (content.match(/^[a-fA-F0-9]{64}$/)) {
|
477
|
+
return {
|
478
|
+
type: IdentifierType.TypeID,
|
479
|
+
typeId: `0x${content}`,
|
480
|
+
original: identifier,
|
481
|
+
};
|
482
|
+
}
|
483
|
+
|
484
|
+
// Type 3: CKBFS URI with transaction hash and index (txhash + 'i' + index)
|
485
|
+
const outPointMatch = content.match(/^([a-fA-F0-9]{64})i(\d+)$/);
|
486
|
+
if (outPointMatch) {
|
487
|
+
const [, txHash, indexStr] = outPointMatch;
|
488
|
+
return {
|
489
|
+
type: IdentifierType.OutPoint,
|
490
|
+
txHash: `0x${txHash}`,
|
491
|
+
index: parseInt(indexStr, 10),
|
492
|
+
original: identifier,
|
493
|
+
};
|
494
|
+
}
|
495
|
+
}
|
496
|
+
|
497
|
+
// Unknown format
|
498
|
+
return {
|
499
|
+
type: IdentifierType.Unknown,
|
500
|
+
original: identifier,
|
501
|
+
};
|
502
|
+
}
|
503
|
+
|
504
|
+
/**
|
505
|
+
* Resolves a CKBFS cell using any supported identifier format
|
506
|
+
* @param client The CKB client to use for blockchain queries
|
507
|
+
* @param identifier The identifier (TypeID, CKBFS URI, or outPoint URI)
|
508
|
+
* @param options Optional configuration for network, version, and useTypeID
|
509
|
+
* @returns Promise resolving to the found cell and transaction info, or null if not found
|
510
|
+
*/
|
511
|
+
async function resolveCKBFSCell(
|
512
|
+
client: any,
|
513
|
+
identifier: string,
|
514
|
+
options: {
|
515
|
+
network?: "mainnet" | "testnet";
|
516
|
+
version?: string;
|
517
|
+
useTypeID?: boolean;
|
518
|
+
} = {},
|
519
|
+
): Promise<{
|
520
|
+
cell: any;
|
521
|
+
transaction: any;
|
522
|
+
outPoint: { txHash: string; index: number };
|
523
|
+
parsedId: ParsedIdentifier;
|
524
|
+
} | null> {
|
525
|
+
const {
|
526
|
+
network = "testnet",
|
527
|
+
version = "20241025.db973a8e8032",
|
528
|
+
useTypeID = false,
|
529
|
+
} = options;
|
530
|
+
|
531
|
+
const parsedId = parseIdentifier(identifier);
|
532
|
+
|
533
|
+
try {
|
534
|
+
if (parsedId.type === IdentifierType.TypeID && parsedId.typeId) {
|
535
|
+
// Use existing TypeID resolution logic
|
536
|
+
const cellInfo = await findCKBFSCellByTypeId(
|
537
|
+
client,
|
538
|
+
parsedId.typeId,
|
539
|
+
network,
|
540
|
+
version,
|
541
|
+
useTypeID,
|
542
|
+
);
|
543
|
+
|
544
|
+
if (cellInfo) {
|
545
|
+
return {
|
546
|
+
...cellInfo,
|
547
|
+
parsedId,
|
548
|
+
};
|
549
|
+
}
|
550
|
+
} else if (
|
551
|
+
parsedId.type === IdentifierType.OutPoint &&
|
552
|
+
parsedId.txHash &&
|
553
|
+
parsedId.index !== undefined
|
554
|
+
) {
|
555
|
+
// Resolve using transaction hash and index
|
556
|
+
const txWithStatus = await client.getTransaction(parsedId.txHash);
|
557
|
+
|
558
|
+
if (!txWithStatus || !txWithStatus.transaction) {
|
559
|
+
console.warn(`Transaction ${parsedId.txHash} not found`);
|
560
|
+
return null;
|
561
|
+
}
|
562
|
+
|
563
|
+
// Import Transaction class dynamically
|
564
|
+
const { Transaction } = await import("@ckb-ccc/core");
|
565
|
+
const tx = Transaction.from(txWithStatus.transaction);
|
566
|
+
|
567
|
+
// Check if the index is valid
|
568
|
+
if (parsedId.index >= tx.outputs.length) {
|
569
|
+
console.warn(
|
570
|
+
`Output index ${parsedId.index} out of range for transaction ${parsedId.txHash}`,
|
571
|
+
);
|
572
|
+
return null;
|
573
|
+
}
|
574
|
+
|
575
|
+
const output = tx.outputs[parsedId.index];
|
576
|
+
|
577
|
+
// Verify it's a CKBFS cell by checking if it has a type script
|
578
|
+
if (!output.type) {
|
579
|
+
console.warn(
|
580
|
+
`Output at index ${parsedId.index} is not a CKBFS cell (no type script)`,
|
581
|
+
);
|
582
|
+
return null;
|
583
|
+
}
|
584
|
+
|
585
|
+
// Create a mock cell object similar to what findSingletonCellByType returns
|
586
|
+
const cell = {
|
587
|
+
outPoint: {
|
588
|
+
txHash: parsedId.txHash,
|
589
|
+
index: parsedId.index,
|
590
|
+
},
|
591
|
+
output,
|
592
|
+
};
|
593
|
+
|
594
|
+
return {
|
595
|
+
cell,
|
596
|
+
transaction: txWithStatus.transaction,
|
597
|
+
outPoint: {
|
598
|
+
txHash: parsedId.txHash,
|
599
|
+
index: parsedId.index,
|
600
|
+
},
|
601
|
+
parsedId,
|
602
|
+
};
|
603
|
+
}
|
604
|
+
|
605
|
+
console.warn(
|
606
|
+
`Unable to resolve identifier: ${identifier} (type: ${parsedId.type})`,
|
607
|
+
);
|
608
|
+
return null;
|
609
|
+
} catch (error) {
|
610
|
+
console.error(
|
611
|
+
`Error resolving CKBFS cell for identifier ${identifier}:`,
|
612
|
+
error,
|
613
|
+
);
|
614
|
+
return null;
|
615
|
+
}
|
616
|
+
}
|
617
|
+
|
618
|
+
/**
|
619
|
+
* Finds a CKBFS cell by TypeID
|
620
|
+
* @param client The CKB client to use for blockchain queries
|
621
|
+
* @param typeId The TypeID (args) of the CKBFS cell to find
|
622
|
+
* @param network The network type (mainnet or testnet)
|
623
|
+
* @param version The protocol version to use
|
624
|
+
* @param useTypeID Whether to use type ID instead of code hash for script matching
|
625
|
+
* @returns Promise resolving to the found cell and transaction info, or null if not found
|
626
|
+
*/
|
627
|
+
async function findCKBFSCellByTypeId(
|
628
|
+
client: any,
|
629
|
+
typeId: string,
|
630
|
+
network: string = "testnet",
|
631
|
+
version: string = "20241025.db973a8e8032",
|
632
|
+
useTypeID: boolean = false,
|
633
|
+
): Promise<{
|
634
|
+
cell: any;
|
635
|
+
transaction: any;
|
636
|
+
outPoint: { txHash: string; index: number };
|
637
|
+
} | null> {
|
638
|
+
try {
|
639
|
+
// Import constants dynamically to avoid circular dependencies
|
640
|
+
const { getCKBFSScriptConfig, NetworkType, ProtocolVersion } = await import(
|
641
|
+
"./constants"
|
642
|
+
);
|
643
|
+
|
644
|
+
// Get CKBFS script config
|
645
|
+
const networkType =
|
646
|
+
network === "mainnet" ? NetworkType.Mainnet : NetworkType.Testnet;
|
647
|
+
const protocolVersion =
|
648
|
+
version === "20240906.ce6724722cf6"
|
649
|
+
? ProtocolVersion.V1
|
650
|
+
: ProtocolVersion.V2;
|
651
|
+
const config = getCKBFSScriptConfig(
|
652
|
+
networkType,
|
653
|
+
protocolVersion,
|
654
|
+
useTypeID,
|
655
|
+
);
|
656
|
+
|
657
|
+
// Create the script to search for
|
658
|
+
const script = {
|
659
|
+
codeHash: config.codeHash,
|
660
|
+
hashType: config.hashType,
|
661
|
+
args: typeId.startsWith("0x") ? typeId : `0x${typeId}`,
|
662
|
+
};
|
663
|
+
|
664
|
+
// Find the cell by type script
|
665
|
+
const cell = await client.findSingletonCellByType(script, true);
|
666
|
+
|
667
|
+
if (!cell) {
|
668
|
+
return null;
|
669
|
+
}
|
670
|
+
|
671
|
+
// Get the transaction that contains this cell
|
672
|
+
const txHash = cell.outPoint.txHash;
|
673
|
+
const txWithStatus = await client.getTransaction(txHash);
|
674
|
+
|
675
|
+
if (!txWithStatus || !txWithStatus.transaction) {
|
676
|
+
throw new Error(`Transaction ${txHash} not found`);
|
677
|
+
}
|
678
|
+
|
679
|
+
return {
|
680
|
+
cell,
|
681
|
+
transaction: txWithStatus.transaction,
|
682
|
+
outPoint: {
|
683
|
+
txHash: cell.outPoint.txHash,
|
684
|
+
index: cell.outPoint.index,
|
685
|
+
},
|
686
|
+
};
|
687
|
+
} catch (error) {
|
688
|
+
console.warn(`Error finding CKBFS cell by TypeID ${typeId}:`, error);
|
689
|
+
return null;
|
690
|
+
}
|
691
|
+
}
|
692
|
+
|
693
|
+
/**
|
694
|
+
* Retrieves complete file content from the blockchain using any supported identifier
|
695
|
+
* @param client The CKB client to use for blockchain queries
|
696
|
+
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
|
697
|
+
* @param options Optional configuration for network, version, and useTypeID
|
698
|
+
* @returns Promise resolving to the complete file content and metadata
|
699
|
+
*/
|
700
|
+
export async function getFileContentFromChainByIdentifier(
|
701
|
+
client: any,
|
702
|
+
identifier: string,
|
703
|
+
options: {
|
704
|
+
network?: "mainnet" | "testnet";
|
705
|
+
version?: string;
|
706
|
+
useTypeID?: boolean;
|
707
|
+
} = {},
|
708
|
+
): Promise<{
|
709
|
+
content: Uint8Array;
|
710
|
+
filename: string;
|
711
|
+
contentType: string;
|
712
|
+
checksum: number;
|
713
|
+
size: number;
|
714
|
+
backLinks: any[];
|
715
|
+
parsedId: ParsedIdentifier;
|
716
|
+
} | null> {
|
717
|
+
const {
|
718
|
+
network = "testnet",
|
719
|
+
version = "20241025.db973a8e8032",
|
720
|
+
useTypeID = false,
|
721
|
+
} = options;
|
722
|
+
|
723
|
+
try {
|
724
|
+
// Resolve the CKBFS cell using any supported identifier format
|
725
|
+
const cellInfo = await resolveCKBFSCell(client, identifier, {
|
726
|
+
network,
|
727
|
+
version,
|
728
|
+
useTypeID,
|
729
|
+
});
|
730
|
+
|
731
|
+
if (!cellInfo) {
|
732
|
+
console.warn(`CKBFS cell with identifier ${identifier} not found`);
|
733
|
+
return null;
|
734
|
+
}
|
735
|
+
|
736
|
+
const { cell, transaction, outPoint, parsedId } = cellInfo;
|
737
|
+
|
738
|
+
// Import Transaction class dynamically
|
739
|
+
const { Transaction } = await import("@ckb-ccc/core");
|
740
|
+
const tx = Transaction.from(transaction);
|
741
|
+
|
742
|
+
// Get output data from the cell
|
743
|
+
const outputIndex = outPoint.index;
|
744
|
+
const outputData = tx.outputsData[outputIndex];
|
745
|
+
|
746
|
+
if (!outputData) {
|
747
|
+
throw new Error(`Output data not found for cell at index ${outputIndex}`);
|
748
|
+
}
|
749
|
+
|
750
|
+
// Import required modules dynamically
|
751
|
+
const { ccc } = await import("@ckb-ccc/core");
|
752
|
+
const { CKBFSData } = await import("./molecule");
|
753
|
+
const { ProtocolVersion } = await import("./constants");
|
754
|
+
|
755
|
+
// Parse the output data
|
756
|
+
const rawData = outputData.startsWith("0x")
|
757
|
+
? ccc.bytesFrom(outputData.slice(2), "hex")
|
758
|
+
: Buffer.from(outputData, "hex");
|
759
|
+
|
760
|
+
// Try to unpack CKBFS data with both protocol versions
|
761
|
+
let ckbfsData: any;
|
762
|
+
let protocolVersion = version;
|
763
|
+
|
764
|
+
try {
|
765
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
766
|
+
} catch (error) {
|
767
|
+
try {
|
768
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
|
769
|
+
protocolVersion = "20240906.ce6724722cf6";
|
770
|
+
} catch (v1Error) {
|
771
|
+
throw new Error(
|
772
|
+
`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
|
773
|
+
);
|
774
|
+
}
|
775
|
+
}
|
776
|
+
|
777
|
+
console.log(`Found CKBFS file: ${ckbfsData.filename}`);
|
778
|
+
console.log(`Content type: ${ckbfsData.contentType}`);
|
779
|
+
console.log(`Protocol version: ${protocolVersion}`);
|
780
|
+
|
781
|
+
// Use existing function to get complete file content
|
782
|
+
const content = await getFileContentFromChain(client, outPoint, ckbfsData);
|
783
|
+
|
784
|
+
return {
|
785
|
+
content,
|
786
|
+
filename: ckbfsData.filename,
|
787
|
+
contentType: ckbfsData.contentType,
|
788
|
+
checksum: ckbfsData.checksum,
|
789
|
+
size: content.length,
|
790
|
+
backLinks: ckbfsData.backLinks || [],
|
791
|
+
parsedId,
|
792
|
+
};
|
793
|
+
} catch (error) {
|
794
|
+
console.error(`Error retrieving file by identifier ${identifier}:`, error);
|
795
|
+
throw error;
|
796
|
+
}
|
797
|
+
}
|
798
|
+
|
799
|
+
/**
|
800
|
+
* Retrieves complete file content from the blockchain using TypeID (legacy function)
|
801
|
+
* @param client The CKB client to use for blockchain queries
|
802
|
+
* @param typeId The TypeID (args) of the CKBFS cell
|
803
|
+
* @param options Optional configuration for network, version, and useTypeID
|
804
|
+
* @returns Promise resolving to the complete file content and metadata
|
805
|
+
*/
|
806
|
+
export async function getFileContentFromChainByTypeId(
|
807
|
+
client: any,
|
808
|
+
typeId: string,
|
809
|
+
options: {
|
810
|
+
network?: "mainnet" | "testnet";
|
811
|
+
version?: string;
|
812
|
+
useTypeID?: boolean;
|
813
|
+
} = {},
|
814
|
+
): Promise<{
|
815
|
+
content: Uint8Array;
|
816
|
+
filename: string;
|
817
|
+
contentType: string;
|
818
|
+
checksum: number;
|
819
|
+
size: number;
|
820
|
+
backLinks: any[];
|
821
|
+
} | null> {
|
822
|
+
const result = await getFileContentFromChainByIdentifier(
|
823
|
+
client,
|
824
|
+
typeId,
|
825
|
+
options,
|
826
|
+
);
|
827
|
+
if (result) {
|
828
|
+
const { parsedId, ...fileData } = result;
|
829
|
+
return fileData;
|
830
|
+
}
|
831
|
+
return null;
|
832
|
+
}
|
833
|
+
|
834
|
+
/**
|
835
|
+
* Saves file content retrieved from blockchain by identifier to disk
|
836
|
+
* @param client The CKB client to use for blockchain queries
|
837
|
+
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
|
838
|
+
* @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
|
839
|
+
* @param options Optional configuration for network, version, and useTypeID
|
840
|
+
* @returns Promise resolving to the path where the file was saved, or null if file not found
|
841
|
+
*/
|
842
|
+
export async function saveFileFromChainByIdentifier(
|
843
|
+
client: any,
|
844
|
+
identifier: string,
|
845
|
+
outputPath?: string,
|
846
|
+
options: {
|
847
|
+
network?: "mainnet" | "testnet";
|
848
|
+
version?: string;
|
849
|
+
useTypeID?: boolean;
|
850
|
+
} = {},
|
851
|
+
): Promise<string | null> {
|
852
|
+
try {
|
853
|
+
// Get file content by identifier
|
854
|
+
const fileData = await getFileContentFromChainByIdentifier(
|
855
|
+
client,
|
856
|
+
identifier,
|
857
|
+
options,
|
858
|
+
);
|
859
|
+
|
860
|
+
if (!fileData) {
|
861
|
+
console.warn(`File with identifier ${identifier} not found`);
|
862
|
+
return null;
|
863
|
+
}
|
864
|
+
|
865
|
+
// Determine output path
|
866
|
+
const filePath = outputPath || fileData.filename;
|
867
|
+
|
868
|
+
// Ensure directory exists
|
869
|
+
const directory = path.dirname(filePath);
|
870
|
+
if (!fs.existsSync(directory)) {
|
871
|
+
fs.mkdirSync(directory, { recursive: true });
|
872
|
+
}
|
873
|
+
|
874
|
+
// Write file
|
875
|
+
fs.writeFileSync(filePath, fileData.content);
|
876
|
+
console.log(`File saved to: ${filePath}`);
|
877
|
+
console.log(`Size: ${fileData.size} bytes`);
|
878
|
+
console.log(`Content type: ${fileData.contentType}`);
|
879
|
+
console.log(`Checksum: ${fileData.checksum}`);
|
880
|
+
|
881
|
+
return filePath;
|
882
|
+
} catch (error) {
|
883
|
+
console.error(`Error saving file by identifier ${identifier}:`, error);
|
884
|
+
throw error;
|
885
|
+
}
|
886
|
+
}
|
887
|
+
|
888
|
+
/**
|
889
|
+
* Saves file content retrieved from blockchain by TypeID to disk (legacy function)
|
890
|
+
* @param client The CKB client to use for blockchain queries
|
891
|
+
* @param typeId The TypeID (args) of the CKBFS cell
|
892
|
+
* @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
|
893
|
+
* @param options Optional configuration for network, version, and useTypeID
|
894
|
+
* @returns Promise resolving to the path where the file was saved, or null if file not found
|
895
|
+
*/
|
896
|
+
export async function saveFileFromChainByTypeId(
|
897
|
+
client: any,
|
898
|
+
typeId: string,
|
899
|
+
outputPath?: string,
|
900
|
+
options: {
|
901
|
+
network?: "mainnet" | "testnet";
|
902
|
+
version?: string;
|
903
|
+
useTypeID?: boolean;
|
904
|
+
} = {},
|
905
|
+
): Promise<string | null> {
|
906
|
+
return await saveFileFromChainByIdentifier(
|
907
|
+
client,
|
908
|
+
typeId,
|
909
|
+
outputPath,
|
910
|
+
options,
|
911
|
+
);
|
912
|
+
}
|
913
|
+
|
914
|
+
/**
|
915
|
+
* Decodes file content directly from identifier using witness decoding (new method)
|
916
|
+
* @param client The CKB client to use for blockchain queries
|
917
|
+
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
|
918
|
+
* @param options Optional configuration for network, version, and useTypeID
|
919
|
+
* @returns Promise resolving to the decoded file content and metadata, or null if not found
|
920
|
+
*/
|
921
|
+
export async function decodeFileFromChainByIdentifier(
|
922
|
+
client: any,
|
923
|
+
identifier: string,
|
924
|
+
options: {
|
925
|
+
network?: "mainnet" | "testnet";
|
926
|
+
version?: string;
|
927
|
+
useTypeID?: boolean;
|
928
|
+
} = {},
|
929
|
+
): Promise<{
|
930
|
+
content: Uint8Array;
|
931
|
+
filename: string;
|
932
|
+
contentType: string;
|
933
|
+
checksum: number;
|
934
|
+
size: number;
|
935
|
+
backLinks: any[];
|
936
|
+
parsedId: ParsedIdentifier;
|
937
|
+
} | null> {
|
938
|
+
const {
|
939
|
+
network = "testnet",
|
940
|
+
version = "20241025.db973a8e8032",
|
941
|
+
useTypeID = false,
|
942
|
+
} = options;
|
943
|
+
|
944
|
+
try {
|
945
|
+
// Resolve the CKBFS cell using any supported identifier format
|
946
|
+
const cellInfo = await resolveCKBFSCell(client, identifier, {
|
947
|
+
network,
|
948
|
+
version,
|
949
|
+
useTypeID,
|
950
|
+
});
|
951
|
+
|
952
|
+
if (!cellInfo) {
|
953
|
+
console.warn(`CKBFS cell with identifier ${identifier} not found`);
|
954
|
+
return null;
|
955
|
+
}
|
956
|
+
|
957
|
+
const { cell, transaction, outPoint, parsedId } = cellInfo;
|
958
|
+
|
959
|
+
// Import required modules dynamically
|
960
|
+
const { Transaction, ccc } = await import("@ckb-ccc/core");
|
961
|
+
const { CKBFSData } = await import("./molecule");
|
962
|
+
const { ProtocolVersion } = await import("./constants");
|
963
|
+
|
964
|
+
const tx = Transaction.from(transaction);
|
965
|
+
|
966
|
+
// Get output data from the cell
|
967
|
+
const outputIndex = outPoint.index;
|
968
|
+
const outputData = tx.outputsData[outputIndex];
|
969
|
+
|
970
|
+
if (!outputData) {
|
971
|
+
throw new Error(`Output data not found for cell at index ${outputIndex}`);
|
972
|
+
}
|
973
|
+
|
974
|
+
// Parse the output data
|
975
|
+
const rawData = outputData.startsWith("0x")
|
976
|
+
? ccc.bytesFrom(outputData.slice(2), "hex")
|
977
|
+
: Buffer.from(outputData, "hex");
|
978
|
+
|
979
|
+
// Try to unpack CKBFS data with both protocol versions
|
980
|
+
let ckbfsData: any;
|
981
|
+
let protocolVersion = version;
|
982
|
+
|
983
|
+
try {
|
984
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
985
|
+
} catch (error) {
|
986
|
+
try {
|
987
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
|
988
|
+
protocolVersion = "20240906.ce6724722cf6";
|
989
|
+
} catch (v1Error) {
|
990
|
+
throw new Error(
|
991
|
+
`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
|
992
|
+
);
|
993
|
+
}
|
994
|
+
}
|
995
|
+
|
996
|
+
console.log(`Found CKBFS file: ${ckbfsData.filename}`);
|
997
|
+
console.log(`Content type: ${ckbfsData.contentType}`);
|
998
|
+
console.log(`Using direct witness decoding method`);
|
999
|
+
|
1000
|
+
// Get witness indexes from CKBFS data
|
1001
|
+
const indexes =
|
1002
|
+
ckbfsData.indexes ||
|
1003
|
+
(ckbfsData.index !== undefined ? [ckbfsData.index] : []);
|
1004
|
+
|
1005
|
+
// Use direct witness decoding method
|
1006
|
+
const content = decodeFileFromWitnessData({
|
1007
|
+
witnesses: tx.witnesses,
|
1008
|
+
indexes: indexes,
|
1009
|
+
filename: ckbfsData.filename,
|
1010
|
+
contentType: ckbfsData.contentType,
|
1011
|
+
});
|
1012
|
+
|
1013
|
+
return {
|
1014
|
+
content: content.content,
|
1015
|
+
filename: ckbfsData.filename,
|
1016
|
+
contentType: ckbfsData.contentType,
|
1017
|
+
checksum: ckbfsData.checksum,
|
1018
|
+
size: content.size,
|
1019
|
+
backLinks: ckbfsData.backLinks || [],
|
1020
|
+
parsedId,
|
1021
|
+
};
|
1022
|
+
} catch (error) {
|
1023
|
+
console.error(`Error decoding file by identifier ${identifier}:`, error);
|
1024
|
+
throw error;
|
1025
|
+
}
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
/**
|
1029
|
+
* Decodes file content directly from TypeID using witness decoding (legacy function)
|
1030
|
+
* @param client The CKB client to use for blockchain queries
|
1031
|
+
* @param typeId The TypeID (args) of the CKBFS cell
|
1032
|
+
* @param options Optional configuration for network, version, and useTypeID
|
1033
|
+
* @returns Promise resolving to the decoded file content and metadata, or null if not found
|
1034
|
+
*/
|
1035
|
+
export async function decodeFileFromChainByTypeId(
|
1036
|
+
client: any,
|
1037
|
+
typeId: string,
|
1038
|
+
options: {
|
1039
|
+
network?: "mainnet" | "testnet";
|
1040
|
+
version?: string;
|
1041
|
+
useTypeID?: boolean;
|
1042
|
+
} = {},
|
1043
|
+
): Promise<{
|
1044
|
+
content: Uint8Array;
|
1045
|
+
filename: string;
|
1046
|
+
contentType: string;
|
1047
|
+
checksum: number;
|
1048
|
+
size: number;
|
1049
|
+
backLinks: any[];
|
1050
|
+
} | null> {
|
1051
|
+
const result = await decodeFileFromChainByIdentifier(client, typeId, options);
|
1052
|
+
if (result) {
|
1053
|
+
const { parsedId, ...fileData } = result;
|
1054
|
+
return fileData;
|
1055
|
+
}
|
1056
|
+
return null;
|
1057
|
+
}
|