@ckbfs/api
Version:
SDK for CKBFS protocol on CKB
601 lines (537 loc) • 17.8 kB
text/typescript
import {
Script,
Signer,
Transaction,
ClientPublicTestnet,
SignerCkbPrivateKey,
ClientPublicMainnet,
} from "@ckb-ccc/core";
import {
calculateChecksum,
verifyChecksum,
updateChecksum,
verifyWitnessChecksum,
} from "./utils/checksum";
import {
createCKBFSCell,
createPublishTransaction as utilCreatePublishTransaction,
preparePublishTransaction,
createAppendTransaction as utilCreateAppendTransaction,
prepareAppendTransaction,
createAppendTransactionDry,
publishCKBFS as utilPublishCKBFS,
appendCKBFS as utilAppendCKBFS,
CKBFSCellOptions,
PublishOptions,
AppendOptions,
} from "./utils/transaction";
import {
readFile,
readFileAsText,
readFileAsUint8Array,
writeFile,
getContentType,
splitFileIntoChunks,
combineChunksToFile,
getFileContentFromChain,
saveFileFromChain,
getFileContentFromChainByTypeId,
saveFileFromChainByTypeId,
decodeFileFromChainByTypeId,
getFileContentFromChainByIdentifier,
saveFileFromChainByIdentifier,
decodeFileFromChainByIdentifier,
parseIdentifier,
IdentifierType,
decodeWitnessContent,
decodeMultipleWitnessContents,
extractFileFromWitnesses,
decodeFileFromWitnessData,
saveFileFromWitnessData,
} 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,
ProtocolVersionType,
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?: ProtocolVersionType;
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?: ProtocolVersionType;
useTypeID?: boolean;
network?: NetworkType;
rpcUrl?: string;
}
/**
* Main CKBFS SDK class
*/
export class CKBFS {
private signer: Signer;
private chunkSize: number;
private network: NetworkType;
private version: ProtocolVersionType;
private useTypeID: boolean;
private rpcUrl: string;
/**
* 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 =
network === "mainnet"
? new ClientPublicMainnet({
url: opts.rpcUrl,
})
: new ClientPublicTestnet({
url: opts.rpcUrl,
});
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;
this.rpcUrl = opts.rpcUrl || client.url;
} 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;
this.rpcUrl = opts.rpcUrl || this.signer.client.url;
}
}
/**
* 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,
preparePublishTransaction,
utilCreateAppendTransaction as createAppendTransaction,
prepareAppendTransaction,
utilPublishCKBFS as publishCKBFS,
utilAppendCKBFS as appendCKBFS,
createAppendTransactionDry,
// File utilities
readFile,
readFileAsText,
readFileAsUint8Array,
writeFile,
getContentType,
splitFileIntoChunks,
combineChunksToFile,
getFileContentFromChain,
saveFileFromChain,
getFileContentFromChainByTypeId,
saveFileFromChainByTypeId,
decodeFileFromChainByTypeId,
getFileContentFromChainByIdentifier,
saveFileFromChainByIdentifier,
decodeFileFromChainByIdentifier,
parseIdentifier,
IdentifierType,
decodeWitnessContent,
decodeMultipleWitnessContents,
extractFileFromWitnesses,
decodeFileFromWitnessData,
saveFileFromWitnessData,
// 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,
ProtocolVersionType,
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,
};