@ckbfs/api
Version:
SDK for CKBFS protocol on CKB
1,058 lines (939 loc) • 30.7 kB
text/typescript
import fs from "fs";
import path from "path";
/**
* Utility functions for file operations
*/
/**
* Reads a file from the file system
* @param filePath The path to the file to read
* @returns Buffer containing the file contents
*/
export function readFile(filePath: string): Buffer {
return fs.readFileSync(filePath);
}
/**
* Reads a file as text from the file system
* @param filePath The path to the file to read
* @returns String containing the file contents
*/
export function readFileAsText(filePath: string): string {
return fs.readFileSync(filePath, "utf-8");
}
/**
* Reads a file as Uint8Array from the file system
* @param filePath The path to the file to read
* @returns Uint8Array containing the file contents
*/
export function readFileAsUint8Array(filePath: string): Uint8Array {
const buffer = fs.readFileSync(filePath);
return new Uint8Array(buffer);
}
/**
* Writes data to a file in the file system
* @param filePath The path to write the file to
* @param data The data to write to the file
*/
export function writeFile(filePath: string, data: Buffer | string): void {
// Ensure the directory exists
const dirPath = path.dirname(filePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(filePath, data);
}
/**
* Gets the MIME content type based on file extension
* @param filePath The path to the file
* @returns The MIME content type for the file
*/
export function getContentType(filePath: string): string {
const extension = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
".txt": "text/plain",
".html": "text/html",
".htm": "text/html",
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".svg": "image/svg+xml",
".pdf": "application/pdf",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".wav": "audio/wav",
".xml": "application/xml",
".zip": "application/zip",
".md": "text/markdown",
".markdown": "text/markdown",
};
return mimeTypes[extension] || "application/octet-stream";
}
/**
* Splits a file into chunks of a specific size
* @param filePath The path to the file to split
* @param chunkSize The maximum size of each chunk in bytes
* @returns Array of Uint8Array chunks
*/
export function splitFileIntoChunks(
filePath: string,
chunkSize: number,
): Uint8Array[] {
const fileBuffer = fs.readFileSync(filePath);
const chunks: Uint8Array[] = [];
for (let i = 0; i < fileBuffer.length; i += chunkSize) {
chunks.push(new Uint8Array(fileBuffer.slice(i, i + chunkSize)));
}
return chunks;
}
/**
* Combines chunks into a single file
* @param chunks Array of chunks to combine
* @param outputPath The path to write the combined file to
*/
export function combineChunksToFile(
chunks: Uint8Array[],
outputPath: string,
): void {
const combinedBuffer = Buffer.concat(
chunks.map((chunk) => Buffer.from(chunk)),
);
writeFile(outputPath, combinedBuffer);
}
/**
* Utility function to safely decode buffer to string
* @param buffer The buffer to decode
* @returns Decoded string or placeholder on error
*/
function safelyDecode(buffer: any): string {
if (!buffer) return "[Unknown]";
try {
if (buffer instanceof Uint8Array) {
return new TextDecoder().decode(buffer);
} else if (typeof buffer === "string") {
return buffer;
} else {
return `[Buffer: ${buffer.toString()}]`;
}
} catch (e) {
return "[Decode Error]";
}
}
/**
* Retrieves complete file content from the blockchain by following backlinks
* @param client The CKB client to use for blockchain queries
* @param outPoint The output point of the latest CKBFS cell
* @param ckbfsData The data from the latest CKBFS cell
* @returns Promise resolving to the complete file content
*/
export async function getFileContentFromChain(
client: any,
outPoint: { txHash: string; index: number },
ckbfsData: any,
): Promise<Uint8Array> {
console.log(`Retrieving file: ${safelyDecode(ckbfsData.filename)}`);
console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
// Prepare to collect all content pieces
const contentPieces: Uint8Array[] = [];
let currentData = ckbfsData;
let currentOutPoint = outPoint;
// Process the current transaction first
const tx = await client.getTransaction(currentOutPoint.txHash);
if (!tx || !tx.transaction) {
throw new Error(`Transaction ${currentOutPoint.txHash} not found`);
}
// Get content from witnesses
const indexes =
currentData.indexes ||
(currentData.index !== undefined ? [currentData.index] : []);
if (indexes.length > 0) {
// Get content from each witness index
for (const idx of indexes) {
if (idx >= tx.transaction.witnesses.length) {
console.warn(`Witness index ${idx} out of range`);
continue;
}
const witnessHex = tx.transaction.witnesses[idx];
const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
// Extract content (skip CKBFS header + version byte)
if (witness.length >= 6 && witness.slice(0, 5).toString() === "CKBFS") {
const content = witness.slice(6);
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
} else {
console.warn(`Witness at index ${idx} is not a valid CKBFS witness`);
}
}
}
// Follow backlinks recursively
if (currentData.backLinks && currentData.backLinks.length > 0) {
// Process each backlink, from most recent to oldest
for (let i = currentData.backLinks.length - 1; i >= 0; i--) {
const backlink = currentData.backLinks[i];
// Get the transaction for this backlink
const backTx = await client.getTransaction(backlink.txHash);
if (!backTx || !backTx.transaction) {
console.warn(`Backlink transaction ${backlink.txHash} not found`);
continue;
}
// Get content from backlink witnesses
const backIndexes =
backlink.indexes ||
(backlink.index !== undefined ? [backlink.index] : []);
if (backIndexes.length > 0) {
// Get content from each witness index
for (const idx of backIndexes) {
if (idx >= backTx.transaction.witnesses.length) {
console.warn(`Backlink witness index ${idx} out of range`);
continue;
}
const witnessHex = backTx.transaction.witnesses[idx];
const witness = Buffer.from(witnessHex.slice(2), "hex"); // Remove 0x prefix
// Extract content (skip CKBFS header + version byte)
if (
witness.length >= 6 &&
witness.slice(0, 5).toString() === "CKBFS"
) {
const content = witness.slice(6);
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
} else {
console.warn(
`Backlink witness at index ${idx} is not a valid CKBFS witness`,
);
}
}
}
}
}
// Combine all content pieces
return Buffer.concat(contentPieces);
}
/**
* Saves file content retrieved from blockchain to disk
* @param content The file content to save
* @param ckbfsData The CKBFS cell data containing file metadata
* @param outputPath Optional path to save the file (defaults to filename in current directory)
* @returns The path where the file was saved
*/
export function saveFileFromChain(
content: Uint8Array,
ckbfsData: any,
outputPath?: string,
): string {
// Get filename from CKBFS data
const filename = safelyDecode(ckbfsData.filename);
// Determine output path
const filePath = outputPath || filename;
// Ensure directory exists
const directory = path.dirname(filePath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
// Write file
fs.writeFileSync(filePath, content);
console.log(`File saved to: ${filePath}`);
console.log(`Size: ${content.length} bytes`);
return filePath;
}
/**
* Decodes content from a single CKBFS witness
* @param witnessHex The witness data in hex format (with or without 0x prefix)
* @returns Object containing the decoded content and metadata, or null if not a valid CKBFS witness
*/
export function decodeWitnessContent(
witnessHex: string,
): { content: Uint8Array; isValid: boolean } | null {
try {
// Remove 0x prefix if present
const hexData = witnessHex.startsWith("0x")
? witnessHex.slice(2)
: witnessHex;
const witness = Buffer.from(hexData, "hex");
// Check if it's a valid CKBFS witness
if (witness.length < 6) {
return null;
}
// Check CKBFS header
const header = witness.slice(0, 5).toString();
if (header !== "CKBFS") {
return null;
}
// Extract content (skip CKBFS header + version byte)
const content = witness.slice(6);
return {
content,
isValid: true,
};
} catch (error) {
console.warn("Error decoding witness content:", error);
return null;
}
}
/**
* Decodes and combines content from multiple CKBFS witnesses
* @param witnessHexArray Array of witness data in hex format
* @param preserveOrder Whether to preserve the order of witnesses (default: true)
* @returns Combined content from all valid CKBFS witnesses
*/
export function decodeMultipleWitnessContents(
witnessHexArray: string[],
preserveOrder: boolean = true,
): Uint8Array {
const contentPieces: Uint8Array[] = [];
for (let i = 0; i < witnessHexArray.length; i++) {
const witnessHex = witnessHexArray[i];
const decoded = decodeWitnessContent(witnessHex);
if (decoded && decoded.isValid) {
if (preserveOrder) {
contentPieces.push(decoded.content);
} else {
contentPieces.unshift(decoded.content);
}
} else {
console.warn(`Witness at index ${i} is not a valid CKBFS witness`);
}
}
return Buffer.concat(contentPieces);
}
/**
* Extracts complete file content from witnesses using specified indexes
* @param witnesses Array of all witnesses from a transaction
* @param indexes Array of witness indexes that contain CKBFS content
* @returns Combined content from the specified witness indexes
*/
export function extractFileFromWitnesses(
witnesses: string[],
indexes: number[],
): Uint8Array {
const relevantWitnesses: string[] = [];
for (const idx of indexes) {
if (idx >= witnesses.length) {
console.warn(
`Witness index ${idx} out of range (total witnesses: ${witnesses.length})`,
);
continue;
}
relevantWitnesses.push(witnesses[idx]);
}
return decodeMultipleWitnessContents(relevantWitnesses, true);
}
/**
* Decodes file content directly from witness data without blockchain queries
* @param witnessData Object containing witness information
* @returns Object containing the decoded file content and metadata
*/
export function decodeFileFromWitnessData(witnessData: {
witnesses: string[];
indexes: number[] | number;
filename?: string;
contentType?: string;
}): {
content: Uint8Array;
filename?: string;
contentType?: string;
size: number;
} {
const { witnesses, indexes, filename, contentType } = witnessData;
// Normalize indexes to array
const indexArray = Array.isArray(indexes) ? indexes : [indexes];
// Extract content from witnesses
const content = extractFileFromWitnesses(witnesses, indexArray);
return {
content,
filename,
contentType,
size: content.length,
};
}
/**
* Saves decoded file content directly from witness data
* @param witnessData Object containing witness information
* @param outputPath Optional path to save the file
* @returns The path where the file was saved
*/
export function saveFileFromWitnessData(
witnessData: {
witnesses: string[];
indexes: number[] | number;
filename?: string;
contentType?: string;
},
outputPath?: string,
): string {
const decoded = decodeFileFromWitnessData(witnessData);
// Determine output path
const filename = decoded.filename || "decoded_file";
const filePath = outputPath || filename;
// Ensure directory exists
const directory = path.dirname(filePath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
// Write file
fs.writeFileSync(filePath, decoded.content);
console.log(`File saved to: ${filePath}`);
console.log(`Size: ${decoded.size} bytes`);
console.log(`Content type: ${decoded.contentType || "unknown"}`);
return filePath;
}
/**
* Identifier types for CKBFS cells
*/
export enum IdentifierType {
TypeID = "typeId",
OutPoint = "outPoint",
Unknown = "unknown",
}
/**
* Parsed identifier information
*/
export interface ParsedIdentifier {
type: IdentifierType;
typeId?: string;
txHash?: string;
index?: number;
original: string;
}
/**
* Detects and parses different CKBFS identifier formats
* @param identifier The identifier string to parse
* @returns Parsed identifier information
*/
export function parseIdentifier(identifier: string): ParsedIdentifier {
const trimmed = identifier.trim();
// Type 1: Pure TypeID hex string (with or without 0x prefix)
if (trimmed.match(/^(0x)?[a-fA-F0-9]{64}$/)) {
const typeId = trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
return {
type: IdentifierType.TypeID,
typeId,
original: identifier,
};
}
// Type 2: CKBFS URI with TypeID
if (trimmed.startsWith("ckbfs://")) {
const content = trimmed.slice(8); // Remove "ckbfs://"
// Check if it's TypeID format (64 hex characters)
if (content.match(/^[a-fA-F0-9]{64}$/)) {
return {
type: IdentifierType.TypeID,
typeId: `0x${content}`,
original: identifier,
};
}
// Type 3: CKBFS URI with transaction hash and index (txhash + 'i' + index)
const outPointMatch = content.match(/^([a-fA-F0-9]{64})i(\d+)$/);
if (outPointMatch) {
const [, txHash, indexStr] = outPointMatch;
return {
type: IdentifierType.OutPoint,
txHash: `0x${txHash}`,
index: parseInt(indexStr, 10),
original: identifier,
};
}
}
// Unknown format
return {
type: IdentifierType.Unknown,
original: identifier,
};
}
/**
* Resolves a CKBFS cell using any supported identifier format
* @param client The CKB client to use for blockchain queries
* @param identifier The identifier (TypeID, CKBFS URI, or outPoint URI)
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the found cell and transaction info, or null if not found
*/
async function resolveCKBFSCell(
client: any,
identifier: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<{
cell: any;
transaction: any;
outPoint: { txHash: string; index: number };
parsedId: ParsedIdentifier;
} | null> {
const {
network = "testnet",
version = "20241025.db973a8e8032",
useTypeID = false,
} = options;
const parsedId = parseIdentifier(identifier);
try {
if (parsedId.type === IdentifierType.TypeID && parsedId.typeId) {
// Use existing TypeID resolution logic
const cellInfo = await findCKBFSCellByTypeId(
client,
parsedId.typeId,
network,
version,
useTypeID,
);
if (cellInfo) {
return {
...cellInfo,
parsedId,
};
}
} else if (
parsedId.type === IdentifierType.OutPoint &&
parsedId.txHash &&
parsedId.index !== undefined
) {
// Resolve using transaction hash and index
const txWithStatus = await client.getTransaction(parsedId.txHash);
if (!txWithStatus || !txWithStatus.transaction) {
console.warn(`Transaction ${parsedId.txHash} not found`);
return null;
}
// Import Transaction class dynamically
const { Transaction } = await import("@ckb-ccc/core");
const tx = Transaction.from(txWithStatus.transaction);
// Check if the index is valid
if (parsedId.index >= tx.outputs.length) {
console.warn(
`Output index ${parsedId.index} out of range for transaction ${parsedId.txHash}`,
);
return null;
}
const output = tx.outputs[parsedId.index];
// Verify it's a CKBFS cell by checking if it has a type script
if (!output.type) {
console.warn(
`Output at index ${parsedId.index} is not a CKBFS cell (no type script)`,
);
return null;
}
// Create a mock cell object similar to what findSingletonCellByType returns
const cell = {
outPoint: {
txHash: parsedId.txHash,
index: parsedId.index,
},
output,
};
return {
cell,
transaction: txWithStatus.transaction,
outPoint: {
txHash: parsedId.txHash,
index: parsedId.index,
},
parsedId,
};
}
console.warn(
`Unable to resolve identifier: ${identifier} (type: ${parsedId.type})`,
);
return null;
} catch (error) {
console.error(
`Error resolving CKBFS cell for identifier ${identifier}:`,
error,
);
return null;
}
}
/**
* Finds a CKBFS cell by TypeID
* @param client The CKB client to use for blockchain queries
* @param typeId The TypeID (args) of the CKBFS cell to find
* @param network The network type (mainnet or testnet)
* @param version The protocol version to use
* @param useTypeID Whether to use type ID instead of code hash for script matching
* @returns Promise resolving to the found cell and transaction info, or null if not found
*/
async function findCKBFSCellByTypeId(
client: any,
typeId: string,
network: string = "testnet",
version: string = "20241025.db973a8e8032",
useTypeID: boolean = false,
): Promise<{
cell: any;
transaction: any;
outPoint: { txHash: string; index: number };
} | null> {
try {
// Import constants dynamically to avoid circular dependencies
const { getCKBFSScriptConfig, NetworkType, ProtocolVersion } = await import(
"./constants"
);
// Get CKBFS script config
const networkType =
network === "mainnet" ? NetworkType.Mainnet : NetworkType.Testnet;
const protocolVersion =
version === "20240906.ce6724722cf6"
? ProtocolVersion.V1
: ProtocolVersion.V2;
const config = getCKBFSScriptConfig(
networkType,
protocolVersion,
useTypeID,
);
// Create the script to search for
const script = {
codeHash: config.codeHash,
hashType: config.hashType,
args: typeId.startsWith("0x") ? typeId : `0x${typeId}`,
};
// Find the cell by type script
const cell = await client.findSingletonCellByType(script, true);
if (!cell) {
return null;
}
// Get the transaction that contains this cell
const txHash = cell.outPoint.txHash;
const txWithStatus = await client.getTransaction(txHash);
if (!txWithStatus || !txWithStatus.transaction) {
throw new Error(`Transaction ${txHash} not found`);
}
return {
cell,
transaction: txWithStatus.transaction,
outPoint: {
txHash: cell.outPoint.txHash,
index: cell.outPoint.index,
},
};
} catch (error) {
console.warn(`Error finding CKBFS cell by TypeID ${typeId}:`, error);
return null;
}
}
/**
* Retrieves complete file content from the blockchain using any supported identifier
* @param client The CKB client to use for blockchain queries
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the complete file content and metadata
*/
export async function getFileContentFromChainByIdentifier(
client: any,
identifier: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<{
content: Uint8Array;
filename: string;
contentType: string;
checksum: number;
size: number;
backLinks: any[];
parsedId: ParsedIdentifier;
} | null> {
const {
network = "testnet",
version = "20241025.db973a8e8032",
useTypeID = false,
} = options;
try {
// Resolve the CKBFS cell using any supported identifier format
const cellInfo = await resolveCKBFSCell(client, identifier, {
network,
version,
useTypeID,
});
if (!cellInfo) {
console.warn(`CKBFS cell with identifier ${identifier} not found`);
return null;
}
const { cell, transaction, outPoint, parsedId } = cellInfo;
// Import Transaction class dynamically
const { Transaction } = await import("@ckb-ccc/core");
const tx = Transaction.from(transaction);
// Get output data from the cell
const outputIndex = outPoint.index;
const outputData = tx.outputsData[outputIndex];
if (!outputData) {
throw new Error(`Output data not found for cell at index ${outputIndex}`);
}
// Import required modules dynamically
const { ccc } = await import("@ckb-ccc/core");
const { CKBFSData } = await import("./molecule");
const { ProtocolVersion } = await import("./constants");
// Parse the output data
const rawData = outputData.startsWith("0x")
? ccc.bytesFrom(outputData.slice(2), "hex")
: Buffer.from(outputData, "hex");
// Try to unpack CKBFS data with both protocol versions
let ckbfsData: any;
let protocolVersion = version;
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
} catch (error) {
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
protocolVersion = "20240906.ce6724722cf6";
} catch (v1Error) {
throw new Error(
`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
);
}
}
console.log(`Found CKBFS file: ${ckbfsData.filename}`);
console.log(`Content type: ${ckbfsData.contentType}`);
console.log(`Protocol version: ${protocolVersion}`);
// Use existing function to get complete file content
const content = await getFileContentFromChain(client, outPoint, ckbfsData);
return {
content,
filename: ckbfsData.filename,
contentType: ckbfsData.contentType,
checksum: ckbfsData.checksum,
size: content.length,
backLinks: ckbfsData.backLinks || [],
parsedId,
};
} catch (error) {
console.error(`Error retrieving file by identifier ${identifier}:`, error);
throw error;
}
}
/**
* Retrieves complete file content from the blockchain using TypeID (legacy function)
* @param client The CKB client to use for blockchain queries
* @param typeId The TypeID (args) of the CKBFS cell
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the complete file content and metadata
*/
export async function getFileContentFromChainByTypeId(
client: any,
typeId: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<{
content: Uint8Array;
filename: string;
contentType: string;
checksum: number;
size: number;
backLinks: any[];
} | null> {
const result = await getFileContentFromChainByIdentifier(
client,
typeId,
options,
);
if (result) {
const { parsedId, ...fileData } = result;
return fileData;
}
return null;
}
/**
* Saves file content retrieved from blockchain by identifier to disk
* @param client The CKB client to use for blockchain queries
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
* @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the path where the file was saved, or null if file not found
*/
export async function saveFileFromChainByIdentifier(
client: any,
identifier: string,
outputPath?: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<string | null> {
try {
// Get file content by identifier
const fileData = await getFileContentFromChainByIdentifier(
client,
identifier,
options,
);
if (!fileData) {
console.warn(`File with identifier ${identifier} not found`);
return null;
}
// Determine output path
const filePath = outputPath || fileData.filename;
// Ensure directory exists
const directory = path.dirname(filePath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
// Write file
fs.writeFileSync(filePath, fileData.content);
console.log(`File saved to: ${filePath}`);
console.log(`Size: ${fileData.size} bytes`);
console.log(`Content type: ${fileData.contentType}`);
console.log(`Checksum: ${fileData.checksum}`);
return filePath;
} catch (error) {
console.error(`Error saving file by identifier ${identifier}:`, error);
throw error;
}
}
/**
* Saves file content retrieved from blockchain by TypeID to disk (legacy function)
* @param client The CKB client to use for blockchain queries
* @param typeId The TypeID (args) of the CKBFS cell
* @param outputPath Optional path to save the file (defaults to filename from CKBFS data)
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the path where the file was saved, or null if file not found
*/
export async function saveFileFromChainByTypeId(
client: any,
typeId: string,
outputPath?: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<string | null> {
return await saveFileFromChainByIdentifier(
client,
typeId,
outputPath,
options,
);
}
/**
* Decodes file content directly from identifier using witness decoding (new method)
* @param client The CKB client to use for blockchain queries
* @param identifier The identifier (TypeID hex, CKBFS TypeID URI, or CKBFS outPoint URI)
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the decoded file content and metadata, or null if not found
*/
export async function decodeFileFromChainByIdentifier(
client: any,
identifier: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<{
content: Uint8Array;
filename: string;
contentType: string;
checksum: number;
size: number;
backLinks: any[];
parsedId: ParsedIdentifier;
} | null> {
const {
network = "testnet",
version = "20241025.db973a8e8032",
useTypeID = false,
} = options;
try {
// Resolve the CKBFS cell using any supported identifier format
const cellInfo = await resolveCKBFSCell(client, identifier, {
network,
version,
useTypeID,
});
if (!cellInfo) {
console.warn(`CKBFS cell with identifier ${identifier} not found`);
return null;
}
const { cell, transaction, outPoint, parsedId } = cellInfo;
// Import required modules dynamically
const { Transaction, ccc } = await import("@ckb-ccc/core");
const { CKBFSData } = await import("./molecule");
const { ProtocolVersion } = await import("./constants");
const tx = Transaction.from(transaction);
// Get output data from the cell
const outputIndex = outPoint.index;
const outputData = tx.outputsData[outputIndex];
if (!outputData) {
throw new Error(`Output data not found for cell at index ${outputIndex}`);
}
// Parse the output data
const rawData = outputData.startsWith("0x")
? ccc.bytesFrom(outputData.slice(2), "hex")
: Buffer.from(outputData, "hex");
// Try to unpack CKBFS data with both protocol versions
let ckbfsData: any;
let protocolVersion = version;
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
} catch (error) {
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
protocolVersion = "20240906.ce6724722cf6";
} catch (v1Error) {
throw new Error(
`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
);
}
}
console.log(`Found CKBFS file: ${ckbfsData.filename}`);
console.log(`Content type: ${ckbfsData.contentType}`);
console.log(`Using direct witness decoding method`);
// Get witness indexes from CKBFS data
const indexes =
ckbfsData.indexes ||
(ckbfsData.index !== undefined ? [ckbfsData.index] : []);
// Use direct witness decoding method
const content = decodeFileFromWitnessData({
witnesses: tx.witnesses,
indexes: indexes,
filename: ckbfsData.filename,
contentType: ckbfsData.contentType,
});
return {
content: content.content,
filename: ckbfsData.filename,
contentType: ckbfsData.contentType,
checksum: ckbfsData.checksum,
size: content.size,
backLinks: ckbfsData.backLinks || [],
parsedId,
};
} catch (error) {
console.error(`Error decoding file by identifier ${identifier}:`, error);
throw error;
}
}
/**
* Decodes file content directly from TypeID using witness decoding (legacy function)
* @param client The CKB client to use for blockchain queries
* @param typeId The TypeID (args) of the CKBFS cell
* @param options Optional configuration for network, version, and useTypeID
* @returns Promise resolving to the decoded file content and metadata, or null if not found
*/
export async function decodeFileFromChainByTypeId(
client: any,
typeId: string,
options: {
network?: "mainnet" | "testnet";
version?: string;
useTypeID?: boolean;
} = {},
): Promise<{
content: Uint8Array;
filename: string;
contentType: string;
checksum: number;
size: number;
backLinks: any[];
} | null> {
const result = await decodeFileFromChainByIdentifier(client, typeId, options);
if (result) {
const { parsedId, ...fileData } = result;
return fileData;
}
return null;
}