@ckbfs/api 1.0.0

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.
@@ -0,0 +1,43 @@
1
+ import { CKBFS, NetworkType, ProtocolVersion } from '../src/index';
2
+
3
+ /**
4
+ * Main CKBFS Examples
5
+ *
6
+ * This file serves as the entry point for running all CKBFS SDK examples.
7
+ *
8
+ * To run all examples:
9
+ * npm run example
10
+ *
11
+ * To run specific examples:
12
+ * npm run example:publish
13
+ * npm run example:append -- --txhash=0x123456...
14
+ */
15
+
16
+ console.log('CKBFS SDK Examples');
17
+ console.log('=================');
18
+ console.log('');
19
+ console.log('Available examples:');
20
+ console.log('1. Publish File Example - npm run example:publish');
21
+ console.log('2. Append File Example - npm run example:append -- --txhash=0x123456...');
22
+ console.log('');
23
+ console.log('Run all examples with: npm run example');
24
+ console.log('');
25
+ console.log('Note: For the append example, you need to provide a transaction hash');
26
+ console.log('of a previously published file using the --txhash parameter or');
27
+ console.log('by setting the PUBLISH_TX_HASH environment variable.');
28
+ console.log('');
29
+
30
+ // Check if we should run all examples
31
+ const runAll = process.argv.includes('--all');
32
+
33
+ if (runAll) {
34
+ console.log('Running all examples is not recommended. Please run specific examples instead.');
35
+ console.log('');
36
+ console.log('For example:');
37
+ console.log(' npm run example:publish');
38
+ console.log(' npm run example:append -- --txhash=0x123456...');
39
+ process.exit(1);
40
+ }
41
+
42
+ // Default behavior - just show instructions
43
+ console.log('For more information, see the README.md file.');
@@ -0,0 +1,76 @@
1
+ import { CKBFS, NetworkType, ProtocolVersion } from '../src/index';
2
+
3
+ // Replace with your actual private key
4
+ const privateKey = process.env.CKB_PRIVATE_KEY || 'your-private-key-here';
5
+
6
+ // Initialize the SDK with network and version options
7
+ const ckbfs = new CKBFS(
8
+ privateKey,
9
+ NetworkType.Testnet, // Use testnet
10
+ {
11
+ version: ProtocolVersion.V2, // Use the latest version (V2)
12
+ chunkSize: 30 * 1024, // 30KB chunks
13
+ useTypeID: false // Use code hash instead of type ID
14
+ }
15
+ );
16
+
17
+ /**
18
+ * Example of publishing a file to CKBFS
19
+ */
20
+ async function publishExample() {
21
+ try {
22
+ // Get address info
23
+ const address = await ckbfs.getAddress();
24
+ console.log(`Using address: ${address.toString()}`);
25
+
26
+ // Get CKBFS script config
27
+ const config = ckbfs.getCKBFSConfig();
28
+ console.log('Using CKBFS config:', config);
29
+
30
+ // Publish a text file to CKBFS
31
+ const filePath = './example.txt';
32
+
33
+ // You can provide additional options
34
+ const options = {
35
+ contentType: 'text/plain',
36
+ filename: 'example.txt',
37
+ // Specify capacity if needed (default is 200 CKB)
38
+ // capacity: 250n * 100000000n
39
+ };
40
+
41
+ console.log(`Publishing file: ${filePath}`);
42
+ const txHash = await ckbfs.publishFile(filePath, options);
43
+
44
+ console.log(`File published successfully!`);
45
+ console.log(`Transaction Hash: ${txHash}`);
46
+ console.log(`View at: https://pudge.explorer.nervos.org/transaction/${txHash}`);
47
+
48
+ return txHash;
49
+ } catch (error) {
50
+ console.error('Error publishing file:', error);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Main function to run the example
57
+ */
58
+ async function main() {
59
+ console.log('Running CKBFS publishing example...');
60
+ console.log('----------------------------------');
61
+ console.log(`Using CKBFS protocol version: ${ProtocolVersion.V2}`);
62
+
63
+ try {
64
+ await publishExample();
65
+ console.log('Example completed successfully!');
66
+ process.exit(0);
67
+ } catch (error) {
68
+ console.error('Example failed:', error);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ // Run the example if this file is executed directly
74
+ if (require.main === module) {
75
+ main().catch(console.error);
76
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@ckbfs/api",
3
+ "version": "1.0.0",
4
+ "description": "SDK for CKBFS protocol on CKB",
5
+ "license": "MIT",
6
+ "author": "Code Monad<code@lab-11.org>",
7
+ "type": "commonjs",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "ts-node src/index.ts",
14
+ "test": "jest",
15
+ "example": "ts-node examples/index.ts",
16
+ "example:publish": "ts-node examples/publish.ts",
17
+ "example:append": "ts-node examples/append.ts"
18
+ },
19
+ "keywords": [
20
+ "ckb",
21
+ "ckbfs",
22
+ "nervos",
23
+ "sdk"
24
+ ],
25
+ "devDependencies": {
26
+ "@types/node": "^22.7.9",
27
+ "ts-node": "^10.9.2",
28
+ "typescript": "^5.6.3",
29
+ "jest": "^29.7.0",
30
+ "ts-jest": "^29.1.2",
31
+ "@types/jest": "^29.5.12"
32
+ },
33
+ "dependencies": {
34
+ "@ckb-ccc/core": "^0.1.0-alpha.4",
35
+ "@ckb-lumos/base": "^0.23.0",
36
+ "@ckb-lumos/codec": "^0.23.0",
37
+ "hash-wasm": "^4.11.0"
38
+ }
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,363 @@
1
+ import { Script, Signer, Transaction, ClientPublicTestnet, SignerCkbPrivateKey } from "@ckb-ccc/core";
2
+ import {
3
+ calculateChecksum,
4
+ verifyChecksum,
5
+ updateChecksum,
6
+ verifyWitnessChecksum
7
+ } from './utils/checksum';
8
+ import {
9
+ createCKBFSCell,
10
+ createPublishTransaction,
11
+ createAppendTransaction,
12
+ publishCKBFS,
13
+ appendCKBFS,
14
+ CKBFSCellOptions,
15
+ PublishOptions,
16
+ AppendOptions
17
+ } from './utils/transaction';
18
+ import {
19
+ readFile,
20
+ readFileAsText,
21
+ readFileAsUint8Array,
22
+ writeFile,
23
+ getContentType,
24
+ splitFileIntoChunks,
25
+ combineChunksToFile
26
+ } from './utils/file';
27
+ import {
28
+ createCKBFSWitness,
29
+ createTextCKBFSWitness,
30
+ extractCKBFSWitnessContent,
31
+ isCKBFSWitness,
32
+ createChunkedCKBFSWitnesses
33
+ } from './utils/witness';
34
+ import {
35
+ CKBFSData,
36
+ BackLinkV1,
37
+ BackLinkV2,
38
+ CKBFSDataType,
39
+ BackLinkType,
40
+ CKBFS_HEADER,
41
+ CKBFS_HEADER_STRING
42
+ } from './utils/molecule';
43
+ import {
44
+ NetworkType,
45
+ ProtocolVersion,
46
+ DEFAULT_NETWORK,
47
+ DEFAULT_VERSION,
48
+ CKBFS_CODE_HASH,
49
+ CKBFS_TYPE_ID,
50
+ ADLER32_CODE_HASH,
51
+ ADLER32_TYPE_ID,
52
+ DEP_GROUP_TX_HASH,
53
+ DEPLOY_TX_HASH,
54
+ getCKBFSScriptConfig,
55
+ CKBFSScriptConfig
56
+ } from './utils/constants';
57
+
58
+ /**
59
+ * Custom options for file publishing and appending
60
+ */
61
+ export interface FileOptions {
62
+ contentType?: string;
63
+ filename?: string;
64
+ capacity?: bigint;
65
+ feeRate?: number;
66
+ network?: NetworkType;
67
+ version?: string;
68
+ useTypeID?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Configuration options for the CKBFS SDK
73
+ */
74
+ export interface CKBFSOptions {
75
+ chunkSize?: number;
76
+ version?: string;
77
+ useTypeID?: boolean;
78
+ network?: NetworkType;
79
+ }
80
+
81
+ /**
82
+ * Main CKBFS SDK class
83
+ */
84
+ export class CKBFS {
85
+ private signer: Signer;
86
+ private chunkSize: number;
87
+ private network: NetworkType;
88
+ private version: string;
89
+ private useTypeID: boolean;
90
+
91
+ /**
92
+ * Creates a new CKBFS SDK instance
93
+ * @param signerOrPrivateKey The signer instance or CKB private key to use for signing transactions
94
+ * @param networkOrOptions The network type or configuration options
95
+ * @param options Additional configuration options when using privateKey
96
+ */
97
+ constructor(
98
+ signerOrPrivateKey: Signer | string,
99
+ networkOrOptions: NetworkType | CKBFSOptions = DEFAULT_NETWORK,
100
+ options?: CKBFSOptions
101
+ ) {
102
+ // Determine if first parameter is a Signer or privateKey
103
+ if (typeof signerOrPrivateKey === 'string') {
104
+ // Initialize with private key
105
+ const privateKey = signerOrPrivateKey;
106
+ const network = typeof networkOrOptions === 'string' ? networkOrOptions : DEFAULT_NETWORK;
107
+ const opts = options || (typeof networkOrOptions === 'object' ? networkOrOptions : {});
108
+
109
+ const client = new ClientPublicTestnet();
110
+ this.signer = new SignerCkbPrivateKey(client, privateKey);
111
+ this.network = network;
112
+ this.chunkSize = opts.chunkSize || 30 * 1024;
113
+ this.version = opts.version || DEFAULT_VERSION;
114
+ this.useTypeID = opts.useTypeID || false;
115
+ } else {
116
+ // Initialize with signer
117
+ this.signer = signerOrPrivateKey;
118
+ const opts = typeof networkOrOptions === 'object' ? networkOrOptions : {};
119
+
120
+ this.network = opts.network || DEFAULT_NETWORK;
121
+ this.chunkSize = opts.chunkSize || 30 * 1024;
122
+ this.version = opts.version || DEFAULT_VERSION;
123
+ this.useTypeID = opts.useTypeID || false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Gets the recommended address object for the signer
129
+ * @returns Promise resolving to the address object
130
+ */
131
+ async getAddress() {
132
+ return this.signer.getRecommendedAddressObj();
133
+ }
134
+
135
+ /**
136
+ * Gets the lock script for the signer
137
+ * @returns Promise resolving to the lock script
138
+ */
139
+ async getLock(): Promise<Script> {
140
+ const address = await this.getAddress();
141
+ return address.script;
142
+ }
143
+
144
+ /**
145
+ * Gets the CKBFS script configuration for the current settings
146
+ * @returns The CKBFS script configuration
147
+ */
148
+ getCKBFSConfig(): CKBFSScriptConfig {
149
+ return getCKBFSScriptConfig(this.network, this.version, this.useTypeID);
150
+ }
151
+
152
+ /**
153
+ * Publishes a file to CKBFS
154
+ * @param filePath The path to the file to publish
155
+ * @param options Options for publishing the file
156
+ * @returns Promise resolving to the transaction hash
157
+ */
158
+ async publishFile(filePath: string, options: FileOptions = {}): Promise<string> {
159
+ // Read the file and split into chunks
160
+ const fileContent = readFileAsUint8Array(filePath);
161
+ const contentChunks = [];
162
+
163
+ for (let i = 0; i < fileContent.length; i += this.chunkSize) {
164
+ contentChunks.push(fileContent.slice(i, i + this.chunkSize));
165
+ }
166
+
167
+ // Get the lock script
168
+ const lock = await this.getLock();
169
+
170
+ // Determine content type if not provided
171
+ const contentType = options.contentType || getContentType(filePath);
172
+
173
+ // Use the filename from the path if not provided
174
+ const pathParts = filePath.split(/[\\\/]/);
175
+ const filename = options.filename || pathParts[pathParts.length - 1];
176
+
177
+ // Create and sign the transaction
178
+ const tx = await publishCKBFS(this.signer, {
179
+ contentChunks,
180
+ contentType,
181
+ filename,
182
+ lock,
183
+ capacity: options.capacity,
184
+ feeRate: options.feeRate,
185
+ network: options.network || this.network,
186
+ version: options.version || this.version,
187
+ useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
188
+ });
189
+
190
+ console.log('tx', tx.stringify());
191
+
192
+ // Send the transaction
193
+ const txHash = await this.signer.sendTransaction(tx);
194
+
195
+ return txHash;
196
+ }
197
+
198
+ /**
199
+ * Appends content to an existing CKBFS file
200
+ * @param filePath The path to the file containing the content to append
201
+ * @param ckbfsCell The CKBFS cell to append to
202
+ * @param options Additional options for the append operation
203
+ * @returns Promise resolving to the transaction hash
204
+ */
205
+ async appendFile(
206
+ filePath: string,
207
+ ckbfsCell: AppendOptions['ckbfsCell'],
208
+ options: Omit<FileOptions, 'contentType' | 'filename'> = {}
209
+ ): Promise<string> {
210
+ // Read the file and split into chunks
211
+ const fileContent = readFileAsUint8Array(filePath);
212
+ const contentChunks = [];
213
+
214
+ for (let i = 0; i < fileContent.length; i += this.chunkSize) {
215
+ contentChunks.push(fileContent.slice(i, i + this.chunkSize));
216
+ }
217
+
218
+ // Create and sign the transaction
219
+ const tx = await appendCKBFS(this.signer, {
220
+ ckbfsCell,
221
+ contentChunks,
222
+ feeRate: options.feeRate,
223
+ network: options.network || this.network,
224
+ version: options.version || this.version
225
+ });
226
+
227
+ // Send the transaction
228
+ const txHash = await this.signer.sendTransaction(tx);
229
+
230
+ return txHash;
231
+ }
232
+
233
+ /**
234
+ * Creates a new transaction for publishing a file but doesn't sign or send it
235
+ * @param filePath The path to the file to publish
236
+ * @param options Options for publishing the file
237
+ * @returns Promise resolving to the unsigned transaction
238
+ */
239
+ async createPublishTransaction(filePath: string, options: FileOptions = {}): Promise<Transaction> {
240
+ // Read the file and split into chunks
241
+ const fileContent = readFileAsUint8Array(filePath);
242
+ const contentChunks = [];
243
+
244
+ for (let i = 0; i < fileContent.length; i += this.chunkSize) {
245
+ contentChunks.push(fileContent.slice(i, i + this.chunkSize));
246
+ }
247
+
248
+ // Get the lock script
249
+ const lock = await this.getLock();
250
+
251
+ // Determine content type if not provided
252
+ const contentType = options.contentType || getContentType(filePath);
253
+
254
+ // Use the filename from the path if not provided
255
+ const pathParts = filePath.split(/[\\\/]/);
256
+ const filename = options.filename || pathParts[pathParts.length - 1];
257
+
258
+ // Create the transaction
259
+ return createPublishTransaction(this.signer, {
260
+ contentChunks,
261
+ contentType,
262
+ filename,
263
+ lock,
264
+ capacity: options.capacity,
265
+ feeRate: options.feeRate,
266
+ network: options.network || this.network,
267
+ version: options.version || this.version,
268
+ useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Creates a new transaction for appending content but doesn't sign or send it
274
+ * @param filePath The path to the file containing the content to append
275
+ * @param ckbfsCell The CKBFS cell to append to
276
+ * @param options Additional options for the append operation
277
+ * @returns Promise resolving to the unsigned transaction
278
+ */
279
+ async createAppendTransaction(
280
+ filePath: string,
281
+ ckbfsCell: AppendOptions['ckbfsCell'],
282
+ options: Omit<FileOptions, 'contentType' | 'filename'> = {}
283
+ ): Promise<Transaction> {
284
+ // Read the file and split into chunks
285
+ const fileContent = readFileAsUint8Array(filePath);
286
+ const contentChunks = [];
287
+
288
+ for (let i = 0; i < fileContent.length; i += this.chunkSize) {
289
+ contentChunks.push(fileContent.slice(i, i + this.chunkSize));
290
+ }
291
+
292
+ // Create the transaction
293
+ return createAppendTransaction(this.signer, {
294
+ ckbfsCell,
295
+ contentChunks,
296
+ feeRate: options.feeRate,
297
+ network: options.network || this.network,
298
+ version: options.version || this.version
299
+ });
300
+ }
301
+ }
302
+
303
+ // Export utility functions
304
+ export {
305
+ // Checksum utilities
306
+ calculateChecksum,
307
+ verifyChecksum,
308
+ updateChecksum,
309
+ verifyWitnessChecksum,
310
+
311
+ // Transaction utilities
312
+ createCKBFSCell,
313
+ createPublishTransaction,
314
+ createAppendTransaction,
315
+ publishCKBFS,
316
+ appendCKBFS,
317
+
318
+ // File utilities
319
+ readFile,
320
+ readFileAsText,
321
+ readFileAsUint8Array,
322
+ writeFile,
323
+ getContentType,
324
+ splitFileIntoChunks,
325
+ combineChunksToFile,
326
+
327
+ // Witness utilities
328
+ createCKBFSWitness,
329
+ createTextCKBFSWitness,
330
+ extractCKBFSWitnessContent,
331
+ isCKBFSWitness,
332
+ createChunkedCKBFSWitnesses,
333
+
334
+ // Molecule definitions
335
+ CKBFSData,
336
+ BackLinkV1,
337
+ BackLinkV2,
338
+
339
+ // Types
340
+ CKBFSDataType,
341
+ BackLinkType,
342
+ CKBFSCellOptions,
343
+ PublishOptions,
344
+ AppendOptions,
345
+
346
+ // Constants
347
+ CKBFS_HEADER,
348
+ CKBFS_HEADER_STRING,
349
+
350
+ // CKBFS Protocol Constants & Configuration
351
+ NetworkType,
352
+ ProtocolVersion,
353
+ DEFAULT_NETWORK,
354
+ DEFAULT_VERSION,
355
+ CKBFS_CODE_HASH,
356
+ CKBFS_TYPE_ID,
357
+ ADLER32_CODE_HASH,
358
+ ADLER32_TYPE_ID,
359
+ DEP_GROUP_TX_HASH,
360
+ DEPLOY_TX_HASH,
361
+ getCKBFSScriptConfig,
362
+ CKBFSScriptConfig
363
+ };
@@ -0,0 +1,74 @@
1
+ import { adler32 } from 'hash-wasm';
2
+
3
+ /**
4
+ * Utility functions for Adler32 checksum generation and verification
5
+ */
6
+
7
+ /**
8
+ * Calculates Adler32 checksum for the provided data
9
+ * @param data The data to calculate checksum for
10
+ * @returns Promise resolving to the calculated checksum as a number
11
+ */
12
+ export async function calculateChecksum(data: Uint8Array): Promise<number> {
13
+ const checksumString = await adler32(data);
14
+ const checksumBuffer = Buffer.from(checksumString, 'hex');
15
+ return checksumBuffer.readUInt32BE();
16
+ }
17
+
18
+ /**
19
+ * Updates an existing checksum with new data
20
+ * @param previousChecksum The existing checksum to update
21
+ * @param newData The new data to add to the checksum
22
+ * @returns Promise resolving to the updated checksum as a number
23
+ */
24
+ export async function updateChecksum(previousChecksum: number, newData: Uint8Array): Promise<number> {
25
+ // In a real implementation, this would require the actual Adler32 state recovery
26
+ // For now, we're simply concatenating the previousChecksum as a hex string with the new data
27
+ // and calculating a new checksum
28
+
29
+ const checksumBytes = Buffer.alloc(4);
30
+ checksumBytes.writeUInt32BE(previousChecksum);
31
+
32
+ // Concatenate the previous checksum bytes with the new data
33
+ const combinedData = Buffer.concat([checksumBytes, Buffer.from(newData)]);
34
+
35
+ // Calculate the new checksum
36
+ return calculateChecksum(combinedData);
37
+ }
38
+
39
+ /**
40
+ * Verifies if a given checksum matches the expected checksum for the data
41
+ * @param data The data to verify
42
+ * @param expectedChecksum The expected checksum
43
+ * @returns Promise resolving to a boolean indicating whether the checksum is valid
44
+ */
45
+ export async function verifyChecksum(data: Uint8Array, expectedChecksum: number): Promise<boolean> {
46
+ const calculatedChecksum = await calculateChecksum(data);
47
+ return calculatedChecksum === expectedChecksum;
48
+ }
49
+
50
+ /**
51
+ * Verifies the checksum of a CKBFS witness
52
+ * @param witness The witness bytes
53
+ * @param expectedChecksum The expected checksum
54
+ * @param backlinks Optional backlinks to use for checksum verification
55
+ * @returns Promise resolving to a boolean indicating whether the checksum is valid
56
+ */
57
+ export async function verifyWitnessChecksum(
58
+ witness: Uint8Array,
59
+ expectedChecksum: number,
60
+ backlinks: { checksum: number }[] = []
61
+ ): Promise<boolean> {
62
+ // Extract the content bytes from the witness (skip the CKBFS header and version)
63
+ const contentBytes = witness.slice(6);
64
+
65
+ // If backlinks are provided, use the last backlink's checksum
66
+ if (backlinks.length > 0) {
67
+ const lastBacklink = backlinks[backlinks.length - 1];
68
+ const updatedChecksum = await updateChecksum(lastBacklink.checksum, contentBytes);
69
+ return updatedChecksum === expectedChecksum;
70
+ }
71
+
72
+ // Otherwise, calculate checksum from scratch
73
+ return verifyChecksum(contentBytes, expectedChecksum);
74
+ }