@ckbfs/api
Version:
SDK for CKBFS protocol on CKB
581 lines (502 loc) • 19.4 kB
text/typescript
import {
CKBFS,
NetworkType,
ProtocolVersion,
ProtocolVersionType,
CKBFSDataType,
CKBFSData,
decodeWitnessContent,
decodeMultipleWitnessContents,
extractFileFromWitnesses,
decodeFileFromWitnessData,
saveFileFromWitnessData,
getFileContentFromChain,
saveFileFromChain,
getFileContentFromChainByTypeId,
saveFileFromChainByTypeId,
decodeFileFromChainByTypeId,
getFileContentFromChainByIdentifier,
saveFileFromChainByIdentifier,
decodeFileFromChainByIdentifier,
parseIdentifier,
IdentifierType,
} from "../src/index";
import { Script, ClientPublicTestnet, Transaction, ccc } from "@ckb-ccc/core";
// Replace with your actual private key (optional for this example)
const privateKey = process.env.CKB_PRIVATE_KEY || "your-private-key-here";
// Parse command line arguments for transaction hash
const txHashArg = process.argv.find((arg) => arg.startsWith("--txhash="));
const targetTxHash = txHashArg
? txHashArg.split("=")[1]
: process.env.TARGET_TX_HASH ||
"0x0000000000000000000000000000000000000000000000000000000000000000";
/**
* Ensures a string is prefixed with '0x'
* @param value The string to ensure is hex prefixed
* @returns A hex prefixed string
*/
function ensureHexPrefix(value: string): `0x${string}` {
if (value.startsWith("0x")) {
return value as `0x${string}`;
}
return `0x${value}` as `0x${string}`;
}
// Initialize CKB client for testnet
const client = new ClientPublicTestnet();
/**
* Example of retrieving and decoding file content using traditional blockchain method
*/
async function traditionalRetrieveExample() {
console.log("=== Traditional Blockchain Retrieval Method ===");
try {
// Get transaction from blockchain
const txWithStatus = await client.getTransaction(targetTxHash);
if (!txWithStatus || !txWithStatus.transaction) {
throw new Error(`Transaction ${targetTxHash} not found`);
}
const tx = Transaction.from(txWithStatus.transaction);
console.log(`Transaction found with ${tx.outputs.length} outputs`);
// Find CKBFS cell (first output with type script)
const ckbfsCellIndex = 0;
const output = tx.outputs[ckbfsCellIndex];
if (!output || !output.type) {
throw new Error("No CKBFS cell found in the transaction");
}
// Get and parse output data
const outputData = tx.outputsData[ckbfsCellIndex];
if (!outputData) {
throw new Error("Output data not found");
}
const rawData = outputData.startsWith("0x")
? ccc.bytesFrom(outputData.slice(2), "hex")
: Buffer.from(outputData, "hex");
// Try both protocol versions for unpacking
let ckbfsData: CKBFSDataType;
let version: ProtocolVersionType;
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
version = ProtocolVersion.V2;
} catch (error) {
console.log("Failed to unpack as V2, trying V1...");
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
version = ProtocolVersion.V1;
} catch (v1Error) {
throw new Error(
`Failed to unpack CKBFS data with both versions: V2(${error}), V1(${v1Error})`,
);
}
}
console.log(`Successfully unpacked CKBFS data using ${version}:`);
console.log(`- Filename: ${ckbfsData.filename}`);
console.log(`- Content Type: ${ckbfsData.contentType}`);
console.log(`- Checksum: ${ckbfsData.checksum}`);
console.log(`- Backlinks: ${ckbfsData.backLinks?.length || 0}`);
// Retrieve complete file content using traditional method
const outPoint = { txHash: targetTxHash, index: ckbfsCellIndex };
const content = await getFileContentFromChain(client, outPoint, ckbfsData);
// Save file using traditional method
const savedPath = saveFileFromChain(
content,
ckbfsData,
`./traditional_${ckbfsData.filename}`,
);
console.log(`Traditional method completed. File saved to: ${savedPath}`);
console.log(`File size: ${content.length} bytes`);
return { ckbfsData, tx, content };
} catch (error) {
console.error("Traditional retrieval failed:", error);
throw error;
}
}
/**
* Example of decoding file content directly from witnesses (new method)
*/
async function directWitnessDecodingExample() {
console.log("\n=== Direct Witness Decoding Method (New) ===");
try {
// Get transaction from blockchain
const txWithStatus = await client.getTransaction(targetTxHash);
if (!txWithStatus || !txWithStatus.transaction) {
throw new Error(`Transaction ${targetTxHash} not found`);
}
const tx = Transaction.from(txWithStatus.transaction);
// Get CKBFS data to know which witnesses contain content
const outputData = tx.outputsData[0];
if (!outputData) {
throw new Error("Output data not found");
}
const rawData = outputData.startsWith("0x")
? ccc.bytesFrom(outputData.slice(2), "hex")
: Buffer.from(outputData, "hex");
let ckbfsData: CKBFSDataType;
let version: ProtocolVersionType;
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
version = ProtocolVersion.V2;
} catch (error) {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
version = ProtocolVersion.V1;
}
console.log(`Using protocol ${version} for witness decoding`);
// Get witness indexes from CKBFS data
const indexes =
ckbfsData.indexes ||
(ckbfsData.index !== undefined ? [ckbfsData.index] : []);
console.log(`Content witness indexes: ${indexes.join(", ")}`);
// Example 1: Decode individual witnesses
console.log("\n--- Example 1: Decode Individual Witnesses ---");
for (const idx of indexes) {
if (idx < tx.witnesses.length) {
const decoded = decodeWitnessContent(tx.witnesses[idx]);
if (decoded) {
console.log(
`Witness ${idx}: ${decoded.content.length} bytes, valid: ${decoded.isValid}`,
);
} else {
console.log(`Witness ${idx}: Not a valid CKBFS witness`);
}
}
}
// Example 2: Decode multiple witnesses at once
console.log("\n--- Example 2: Decode Multiple Witnesses ---");
const relevantWitnesses = indexes
.map((idx) => tx.witnesses[idx])
.filter(Boolean);
const combinedContent = decodeMultipleWitnessContents(
relevantWitnesses,
true,
);
console.log(
`Combined content from ${relevantWitnesses.length} witnesses: ${combinedContent.length} bytes`,
);
// Example 3: Extract file using witness indexes
console.log("\n--- Example 3: Extract File Using Indexes ---");
const extractedContent = extractFileFromWitnesses(tx.witnesses, indexes);
console.log(`Extracted content: ${extractedContent.length} bytes`);
// Example 4: High-level decode with metadata
console.log("\n--- Example 4: High-level Decode with Metadata ---");
const decodedFile = decodeFileFromWitnessData({
witnesses: tx.witnesses,
indexes: indexes,
filename: ckbfsData.filename,
contentType: ckbfsData.contentType,
});
console.log(`Decoded file:`);
console.log(`- Filename: ${decodedFile.filename}`);
console.log(`- Content Type: ${decodedFile.contentType}`);
console.log(`- Size: ${decodedFile.size} bytes`);
// Example 5: Save file directly from witness data
console.log("\n--- Example 5: Save File from Witness Data ---");
const savedPath = saveFileFromWitnessData(
{
witnesses: tx.witnesses,
indexes: indexes,
filename: ckbfsData.filename,
contentType: ckbfsData.contentType,
},
`./direct_${ckbfsData.filename}`,
);
console.log(
`Direct witness decoding completed. File saved to: ${savedPath}`,
);
return { decodedFile, extractedContent };
} catch (error) {
console.error("Direct witness decoding failed:", error);
throw error;
}
}
/**
* Example of handling backlinks with direct witness decoding
*/
async function backlinksDecodingExample() {
console.log("\n=== Backlinks Decoding Example ===");
try {
// Get main transaction
const txWithStatus = await client.getTransaction(targetTxHash);
if (!txWithStatus || !txWithStatus.transaction) {
throw new Error(`Transaction ${targetTxHash} not found`);
}
const tx = Transaction.from(txWithStatus.transaction);
const outputData = tx.outputsData[0];
const rawData = outputData.startsWith("0x")
? ccc.bytesFrom(outputData.slice(2), "hex")
: Buffer.from(outputData, "hex");
let ckbfsData: CKBFSDataType;
try {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
} catch (error) {
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
}
console.log(`Found ${ckbfsData.backLinks?.length || 0} backlinks`);
if (ckbfsData.backLinks && ckbfsData.backLinks.length > 0) {
// Process each backlink
for (let i = 0; i < ckbfsData.backLinks.length; i++) {
const backlink = ckbfsData.backLinks[i];
console.log(`\nProcessing backlink ${i + 1}:`);
console.log(`- Transaction: ${backlink.txHash}`);
console.log(`- Checksum: ${backlink.checksum}`);
try {
// Get backlink transaction
const backTxWithStatus = await client.getTransaction(backlink.txHash);
if (backTxWithStatus && backTxWithStatus.transaction) {
const backTx = Transaction.from(backTxWithStatus.transaction);
// Get backlink indexes
const backIndexes =
backlink.indexes ||
(backlink.index !== undefined ? [backlink.index] : []);
console.log(`- Witness indexes: ${backIndexes.join(", ")}`);
// Decode content from backlink witnesses
const backlinkContent = extractFileFromWitnesses(
backTx.witnesses,
backIndexes,
);
console.log(`- Content size: ${backlinkContent.length} bytes`);
// Save backlink content
const backlinkPath = `./backlink_${i + 1}_content.bin`;
require("fs").writeFileSync(backlinkPath, backlinkContent);
console.log(`- Saved to: ${backlinkPath}`);
}
} catch (error) {
console.warn(`Failed to process backlink ${i + 1}: ${error}`);
}
}
} else {
console.log("No backlinks found in this file");
}
} catch (error) {
console.error("Backlinks decoding failed:", error);
throw error;
}
}
/**
* Example of retrieving files using generic identifiers (new method)
*/
async function genericIdentifierRetrievalExample() {
console.log("\n=== Generic Identifier Retrieval Example ===");
try {
// Get transaction to extract TypeID from it
const txWithStatus = await client.getTransaction(targetTxHash);
if (!txWithStatus || !txWithStatus.transaction) {
throw new Error(`Transaction ${targetTxHash} not found`);
}
const tx = Transaction.from(txWithStatus.transaction);
// Find CKBFS cell (first output with type script)
const ckbfsCellIndex = 0;
const output = tx.outputs[ckbfsCellIndex];
if (!output || !output.type) {
throw new Error("No CKBFS cell found in the transaction");
}
// Extract TypeID from the type script args
const typeId = output.type.args;
console.log(`Extracted TypeID: ${typeId}`);
if (!typeId || typeId === "0x") {
console.log(
"No TypeID found in this transaction, skipping identifier examples",
);
return;
}
// Create different identifier formats for demonstration
const identifiers = [
typeId, // Pure TypeID hex string
`ckbfs://${typeId.slice(2)}`, // CKBFS URI with TypeID
`ckbfs://${targetTxHash.slice(2)}i${ckbfsCellIndex}`, // CKBFS URI with outPoint
];
console.log("\nTesting different identifier formats:");
identifiers.forEach((id, index) => {
const parsed = parseIdentifier(id);
console.log(`${index + 1}. ${id}`);
console.log(` Type: ${parsed.type}`);
if (parsed.typeId) console.log(` TypeID: ${parsed.typeId}`);
if (parsed.txHash)
console.log(` TxHash: ${parsed.txHash}, Index: ${parsed.index}`);
});
// Example 1: Get file content using generic identifier (traditional method with backlinks)
console.log(
"\n--- Example 1: Get File Content by Generic Identifier (Traditional) ---",
);
// Test with TypeID format
const fileData = await getFileContentFromChainByIdentifier(
client,
identifiers[0],
{
network: "testnet",
version: ProtocolVersion.V2,
useTypeID: false,
},
);
if (fileData) {
console.log(`Retrieved file: ${fileData.filename}`);
console.log(`Content type: ${fileData.contentType}`);
console.log(`Size: ${fileData.size} bytes`);
console.log(`Checksum: ${fileData.checksum}`);
console.log(`Backlinks: ${fileData.backLinks.length}`);
console.log(`Parsed identifier type: ${fileData.parsedId.type}`);
} else {
console.log("File not found by identifier");
return;
}
// Example 2: Test all identifier formats
console.log("\n--- Example 2: Test All Identifier Formats ---");
for (let i = 0; i < identifiers.length; i++) {
const identifier = identifiers[i];
console.log(`\nTesting identifier ${i + 1}: ${identifier}`);
try {
const testData = await getFileContentFromChainByIdentifier(
client,
identifier,
{
network: "testnet",
version: ProtocolVersion.V2,
useTypeID: false,
},
);
if (testData) {
console.log(
`✓ Success: ${testData.filename} (${testData.size} bytes)`,
);
console.log(` Parsed as: ${testData.parsedId.type}`);
} else {
console.log(`✗ Failed: File not found`);
}
} catch (error) {
console.log(`✗ Error: ${error}`);
}
}
// Example 3: Save file using CKBFS URI format
console.log("\n--- Example 3: Save File using CKBFS URI ---");
const savedPath = await saveFileFromChainByIdentifier(
client,
identifiers[1], // Use CKBFS URI format
`./identifier_${fileData.filename}`,
{
network: "testnet",
version: ProtocolVersion.V2,
useTypeID: false,
},
);
if (savedPath) {
console.log(`File saved via identifier to: ${savedPath}`);
}
// Example 4: Decode file using outPoint format with direct witness method
console.log(
"\n--- Example 4: Decode File using OutPoint Format (Direct Witness) ---",
);
const decodedData = await decodeFileFromChainByIdentifier(
client,
identifiers[2],
{
network: "testnet",
version: ProtocolVersion.V2,
useTypeID: false,
},
);
if (decodedData) {
console.log(`Decoded file: ${decodedData.filename}`);
console.log(`Content type: ${decodedData.contentType}`);
console.log(`Size: ${decodedData.size} bytes`);
console.log(`Checksum: ${decodedData.checksum}`);
console.log(`Parsed identifier type: ${decodedData.parsedId.type}`);
// Verify content matches
const contentMatches =
Buffer.compare(fileData.content, decodedData.content) === 0;
console.log(
`Content verification: ${contentMatches ? "PASSED" : "FAILED"}`,
);
}
console.log("\nGeneric identifier retrieval completed successfully!");
return { fileData, decodedData, identifiers };
} catch (error) {
console.error("Generic identifier retrieval failed:", error);
throw error;
}
}
/**
* Performance comparison between traditional and direct methods
*/
async function performanceComparisonExample() {
console.log("\n=== Performance Comparison ===");
try {
// Traditional method timing
const traditionalStart = Date.now();
const traditionalResult = await traditionalRetrieveExample();
const traditionalTime = Date.now() - traditionalStart;
// Direct method timing
const directStart = Date.now();
const directResult = await directWitnessDecodingExample();
const directTime = Date.now() - directStart;
console.log("\n--- Performance Results ---");
console.log(`Traditional method: ${traditionalTime}ms`);
console.log(`Direct witness method: ${directTime}ms`);
console.log(
`Performance improvement: ${(((traditionalTime - directTime) / traditionalTime) * 100).toFixed(1)}%`,
);
// Verify content matches
const contentMatches =
Buffer.compare(
traditionalResult.content,
directResult.extractedContent,
) === 0;
console.log(
`Content verification: ${contentMatches ? "PASSED" : "FAILED"}`,
);
} catch (error) {
console.error("Performance comparison failed:", error);
}
}
/**
* Main function to run all examples
*/
async function main() {
console.log("Running CKBFS retrieve and witness decoding examples...");
console.log("=========================================================");
console.log(`Target transaction: ${targetTxHash}`);
// Validate transaction hash
if (
targetTxHash ===
"0x0000000000000000000000000000000000000000000000000000000000000000"
) {
console.error(
"Please provide a valid transaction hash by setting the TARGET_TX_HASH environment variable or using --txhash=0x... argument",
);
console.log("\nExample usage:");
console.log(" npm run example:retrieve -- --txhash=0x123456...");
console.log(" TARGET_TX_HASH=0x123456... npm run example:retrieve");
process.exit(1);
}
try {
// Run traditional method
await traditionalRetrieveExample();
// Run new direct witness decoding methods
await directWitnessDecodingExample();
// Handle backlinks if present
await backlinksDecodingExample();
// Run generic identifier-based retrieval examples
await genericIdentifierRetrievalExample();
console.log("\n=== Summary ===");
console.log("All examples completed successfully!");
console.log("\nKey advantages of direct witness decoding:");
console.log("- No need for recursive blockchain queries");
console.log("- Faster performance for simple cases");
console.log("- More control over witness processing");
console.log("- Useful for offline processing with cached data");
console.log("\nTraditional method advantages:");
console.log("- Automatic backlink following");
console.log("- Complete file reconstruction");
console.log("- Built-in error handling for complex cases");
console.log("\nGeneric identifier method advantages:");
console.log("- Flexible input formats (TypeID, CKBFS URIs, outPoint)");
console.log("- Simple interface - works with any identifier format");
console.log("- No need to know specific format beforehand");
console.log("- Automatic format detection and parsing");
console.log("- Works with both traditional and direct witness decoding");
process.exit(0);
} catch (error) {
console.error("Examples failed:", error);
process.exit(1);
}
}
// Run the example if this file is executed directly
if (require.main === module) {
main().catch(console.error);
}