@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,420 @@
1
+ import { ccc, Transaction, Script, Signer } from "@ckb-ccc/core";
2
+ import { calculateChecksum, updateChecksum } from './checksum';
3
+ import { CKBFSData, BackLinkType, CKBFSDataType } from './molecule';
4
+ import { createChunkedCKBFSWitnesses } from './witness';
5
+ import {
6
+ getCKBFSScriptConfig,
7
+ NetworkType,
8
+ ProtocolVersion,
9
+ DEFAULT_NETWORK,
10
+ DEFAULT_VERSION
11
+ } from './constants';
12
+
13
+ /**
14
+ * Utility functions for CKB transaction creation and handling
15
+ */
16
+
17
+ /**
18
+ * Options for creating a CKBFS cell
19
+ */
20
+ export interface CKBFSCellOptions {
21
+ contentType: string;
22
+ filename: string;
23
+ capacity?: bigint;
24
+ lock: Script;
25
+ network?: NetworkType;
26
+ version?: string;
27
+ useTypeID?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Options for publishing a file to CKBFS
32
+ */
33
+ export interface PublishOptions extends CKBFSCellOptions {
34
+ contentChunks: Uint8Array[];
35
+ feeRate?: number;
36
+ }
37
+
38
+ /**
39
+ * Options for appending content to a CKBFS file
40
+ */
41
+ export interface AppendOptions {
42
+ ckbfsCell: {
43
+ outPoint: { txHash: string; index: number };
44
+ data: CKBFSDataType;
45
+ type: Script;
46
+ lock: Script;
47
+ capacity: bigint;
48
+ };
49
+ contentChunks: Uint8Array[];
50
+ feeRate?: number;
51
+ network?: NetworkType;
52
+ version?: string;
53
+ }
54
+
55
+ /**
56
+ * Ensures a string is prefixed with '0x'
57
+ * @param value The string to ensure is hex prefixed
58
+ * @returns A hex prefixed string
59
+ */
60
+ function ensureHexPrefix(value: string): `0x${string}` {
61
+ if (value.startsWith('0x')) {
62
+ return value as `0x${string}`;
63
+ }
64
+ return `0x${value}` as `0x${string}`;
65
+ }
66
+
67
+ /**
68
+ * Creates a CKBFS cell
69
+ * @param options Options for creating the CKBFS cell
70
+ * @returns The created cell output
71
+ */
72
+ export function createCKBFSCell(options: CKBFSCellOptions) {
73
+ const {
74
+ contentType,
75
+ filename,
76
+ capacity,
77
+ lock,
78
+ network = DEFAULT_NETWORK,
79
+ version = DEFAULT_VERSION,
80
+ useTypeID = false
81
+ } = options;
82
+
83
+ // Get CKBFS script config
84
+ const config = getCKBFSScriptConfig(network, version, useTypeID);
85
+
86
+ // Create pre CKBFS type script
87
+ const preCkbfsTypeScript = new Script(
88
+ ensureHexPrefix(config.codeHash),
89
+ config.hashType as any,
90
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
91
+ );
92
+
93
+ // Return the cell output
94
+ return {
95
+ lock,
96
+ type: preCkbfsTypeScript,
97
+ capacity: capacity || 200n * 100000000n, // Default 200 CKB
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Creates a transaction for publishing a file to CKBFS
103
+ * @param signer The signer to use for the transaction
104
+ * @param options Options for publishing the file
105
+ * @returns Promise resolving to the created transaction
106
+ */
107
+ export async function createPublishTransaction(
108
+ signer: Signer,
109
+ options: PublishOptions
110
+ ): Promise<Transaction> {
111
+ const {
112
+ contentChunks,
113
+ contentType,
114
+ filename,
115
+ lock,
116
+ feeRate,
117
+ network = DEFAULT_NETWORK,
118
+ version = DEFAULT_VERSION,
119
+ useTypeID = false
120
+ } = options;
121
+
122
+ // Calculate checksum for the combined content
123
+ const textEncoder = new TextEncoder();
124
+ const combinedContent = Buffer.concat(contentChunks);
125
+ const checksum = await calculateChecksum(combinedContent);
126
+
127
+ // Create CKBFS witnesses
128
+ const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
129
+
130
+ // Calculate the actual witness indices where our content is placed
131
+ // Index 0 is reserved for the secp256k1 witness for signing
132
+ // So our CKBFS data starts at index 1
133
+ const contentStartIndex = 1;
134
+ const witnessIndices = Array.from(
135
+ { length: contentChunks.length },
136
+ (_, i) => contentStartIndex + i
137
+ );
138
+
139
+ // Create CKBFS cell output data based on version
140
+ let outputData: Uint8Array;
141
+
142
+ if (version === ProtocolVersion.V1) {
143
+ // V1 format: Single index field
144
+ // For V1, use the first index where content is placed
145
+ outputData = CKBFSData.pack({
146
+ index: [contentStartIndex],
147
+ checksum,
148
+ contentType: textEncoder.encode(contentType),
149
+ filename: textEncoder.encode(filename),
150
+ backLinks: [],
151
+ }, version);
152
+ } else {
153
+ // V2 format: Multiple indexes
154
+ // For V2, use all the indices where content is placed
155
+ outputData = CKBFSData.pack({
156
+ indexes: witnessIndices,
157
+ checksum,
158
+ contentType: textEncoder.encode(contentType),
159
+ filename: textEncoder.encode(filename),
160
+ backLinks: [],
161
+ }, version);
162
+ }
163
+
164
+ // Get CKBFS script config
165
+ const config = getCKBFSScriptConfig(network, version, useTypeID);
166
+
167
+ // Create pre transaction without cell deps initially
168
+ const preTx = Transaction.from({
169
+ outputs: [
170
+ createCKBFSCell({
171
+ contentType,
172
+ filename,
173
+ lock,
174
+ network,
175
+ version,
176
+ useTypeID
177
+ })
178
+ ],
179
+ witnesses: [
180
+ [], // Empty secp witness for signing
181
+ ...ckbfsWitnesses.map(w => `0x${Buffer.from(w).toString('hex')}`),
182
+ ],
183
+ outputsData: [
184
+ outputData,
185
+ ]
186
+ });
187
+
188
+ // Add the CKBFS dep group cell dependency
189
+ preTx.addCellDeps({
190
+ outPoint: {
191
+ txHash: ensureHexPrefix(config.depTxHash),
192
+ index: config.depIndex || 0,
193
+ },
194
+ depType: "depGroup"
195
+ });
196
+
197
+ // Get the recommended address to ensure lock script cell deps are included
198
+ const address = await signer.getRecommendedAddressObj();
199
+
200
+ // Complete inputs by capacity
201
+ await preTx.completeInputsByCapacity(signer);
202
+
203
+ // Complete fee change to lock
204
+ await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
205
+
206
+ // Create type ID args
207
+ const args = ccc.hashTypeId(preTx.inputs[0], 0x0);
208
+
209
+ // Create CKBFS type script with type ID
210
+ const ckbfsTypeScript = new Script(
211
+ ensureHexPrefix(config.codeHash),
212
+ config.hashType as any,
213
+ args
214
+ );
215
+
216
+ // Create final transaction with same cell deps as preTx
217
+ const tx = Transaction.from({
218
+ cellDeps: preTx.cellDeps,
219
+ witnesses: [
220
+ [], // Reset first witness for signing
221
+ ...preTx.witnesses.slice(1)
222
+ ],
223
+ outputsData: preTx.outputsData,
224
+ inputs: preTx.inputs,
225
+ outputs: [
226
+ {
227
+ lock,
228
+ type: ckbfsTypeScript,
229
+ capacity: preTx.outputs[0].capacity,
230
+ },
231
+ ...preTx.outputs.slice(1) // Include rest of outputs (e.g., change)
232
+ ]
233
+ });
234
+
235
+ return tx;
236
+ }
237
+
238
+ /**
239
+ * Creates a transaction for appending content to a CKBFS file
240
+ * @param signer The signer to use for the transaction
241
+ * @param options Options for appending content
242
+ * @returns Promise resolving to the created transaction
243
+ */
244
+ export async function createAppendTransaction(
245
+ signer: Signer,
246
+ options: AppendOptions
247
+ ): Promise<Transaction> {
248
+ const {
249
+ ckbfsCell,
250
+ contentChunks,
251
+ feeRate,
252
+ network = DEFAULT_NETWORK,
253
+ version = DEFAULT_VERSION
254
+ } = options;
255
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
256
+
257
+ // Get CKBFS script config early to use version info
258
+ const config = getCKBFSScriptConfig(network, version);
259
+
260
+ // Create CKBFS witnesses - this may vary between V1 and V2
261
+ const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
262
+
263
+ // Combine the new content chunks
264
+ const combinedContent = Buffer.concat(contentChunks);
265
+
266
+ // Instead of calculating a new checksum from scratch, update the existing checksum
267
+ // with the new content - this is more efficient and matches the Adler32 algorithm's
268
+ // cumulative nature
269
+ const contentChecksum = await updateChecksum(data.checksum, combinedContent);
270
+ console.log(`Updated checksum from ${data.checksum} to ${contentChecksum} for appended content`);
271
+
272
+ // Create backlink for the current state based on version
273
+ let newBackLink: any;
274
+
275
+ if (version === ProtocolVersion.V1) {
276
+ // V1 format: Use index field (single number)
277
+ newBackLink = {
278
+ txHash: outPoint.txHash,
279
+ index: data.index && data.index.length > 0 ? data.index[0] : 0,
280
+ checksum: data.checksum,
281
+ };
282
+ } else {
283
+ // V2 format: Use indexes field (array of numbers)
284
+ newBackLink = {
285
+ txHash: outPoint.txHash,
286
+ indexes: data.indexes || data.index || [],
287
+ checksum: data.checksum,
288
+ };
289
+ }
290
+
291
+ // Update backlinks
292
+ const backLinks = [newBackLink];
293
+
294
+ // Define indices based on version
295
+ let outputData: Uint8Array;
296
+
297
+ // Calculate the actual witness indices where our content is placed
298
+ // Index 0 is reserved for the secp256k1 witness for signing
299
+ // So our CKBFS data starts at index 1
300
+ const contentStartIndex = 1;
301
+ const witnessIndices = Array.from(
302
+ { length: contentChunks.length },
303
+ (_, i) => contentStartIndex + i
304
+ );
305
+
306
+ if (version === ProtocolVersion.V1) {
307
+ // In V1, use the first index where content is placed
308
+ // (even if we have multiple witnesses, V1 only supports a single index)
309
+ outputData = CKBFSData.pack({
310
+ index: [contentStartIndex],
311
+ checksum: contentChecksum,
312
+ contentType: data.contentType,
313
+ filename: data.filename,
314
+ backLinks,
315
+ }, version);
316
+ } else {
317
+ // In V2, use all the indices where content is placed
318
+ outputData = CKBFSData.pack({
319
+ indexes: witnessIndices,
320
+ checksum: contentChecksum,
321
+ contentType: data.contentType,
322
+ filename: data.filename,
323
+ backLinks,
324
+ }, version);
325
+ }
326
+
327
+ // Pack the original data to get its size - use the appropriate version
328
+ const originalData = CKBFSData.pack(data, version);
329
+ const originalDataSize = originalData.length;
330
+
331
+ // Get sizes
332
+ const newDataSize = outputData.length;
333
+ const dataSizeDiff = newDataSize - originalDataSize;
334
+
335
+ // Calculate the additional capacity needed (in shannons)
336
+ // CKB requires 1 shannon per byte of data
337
+ const additionalCapacity = BigInt(Math.max(0, dataSizeDiff)) * 100000000n;
338
+
339
+ // Add the additional capacity to the original cell capacity
340
+ console.log(`Original capacity: ${capacity}, Additional needed: ${additionalCapacity}, Data size diff: ${dataSizeDiff}, Version: ${version}`);
341
+ const outputCapacity = capacity + additionalCapacity;
342
+
343
+ // Create initial transaction with the CKBFS cell input
344
+ const tx = Transaction.from({
345
+ inputs: [
346
+ {
347
+ previousOutput: {
348
+ txHash: outPoint.txHash,
349
+ index: outPoint.index,
350
+ },
351
+ since: "0x0",
352
+ }
353
+ ],
354
+ outputs: [
355
+ {
356
+ lock,
357
+ type,
358
+ capacity: outputCapacity,
359
+ }
360
+ ],
361
+ witnesses: [
362
+ [], // Empty secp witness for signing
363
+ ...ckbfsWitnesses.map(w => `0x${Buffer.from(w).toString('hex')}`),
364
+ ],
365
+ outputsData: [
366
+ outputData,
367
+ ]
368
+ });
369
+
370
+ // Add the CKBFS dep group cell dependency
371
+ tx.addCellDeps({
372
+ outPoint: {
373
+ txHash: ensureHexPrefix(config.depTxHash),
374
+ index: config.depIndex || 0,
375
+ },
376
+ depType: "depGroup"
377
+ });
378
+
379
+ // Get the recommended address to ensure lock script cell deps are included
380
+ const address = await signer.getRecommendedAddressObj();
381
+
382
+ // If we need more capacity than the original cell had, add additional inputs
383
+ if (additionalCapacity > 0n) {
384
+ // Add more inputs to cover the increased capacity
385
+ await tx.completeInputsByCapacity(signer);
386
+ }
387
+
388
+ // Complete fee
389
+ await tx.completeFeeChangeToLock(signer, lock || address.script, feeRate || 2000);
390
+
391
+ return tx;
392
+ }
393
+
394
+ /**
395
+ * Creates a complete transaction for publishing a file to CKBFS
396
+ * @param signer The signer to use for the transaction
397
+ * @param options Options for publishing the file
398
+ * @returns Promise resolving to the signed transaction
399
+ */
400
+ export async function publishCKBFS(
401
+ signer: Signer,
402
+ options: PublishOptions
403
+ ): Promise<Transaction> {
404
+ const tx = await createPublishTransaction(signer, options);
405
+ return signer.signTransaction(tx);
406
+ }
407
+
408
+ /**
409
+ * Creates a complete transaction for appending content to a CKBFS file
410
+ * @param signer The signer to use for the transaction
411
+ * @param options Options for appending content
412
+ * @returns Promise resolving to the signed transaction
413
+ */
414
+ export async function appendCKBFS(
415
+ signer: Signer,
416
+ options: AppendOptions
417
+ ): Promise<Transaction> {
418
+ const tx = await createAppendTransaction(signer, options);
419
+ return signer.signTransaction(tx);
420
+ }
@@ -0,0 +1,76 @@
1
+ import { CKBFS_HEADER } from './molecule';
2
+
3
+ /**
4
+ * Utility functions for creating and handling CKBFS witnesses
5
+ */
6
+
7
+ /**
8
+ * Creates a CKBFS witness with content
9
+ * @param content The content to include in the witness
10
+ * @param version Optional version byte (default is 0)
11
+ * @returns Uint8Array containing the witness data
12
+ */
13
+ export function createCKBFSWitness(content: Uint8Array, version: number = 0): Uint8Array {
14
+ // Create witness with CKBFS header, version byte, and content
15
+ const versionByte = new Uint8Array([version]);
16
+ return Buffer.concat([CKBFS_HEADER, versionByte, content]);
17
+ }
18
+
19
+ /**
20
+ * Creates a CKBFS witness with text content
21
+ * @param text The text content to include in the witness
22
+ * @param version Optional version byte (default is 0)
23
+ * @returns Uint8Array containing the witness data
24
+ */
25
+ export function createTextCKBFSWitness(text: string, version: number = 0): Uint8Array {
26
+ const textEncoder = new TextEncoder();
27
+ const contentBytes = textEncoder.encode(text);
28
+ return createCKBFSWitness(contentBytes, version);
29
+ }
30
+
31
+ /**
32
+ * Extracts content from a CKBFS witness
33
+ * @param witness The CKBFS witness data
34
+ * @returns Object containing the extracted version and content bytes
35
+ */
36
+ export function extractCKBFSWitnessContent(witness: Uint8Array): { version: number; content: Uint8Array } {
37
+ // Ensure the witness has the CKBFS header
38
+ const header = witness.slice(0, 5);
39
+ const headerString = new TextDecoder().decode(header);
40
+
41
+ if (headerString !== 'CKBFS') {
42
+ throw new Error('Invalid CKBFS witness: missing CKBFS header');
43
+ }
44
+
45
+ // Extract version byte and content
46
+ const version = witness[5];
47
+ const content = witness.slice(6);
48
+
49
+ return { version, content };
50
+ }
51
+
52
+ /**
53
+ * Checks if a witness is a valid CKBFS witness
54
+ * @param witness The witness data to check
55
+ * @returns Boolean indicating whether the witness is a valid CKBFS witness
56
+ */
57
+ export function isCKBFSWitness(witness: Uint8Array): boolean {
58
+ if (witness.length < 6) {
59
+ return false;
60
+ }
61
+
62
+ const header = witness.slice(0, 5);
63
+ const headerString = new TextDecoder().decode(header);
64
+
65
+ return headerString === 'CKBFS';
66
+ }
67
+
68
+ /**
69
+ * Creates an array of witnesses for a CKBFS transaction from content chunks
70
+ * @param contentChunks Array of content chunks
71
+ * @param version Optional version byte (default is 0)
72
+ * @returns Array of Uint8Array witnesses
73
+ */
74
+ export function createChunkedCKBFSWitnesses(contentChunks: Uint8Array[], version: number = 0): Uint8Array[] {
75
+ return contentChunks.map(chunk => createCKBFSWitness(chunk, version));
76
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "**/*.test.ts"]
15
+ }