@ckbfs/api
Version:
SDK for CKBFS protocol on CKB
518 lines (454 loc) • 16.3 kB
text/typescript
import { Script, Signer, Transaction, ClientPublicTestnet, SignerCkbPrivateKey } from "@ckb-ccc/core";
import {
calculateChecksum,
verifyChecksum,
updateChecksum,
verifyWitnessChecksum
} from './utils/checksum';
import {
createCKBFSCell,
createPublishTransaction as utilCreatePublishTransaction,
createAppendTransaction as utilCreateAppendTransaction,
publishCKBFS as utilPublishCKBFS,
appendCKBFS as utilAppendCKBFS,
CKBFSCellOptions,
PublishOptions,
AppendOptions
} from './utils/transaction';
import {
readFile,
readFileAsText,
readFileAsUint8Array,
writeFile,
getContentType,
splitFileIntoChunks,
combineChunksToFile,
getFileContentFromChain,
saveFileFromChain
} from './utils/file';
import {
createCKBFSWitness,
createTextCKBFSWitness,
extractCKBFSWitnessContent,
isCKBFSWitness,
createChunkedCKBFSWitnesses
} from './utils/witness';
import {
CKBFSData,
BackLinkV1,
BackLinkV2,
CKBFSDataType,
BackLinkType,
CKBFS_HEADER,
CKBFS_HEADER_STRING
} from './utils/molecule';
import {
NetworkType,
ProtocolVersion,
DEFAULT_NETWORK,
DEFAULT_VERSION,
CKBFS_CODE_HASH,
CKBFS_TYPE_ID,
ADLER32_CODE_HASH,
ADLER32_TYPE_ID,
DEP_GROUP_TX_HASH,
DEPLOY_TX_HASH,
getCKBFSScriptConfig,
CKBFSScriptConfig
} from './utils/constants';
import { ensureHexPrefix } from './utils/transaction';
// Helper to encode string to Uint8Array
const textEncoder = new TextEncoder();
/**
* Custom options for file publishing and appending
*/
export interface FileOptions {
contentType?: string;
filename?: string;
capacity?: bigint;
feeRate?: number;
network?: NetworkType;
version?: string;
useTypeID?: boolean;
}
/**
* Options required when publishing content directly (string or Uint8Array)
*/
export type PublishContentOptions = Omit<FileOptions, 'capacity' | 'contentType' | 'filename'> &
Required<Pick<FileOptions, 'contentType' | 'filename'>> &
{ capacity?: bigint };
/**
* Options required when appending content directly (string or Uint8Array)
*/
export type AppendContentOptions = Omit<FileOptions, 'contentType' | 'filename' | 'capacity'> &
{ capacity?: bigint };
/**
* Configuration options for the CKBFS SDK
*/
export interface CKBFSOptions {
chunkSize?: number;
version?: string;
useTypeID?: boolean;
network?: NetworkType;
}
/**
* Main CKBFS SDK class
*/
export class CKBFS {
private signer: Signer;
private chunkSize: number;
private network: NetworkType;
private version: string;
private useTypeID: boolean;
/**
* Creates a new CKBFS SDK instance
* @param signerOrPrivateKey The signer instance or CKB private key to use for signing transactions
* @param networkOrOptions The network type or configuration options
* @param options Additional configuration options when using privateKey
*/
constructor(
signerOrPrivateKey: Signer | string,
networkOrOptions: NetworkType | CKBFSOptions = DEFAULT_NETWORK,
options?: CKBFSOptions
) {
// Determine if first parameter is a Signer or privateKey
if (typeof signerOrPrivateKey === 'string') {
// Initialize with private key
const privateKey = signerOrPrivateKey;
const network = typeof networkOrOptions === 'string' ? networkOrOptions : DEFAULT_NETWORK;
const opts = options || (typeof networkOrOptions === 'object' ? networkOrOptions : {});
const client = new ClientPublicTestnet();
this.signer = new SignerCkbPrivateKey(client, privateKey);
this.network = network;
this.chunkSize = opts.chunkSize || 30 * 1024;
this.version = opts.version || DEFAULT_VERSION;
this.useTypeID = opts.useTypeID || false;
} else {
// Initialize with signer
this.signer = signerOrPrivateKey;
const opts = typeof networkOrOptions === 'object' ? networkOrOptions : {};
this.network = opts.network || DEFAULT_NETWORK;
this.chunkSize = opts.chunkSize || 30 * 1024;
this.version = opts.version || DEFAULT_VERSION;
this.useTypeID = opts.useTypeID || false;
}
}
/**
* Gets the recommended address object for the signer
* @returns Promise resolving to the address object
*/
async getAddress() {
return this.signer.getRecommendedAddressObj();
}
/**
* Gets the lock script for the signer
* @returns Promise resolving to the lock script
*/
async getLock(): Promise<Script> {
const address = await this.getAddress();
return address.script;
}
/**
* Gets the CKBFS script configuration for the current settings
* @returns The CKBFS script configuration
*/
getCKBFSConfig(): CKBFSScriptConfig {
return getCKBFSScriptConfig(this.network, this.version, this.useTypeID);
}
/**
* Publishes a file to CKBFS
* @param filePath The path to the file to publish
* @param options Options for publishing the file
* @returns Promise resolving to the transaction hash
*/
async publishFile(filePath: string, options: FileOptions = {}): Promise<string> {
// Read the file and split into chunks
const fileContent = readFileAsUint8Array(filePath);
const contentChunks = [];
for (let i = 0; i < fileContent.length; i += this.chunkSize) {
contentChunks.push(fileContent.slice(i, i + this.chunkSize));
}
// Get the lock script
const lock = await this.getLock();
// Determine content type if not provided
const contentType = options.contentType || getContentType(filePath);
// Use the filename from the path if not provided
const pathParts = filePath.split(/[\\\/]/);
const filename = options.filename || pathParts[pathParts.length - 1];
// Create and sign the transaction using the utility function
const tx = await utilPublishCKBFS(this.signer, {
contentChunks,
contentType,
filename,
lock,
capacity: options.capacity,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version,
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
});
console.log('Publish file tx:', tx.stringify());
// Send the transaction
const txHash = await this.signer.sendTransaction(tx);
return ensureHexPrefix(txHash);
}
/**
* Publishes content (string or Uint8Array) directly to CKBFS
* @param content The content string or byte array to publish
* @param options Options for publishing the content (contentType and filename are required)
* @returns Promise resolving to the transaction hash
*/
async publishContent(content: string | Uint8Array, options: PublishContentOptions): Promise<string> {
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
const contentChunks = [];
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
}
const lock = await this.getLock();
// Use provided contentType and filename (required)
const { contentType, filename } = options;
const tx = await utilPublishCKBFS(this.signer, {
contentChunks,
contentType,
filename,
lock,
capacity: options.capacity,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version,
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
});
console.log('Publish content tx:', tx.stringify());
const txHash = await this.signer.sendTransaction(tx);
return ensureHexPrefix(txHash);
}
/**
* Appends content from a file to an existing CKBFS file
* @param filePath The path to the file containing the content to append
* @param ckbfsCell The CKBFS cell to append to
* @param options Additional options for the append operation
* @returns Promise resolving to the transaction hash
*/
async appendFile(
filePath: string,
ckbfsCell: AppendOptions['ckbfsCell'],
options: Omit<FileOptions, 'contentType' | 'filename'> = {}
): Promise<string> {
// Read the file and split into chunks
const fileContent = readFileAsUint8Array(filePath);
const contentChunks = [];
for (let i = 0; i < fileContent.length; i += this.chunkSize) {
contentChunks.push(fileContent.slice(i, i + this.chunkSize));
}
// Create and sign the transaction using the utility function
const tx = await utilAppendCKBFS(this.signer, {
ckbfsCell,
contentChunks,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version
});
console.log('Append file tx:', tx.stringify());
// Send the transaction
const txHash = await this.signer.sendTransaction(tx);
return ensureHexPrefix(txHash);
}
/**
* Appends content (string or Uint8Array) directly to an existing CKBFS file
* @param content The content string or byte array to append
* @param ckbfsCell The CKBFS cell to append to
* @param options Additional options for the append operation
* @returns Promise resolving to the transaction hash
*/
async appendContent(
content: string | Uint8Array,
ckbfsCell: AppendOptions['ckbfsCell'],
options: AppendContentOptions = {}
): Promise<string> {
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
const contentChunks = [];
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
}
const tx = await utilAppendCKBFS(this.signer, {
ckbfsCell,
contentChunks,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version
// No useTypeID option for append
});
console.log('Append content tx:', tx.stringify());
const txHash = await this.signer.sendTransaction(tx);
return ensureHexPrefix(txHash);
}
/**
* Creates a new transaction for publishing a file but doesn't sign or send it
* @param filePath The path to the file to publish
* @param options Options for publishing the file
* @returns Promise resolving to the unsigned transaction
*/
async createPublishTransaction(filePath: string, options: FileOptions = {}): Promise<Transaction> {
// Read the file and split into chunks
const fileContent = readFileAsUint8Array(filePath);
const contentChunks = [];
for (let i = 0; i < fileContent.length; i += this.chunkSize) {
contentChunks.push(fileContent.slice(i, i + this.chunkSize));
}
// Get the lock script
const lock = await this.getLock();
// Determine content type if not provided
const contentType = options.contentType || getContentType(filePath);
// Use the filename from the path if not provided
const pathParts = filePath.split(/[\\\/]/);
const filename = options.filename || pathParts[pathParts.length - 1];
// Create the transaction using the utility function
return utilCreatePublishTransaction(this.signer, {
contentChunks,
contentType,
filename,
lock,
capacity: options.capacity,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version,
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
});
}
/**
* Creates a new transaction for publishing content (string or Uint8Array) directly, but doesn't sign or send it
* @param content The content string or byte array to publish
* @param options Options for publishing the content (contentType and filename are required)
* @returns Promise resolving to the unsigned transaction
*/
async createPublishContentTransaction(content: string | Uint8Array, options: PublishContentOptions): Promise<Transaction> {
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
const contentChunks = [];
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
}
const lock = await this.getLock();
// Use provided contentType and filename (required)
const { contentType, filename } = options;
return utilCreatePublishTransaction(this.signer, {
contentChunks,
contentType,
filename,
lock,
capacity: options.capacity,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version,
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
});
}
/**
* Creates a new transaction for appending content from a file but doesn't sign or send it
* @param filePath The path to the file containing the content to append
* @param ckbfsCell The CKBFS cell to append to
* @param options Additional options for the append operation
* @returns Promise resolving to the unsigned transaction
*/
async createAppendTransaction(
filePath: string,
ckbfsCell: AppendOptions['ckbfsCell'],
options: Omit<FileOptions, 'contentType' | 'filename'> = {}
): Promise<Transaction> {
// Read the file and split into chunks
const fileContent = readFileAsUint8Array(filePath);
const contentChunks = [];
for (let i = 0; i < fileContent.length; i += this.chunkSize) {
contentChunks.push(fileContent.slice(i, i + this.chunkSize));
}
// Create the transaction using the utility function
return utilCreateAppendTransaction(this.signer, {
ckbfsCell,
contentChunks,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version
});
}
/**
* Creates a new transaction for appending content (string or Uint8Array) directly, but doesn't sign or send it
* @param content The content string or byte array to append
* @param ckbfsCell The CKBFS cell to append to
* @param options Additional options for the append operation
* @returns Promise resolving to the unsigned transaction
*/
async createAppendContentTransaction(
content: string | Uint8Array,
ckbfsCell: AppendOptions['ckbfsCell'],
options: AppendContentOptions = {}
): Promise<Transaction> {
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
const contentChunks = [];
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
}
return utilCreateAppendTransaction(this.signer, {
ckbfsCell,
contentChunks,
feeRate: options.feeRate,
network: options.network || this.network,
version: options.version || this.version
// No useTypeID option for append
});
}
}
// Export utility functions
export {
// Checksum utilities
calculateChecksum,
verifyChecksum,
updateChecksum,
verifyWitnessChecksum,
// Transaction utilities (Exporting original names from transaction.ts)
createCKBFSCell,
utilCreatePublishTransaction as createPublishTransaction,
utilCreateAppendTransaction as createAppendTransaction,
utilPublishCKBFS as publishCKBFS,
utilAppendCKBFS as appendCKBFS,
// File utilities
readFile,
readFileAsText,
readFileAsUint8Array,
writeFile,
getContentType,
splitFileIntoChunks,
combineChunksToFile,
getFileContentFromChain,
saveFileFromChain,
// Witness utilities
createCKBFSWitness,
createTextCKBFSWitness,
extractCKBFSWitnessContent,
isCKBFSWitness,
createChunkedCKBFSWitnesses,
// Molecule definitions
CKBFSData,
BackLinkV1,
BackLinkV2,
// Types
CKBFSDataType,
BackLinkType,
CKBFSCellOptions,
PublishOptions,
AppendOptions,
// Constants
CKBFS_HEADER,
CKBFS_HEADER_STRING,
// CKBFS Protocol Constants & Configuration
NetworkType,
ProtocolVersion,
DEFAULT_NETWORK,
DEFAULT_VERSION,
CKBFS_CODE_HASH,
CKBFS_TYPE_ID,
ADLER32_CODE_HASH,
ADLER32_TYPE_ID,
DEP_GROUP_TX_HASH,
DEPLOY_TX_HASH,
getCKBFSScriptConfig,
CKBFSScriptConfig
};