@ckbfs/api 1.5.1 → 2.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.
Files changed (45) hide show
  1. package/README.md +31 -6
  2. package/RFC.v3.md +210 -0
  3. package/dist/index.d.ts +72 -7
  4. package/dist/index.js +437 -75
  5. package/dist/utils/checksum.d.ts +16 -0
  6. package/dist/utils/checksum.js +74 -8
  7. package/dist/utils/constants.d.ts +2 -1
  8. package/dist/utils/constants.js +12 -2
  9. package/dist/utils/file.d.ts +44 -0
  10. package/dist/utils/file.js +303 -30
  11. package/dist/utils/molecule.d.ts +13 -1
  12. package/dist/utils/molecule.js +32 -5
  13. package/dist/utils/transaction-backup.d.ts +117 -0
  14. package/dist/utils/transaction-backup.js +624 -0
  15. package/dist/utils/transaction.d.ts +7 -115
  16. package/dist/utils/transaction.js +45 -622
  17. package/dist/utils/transactions/index.d.ts +8 -0
  18. package/dist/utils/transactions/index.js +31 -0
  19. package/dist/utils/transactions/shared.d.ts +57 -0
  20. package/dist/utils/transactions/shared.js +17 -0
  21. package/dist/utils/transactions/v1v2.d.ts +80 -0
  22. package/dist/utils/transactions/v1v2.js +592 -0
  23. package/dist/utils/transactions/v3.d.ts +124 -0
  24. package/dist/utils/transactions/v3.js +369 -0
  25. package/dist/utils/witness.d.ts +45 -0
  26. package/dist/utils/witness.js +145 -3
  27. package/examples/append-v3.ts +310 -0
  28. package/examples/chunked-publish.ts +307 -0
  29. package/examples/publish-v3.ts +152 -0
  30. package/examples/publish.ts +4 -4
  31. package/examples/retrieve-v3.ts +222 -0
  32. package/package.json +6 -2
  33. package/small-example.txt +1 -0
  34. package/src/index.ts +568 -87
  35. package/src/utils/checksum.ts +90 -9
  36. package/src/utils/constants.ts +19 -2
  37. package/src/utils/file.ts +386 -35
  38. package/src/utils/molecule.ts +43 -6
  39. package/src/utils/transaction-backup.ts +849 -0
  40. package/src/utils/transaction.ts +39 -848
  41. package/src/utils/transactions/index.ts +16 -0
  42. package/src/utils/transactions/shared.ts +64 -0
  43. package/src/utils/transactions/v1v2.ts +791 -0
  44. package/src/utils/transactions/v3.ts +564 -0
  45. package/src/utils/witness.ts +193 -0
@@ -0,0 +1,791 @@
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
+ ProtocolVersionType,
10
+ DEFAULT_NETWORK,
11
+ DEFAULT_VERSION,
12
+ } from "../constants";
13
+ import {
14
+ ensureHexPrefix,
15
+ BasePublishOptions,
16
+ BaseAppendOptions,
17
+ CKBFSCellOptions
18
+ } from "./shared";
19
+
20
+ /**
21
+ * V1 and V2 CKBFS transaction utilities
22
+ */
23
+
24
+ /**
25
+ * Options for publishing a file to CKBFS (V1/V2)
26
+ */
27
+ export interface PublishOptions extends BasePublishOptions {}
28
+
29
+ /**
30
+ * Options for appending content to a CKBFS file (V1/V2)
31
+ */
32
+ export interface AppendOptions extends BaseAppendOptions {}
33
+
34
+ /**
35
+ * Creates a CKBFS cell
36
+ * @param options Options for creating the CKBFS cell
37
+ * @returns The created cell output
38
+ */
39
+ export function createCKBFSCell(options: CKBFSCellOptions) {
40
+ const {
41
+ contentType,
42
+ filename,
43
+ capacity,
44
+ lock,
45
+ network = DEFAULT_NETWORK,
46
+ version = DEFAULT_VERSION,
47
+ useTypeID = false,
48
+ } = options;
49
+
50
+ // Get CKBFS script config
51
+ const config = getCKBFSScriptConfig(network, version, useTypeID);
52
+
53
+ // Create pre CKBFS type script
54
+ const preCkbfsTypeScript = new Script(
55
+ ensureHexPrefix(config.codeHash),
56
+ config.hashType as any,
57
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
58
+ );
59
+
60
+ // Return the cell output
61
+ return {
62
+ lock,
63
+ type: preCkbfsTypeScript,
64
+ capacity: capacity || 200n * 100000000n, // Default 200 CKB
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Prepares a transaction for publishing a file to CKBFS without fee and change handling
70
+ * You will need to manually set the typeID if you did not provide inputs, or just check is return value emptyTypeID is true
71
+ * @param options Options for publishing the file
72
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
73
+ */
74
+ export async function preparePublishTransaction(
75
+ options: PublishOptions,
76
+ ): Promise<{tx: Transaction, outputIndex: number, emptyTypeID: boolean}> { // if emptyTypeID is true, you shall manually set the typeID after
77
+ const {
78
+ from,
79
+ contentChunks,
80
+ contentType,
81
+ filename,
82
+ lock,
83
+ capacity,
84
+ network = DEFAULT_NETWORK,
85
+ version = DEFAULT_VERSION,
86
+ useTypeID = false,
87
+ } = options;
88
+
89
+ // Calculate checksum for the combined content
90
+ const combinedContent = Buffer.concat(contentChunks);
91
+ const checksum = await calculateChecksum(combinedContent);
92
+
93
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
94
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
95
+ // not to be confused with the Protocol Version (V1 vs V2)
96
+ const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
97
+
98
+ // Calculate the actual witness indices where our content is placed
99
+
100
+ const contentStartIndex = from?.witnesses.length || 1;
101
+ const witnessIndices = Array.from(
102
+ { length: contentChunks.length },
103
+ (_, i) => contentStartIndex + i,
104
+ );
105
+
106
+ // Create CKBFS cell output data based on version
107
+ let outputData: Uint8Array;
108
+
109
+ if (version === ProtocolVersion.V1) {
110
+ // V1 format: Single index field (a single number, not an array)
111
+ // For V1, use the first index where content is placed
112
+ outputData = CKBFSData.pack(
113
+ {
114
+ index: contentStartIndex,
115
+ checksum,
116
+ contentType: contentType,
117
+ filename: filename,
118
+ backLinks: [],
119
+ },
120
+ version,
121
+ );
122
+ } else {
123
+ // V2 format: Multiple indexes (array of numbers)
124
+ // For V2, use all the indices where content is placed
125
+ outputData = CKBFSData.pack(
126
+ {
127
+ indexes: witnessIndices,
128
+ checksum,
129
+ contentType,
130
+ filename,
131
+ backLinks: [],
132
+ },
133
+ version,
134
+ );
135
+ }
136
+
137
+ // Get CKBFS script config
138
+ const config = getCKBFSScriptConfig(network, version, useTypeID);
139
+
140
+ const preCkbfsTypeScript = new Script(
141
+ ensureHexPrefix(config.codeHash),
142
+ config.hashType as any,
143
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
144
+ );
145
+ const ckbfsCellSize =
146
+ BigInt(
147
+ outputData.length +
148
+ preCkbfsTypeScript.occupiedSize +
149
+ lock.occupiedSize +
150
+ 8,
151
+ ) * 100000000n;
152
+ // Create pre transaction without cell deps initially
153
+ let preTx: Transaction;
154
+ if(from) {
155
+ // If from is not empty, inject/merge the fields
156
+ preTx = Transaction.from({
157
+ ...from,
158
+ outputs: from.outputs.length === 0
159
+ ? [
160
+ createCKBFSCell({
161
+ contentType,
162
+ filename,
163
+ lock,
164
+ network,
165
+ version,
166
+ useTypeID,
167
+ capacity: ckbfsCellSize || capacity,
168
+ }),
169
+ ]
170
+ : [
171
+ ...from.outputs,
172
+ createCKBFSCell({
173
+ contentType,
174
+ filename,
175
+ lock,
176
+ network,
177
+ version,
178
+ useTypeID,
179
+ capacity: ckbfsCellSize || capacity,
180
+ }),
181
+ ],
182
+ witnesses: from.witnesses.length === 0
183
+ ? [
184
+ [], // Empty secp witness for signing if not provided
185
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
186
+ ]
187
+ : [
188
+ ...from.witnesses,
189
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
190
+ ],
191
+ outputsData: from.outputsData.length === 0
192
+ ? [outputData]
193
+ : [
194
+ ...from.outputsData,
195
+ outputData,
196
+ ],
197
+ });
198
+ } else {
199
+ preTx = Transaction.from({
200
+ outputs: [
201
+ createCKBFSCell({
202
+ contentType,
203
+ filename,
204
+ lock,
205
+ network,
206
+ version,
207
+ useTypeID,
208
+ capacity: ckbfsCellSize || capacity,
209
+ }),
210
+ ],
211
+ witnesses: [
212
+ [], // Empty secp witness for signing
213
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
214
+ ],
215
+ outputsData: [outputData],
216
+ });
217
+ }
218
+
219
+ // Add the CKBFS dep group cell dependency
220
+ preTx.addCellDeps({
221
+ outPoint: {
222
+ txHash: ensureHexPrefix(config.depTxHash),
223
+ index: config.depIndex || 0,
224
+ },
225
+ depType: "depGroup",
226
+ });
227
+
228
+ // Create type ID args
229
+ const outputIndex = from ? from.outputs.length : 0;
230
+ const args = preTx.inputs.length > 0 ? ccc.hashTypeId(preTx.inputs[0], outputIndex) : "0x0000000000000000000000000000000000000000000000000000000000000000";
231
+
232
+ // Create CKBFS type script with type ID
233
+ const ckbfsTypeScript = new Script(
234
+ ensureHexPrefix(config.codeHash),
235
+ config.hashType as any,
236
+ args,
237
+ );
238
+
239
+ // Create final transaction with same cell deps as preTx
240
+ const tx = Transaction.from({
241
+ cellDeps: preTx.cellDeps,
242
+ witnesses: preTx.witnesses,
243
+ outputsData: preTx.outputsData,
244
+ inputs: preTx.inputs,
245
+ outputs: outputIndex === 0
246
+ ? [
247
+ {
248
+ lock,
249
+ type: ckbfsTypeScript,
250
+ capacity: preTx.outputs[outputIndex].capacity,
251
+ },
252
+ ]
253
+ : [
254
+ ...preTx.outputs.slice(0, outputIndex), // Include rest of outputs (e.g., change)
255
+ {
256
+ lock,
257
+ type: ckbfsTypeScript,
258
+ capacity: preTx.outputs[outputIndex].capacity,
259
+ },
260
+ ],
261
+ });
262
+
263
+ return {tx, outputIndex, emptyTypeID: args === "0x0000000000000000000000000000000000000000000000000000000000000000"};
264
+ }
265
+
266
+ /**
267
+ * Creates a transaction for publishing a file to CKBFS
268
+ * @param signer The signer to use for the transaction
269
+ * @param options Options for publishing the file
270
+ * @returns Promise resolving to the created transaction
271
+ */
272
+ export async function createPublishTransaction(
273
+ signer: Signer,
274
+ options: PublishOptions,
275
+ ): Promise<Transaction> {
276
+ const {
277
+ feeRate,
278
+ lock,
279
+ } = options;
280
+
281
+ // Use preparePublishTransaction to create the base transaction
282
+ const { tx: preTx, outputIndex, emptyTypeID } = await preparePublishTransaction(options);
283
+
284
+ // Complete inputs by capacity
285
+ await preTx.completeInputsByCapacity(signer);
286
+
287
+ // Complete fee change to lock
288
+ await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
289
+
290
+ // If emptyTypeID is true, we need to create the proper type ID args
291
+ if (emptyTypeID) {
292
+ // Get CKBFS script config
293
+ const config = getCKBFSScriptConfig(options.network || DEFAULT_NETWORK, options.version || DEFAULT_VERSION, options.useTypeID || false);
294
+
295
+ // Create type ID args
296
+ const args = ccc.hashTypeId(preTx.inputs[0], outputIndex);
297
+
298
+ // Create CKBFS type script with type ID
299
+ const ckbfsTypeScript = new Script(
300
+ ensureHexPrefix(config.codeHash),
301
+ config.hashType as any,
302
+ args,
303
+ );
304
+
305
+ // Create final transaction with updated type script
306
+ const tx = Transaction.from({
307
+ cellDeps: preTx.cellDeps,
308
+ witnesses: preTx.witnesses,
309
+ outputsData: preTx.outputsData,
310
+ inputs: preTx.inputs,
311
+ outputs: preTx.outputs.map((output, index) =>
312
+ index === outputIndex
313
+ ? {
314
+ ...output,
315
+ type: ckbfsTypeScript,
316
+ }
317
+ : output
318
+ ),
319
+ });
320
+
321
+ return tx;
322
+ }
323
+
324
+ return preTx;
325
+ }
326
+
327
+ /**
328
+ * Prepares a transaction for appending content to a CKBFS file without fee and change handling
329
+ * @param options Options for appending content
330
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
331
+ */
332
+ export async function prepareAppendTransaction(
333
+ options: AppendOptions,
334
+ ): Promise<{tx: Transaction, outputIndex: number}> {
335
+ const {
336
+ from,
337
+ ckbfsCell,
338
+ contentChunks,
339
+ network = DEFAULT_NETWORK,
340
+ version = DEFAULT_VERSION,
341
+ } = options;
342
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
343
+
344
+ // Get CKBFS script config early to use version info
345
+ const config = getCKBFSScriptConfig(network, version);
346
+
347
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
348
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
349
+ // not to be confused with the Protocol Version (V1 vs V2)
350
+ const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
351
+
352
+ // Combine the new content chunks for checksum calculation
353
+ const combinedContent = Buffer.concat(contentChunks);
354
+
355
+ // Update the existing checksum with the new content - this matches Adler32's
356
+ // cumulative nature as required by Rule 11 in the RFC
357
+ const contentChecksum = await updateChecksum(data.checksum, combinedContent);
358
+
359
+ // Calculate the actual witness indices where our content is placed
360
+ const contentStartIndex = from?.witnesses.length || 1;
361
+ const witnessIndices = Array.from(
362
+ { length: contentChunks.length },
363
+ (_, i) => contentStartIndex + i,
364
+ );
365
+
366
+ // Create backlink for the current state based on version
367
+ let newBackLink: any;
368
+
369
+ if (version === ProtocolVersion.V1) {
370
+ // V1 format: Use index field (single number)
371
+ newBackLink = {
372
+ // In V1, field order is index, checksum, txHash
373
+ // and index is a single number value, not an array
374
+ index:
375
+ data.index ||
376
+ (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
377
+ checksum: data.checksum,
378
+ txHash: outPoint.txHash,
379
+ };
380
+ } else {
381
+ // V2 format: Use indexes field (array of numbers)
382
+ newBackLink = {
383
+ // In V2, field order is indexes, checksum, txHash
384
+ // and indexes is an array of numbers
385
+ indexes: data.indexes || (data.index ? [data.index] : []),
386
+ checksum: data.checksum,
387
+ txHash: outPoint.txHash,
388
+ };
389
+ }
390
+
391
+ // Update backlinks - add the new one to the existing backlinks array
392
+ const backLinks = [...(data.backLinks || []), newBackLink];
393
+
394
+ // Define output data based on version
395
+ let outputData: Uint8Array;
396
+
397
+ if (version === ProtocolVersion.V1) {
398
+ // In V1, index is a single number, not an array
399
+ // The first witness index is used (V1 can only reference one witness)
400
+ outputData = CKBFSData.pack(
401
+ {
402
+ index: witnessIndices[0], // Use only the first index as a number
403
+ checksum: contentChecksum,
404
+ contentType: data.contentType,
405
+ filename: data.filename,
406
+ backLinks,
407
+ },
408
+ ProtocolVersion.V1,
409
+ ); // Explicitly use V1 for packing
410
+ } else {
411
+ // In V2, indexes is an array of witness indices
412
+ outputData = CKBFSData.pack(
413
+ {
414
+ indexes: witnessIndices,
415
+ checksum: contentChecksum,
416
+ contentType: data.contentType,
417
+ filename: data.filename,
418
+ backLinks,
419
+ },
420
+ ProtocolVersion.V2,
421
+ ); // Explicitly use V2 for packing
422
+ }
423
+
424
+ // Calculate the required capacity for the output cell
425
+ // This accounts for:
426
+ // 1. The output data size
427
+ // 2. The type script's occupied size
428
+ // 3. The lock script's occupied size
429
+ // 4. A constant of 8 bytes (for header overhead)
430
+ const ckbfsCellSize =
431
+ BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) *
432
+ 100000000n;
433
+
434
+ // Use the maximum value between calculated size and original capacity
435
+ // to ensure we have enough capacity but don't decrease capacity unnecessarily
436
+ const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
437
+
438
+ // Create initial transaction with the CKBFS cell input
439
+ let preTx: Transaction;
440
+ if (from) {
441
+ // If from is not empty, inject/merge the fields
442
+ preTx = Transaction.from({
443
+ ...from,
444
+ inputs: from.inputs.length === 0
445
+ ? [
446
+ {
447
+ previousOutput: {
448
+ txHash: outPoint.txHash,
449
+ index: outPoint.index,
450
+ },
451
+ since: "0x0",
452
+ },
453
+ ]
454
+ : [
455
+ ...from.inputs,
456
+ {
457
+ previousOutput: {
458
+ txHash: outPoint.txHash,
459
+ index: outPoint.index,
460
+ },
461
+ since: "0x0",
462
+ },
463
+ ],
464
+ outputs: from.outputs.length === 0
465
+ ? [
466
+ {
467
+ lock,
468
+ type,
469
+ capacity: outputCapacity,
470
+ },
471
+ ]
472
+ : [
473
+ ...from.outputs,
474
+ {
475
+ lock,
476
+ type,
477
+ capacity: outputCapacity,
478
+ },
479
+ ],
480
+ outputsData: from.outputsData.length === 0
481
+ ? [outputData]
482
+ : [
483
+ ...from.outputsData,
484
+ outputData,
485
+ ],
486
+ witnesses: from.witnesses.length === 0
487
+ ? [
488
+ [], // Empty secp witness for signing if not provided
489
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
490
+ ]
491
+ : [
492
+ ...from.witnesses,
493
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
494
+ ],
495
+ });
496
+ } else {
497
+ preTx = Transaction.from({
498
+ inputs: [
499
+ {
500
+ previousOutput: {
501
+ txHash: outPoint.txHash,
502
+ index: outPoint.index,
503
+ },
504
+ since: "0x0",
505
+ },
506
+ ],
507
+ outputs: [
508
+ {
509
+ lock,
510
+ type,
511
+ capacity: outputCapacity,
512
+ },
513
+ ],
514
+ outputsData: [outputData],
515
+ witnesses: [
516
+ [], // Empty secp witness for signing
517
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
518
+ ],
519
+ });
520
+ }
521
+
522
+ // Add the CKBFS dep group cell dependency
523
+ preTx.addCellDeps({
524
+ outPoint: {
525
+ txHash: ensureHexPrefix(config.depTxHash),
526
+ index: config.depIndex || 0,
527
+ },
528
+ depType: "depGroup",
529
+ });
530
+
531
+ const outputIndex = from ? from.outputs.length : 0;
532
+
533
+ return {tx: preTx, outputIndex};
534
+ }
535
+
536
+ /**
537
+ * Creates a transaction for appending content to a CKBFS file
538
+ * @param signer The signer to use for the transaction
539
+ * @param options Options for appending content
540
+ * @returns Promise resolving to the created transaction
541
+ */
542
+ export async function createAppendTransaction(
543
+ signer: Signer,
544
+ options: AppendOptions,
545
+ ): Promise<Transaction> {
546
+ const {
547
+ ckbfsCell,
548
+ feeRate,
549
+ } = options;
550
+ const { lock } = ckbfsCell;
551
+
552
+ // Use prepareAppendTransaction to create the base transaction
553
+ const { tx: preTx, outputIndex } = await prepareAppendTransaction(options);
554
+
555
+ // Get the recommended address to ensure lock script cell deps are included
556
+ const address = await signer.getRecommendedAddressObj();
557
+
558
+ const inputsBefore = preTx.inputs.length;
559
+ // If we need more capacity than the original cell had, add additional inputs
560
+ if (preTx.outputs[outputIndex].capacity > ckbfsCell.capacity) {
561
+ console.log(
562
+ `Need additional capacity: ${preTx.outputs[outputIndex].capacity - ckbfsCell.capacity} shannons`,
563
+ );
564
+ // Add more inputs to cover the increased capacity
565
+ await preTx.completeInputsByCapacity(signer);
566
+ }
567
+
568
+ const witnesses: any = [];
569
+ // add empty witness for signer if ckbfs's lock is the same as signer's lock
570
+ if (address.script.hash() === lock.hash()) {
571
+ witnesses.push("0x");
572
+ }
573
+ // add ckbfs witnesses (skip the first witness which is for signing)
574
+ witnesses.push(...preTx.witnesses.slice(1));
575
+
576
+ // Add empty witnesses for additional signer inputs
577
+ // This is to ensure that the transaction is valid and can be signed
578
+ for (let i = inputsBefore; i < preTx.inputs.length; i++) {
579
+ witnesses.push("0x");
580
+ }
581
+ preTx.witnesses = witnesses;
582
+
583
+ // Complete fee
584
+ await preTx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
585
+
586
+ return preTx;
587
+ }
588
+
589
+ /**
590
+ * Creates a complete transaction for publishing a file to CKBFS
591
+ * @param signer The signer to use for the transaction
592
+ * @param options Options for publishing the file
593
+ * @returns Promise resolving to the signed transaction
594
+ */
595
+ export async function publishCKBFS(
596
+ signer: Signer,
597
+ options: PublishOptions,
598
+ ): Promise<Transaction> {
599
+ const tx = await createPublishTransaction(signer, options);
600
+ return signer.signTransaction(tx);
601
+ }
602
+
603
+ /**
604
+ * Creates a transaction for appending content to a CKBFS file (dry run without fee completion)
605
+ * @param signer The signer to use for the transaction
606
+ * @param options Options for appending content
607
+ * @returns Promise resolving to the created transaction
608
+ */
609
+ export async function createAppendTransactionDry(
610
+ signer: Signer,
611
+ options: AppendOptions,
612
+ ): Promise<Transaction> {
613
+ const {
614
+ ckbfsCell,
615
+ contentChunks,
616
+ network = DEFAULT_NETWORK,
617
+ version = DEFAULT_VERSION,
618
+ } = options;
619
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
620
+
621
+ // Get CKBFS script config early to use version info
622
+ const config = getCKBFSScriptConfig(network, version);
623
+
624
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
625
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
626
+ // not to be confused with the Protocol Version (V1 vs V2)
627
+ const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
628
+
629
+ // Combine the new content chunks for checksum calculation
630
+ const combinedContent = Buffer.concat(contentChunks);
631
+
632
+ // Update the existing checksum with the new content - this matches Adler32's
633
+ // cumulative nature as required by Rule 11 in the RFC
634
+ const contentChecksum = await updateChecksum(data.checksum, combinedContent);
635
+ console.log(
636
+ `Updated checksum from ${data.checksum} to ${contentChecksum} for appended content`,
637
+ );
638
+
639
+ // Get the recommended address to ensure lock script cell deps are included
640
+ const address = await signer.getRecommendedAddressObj();
641
+
642
+ // Calculate the actual witness indices where our content is placed
643
+ // CKBFS data starts at index 1 if signer's lock script is the same as ckbfs's lock script
644
+ // else CKBFS data starts at index 0
645
+ const contentStartIndex = address.script.hash() === lock.hash() ? 1 : 0;
646
+ const witnessIndices = Array.from(
647
+ { length: contentChunks.length },
648
+ (_, i) => contentStartIndex + i,
649
+ );
650
+
651
+ // Create backlink for the current state based on version
652
+ let newBackLink: any;
653
+
654
+ if (version === ProtocolVersion.V1) {
655
+ // V1 format: Use index field (single number)
656
+ newBackLink = {
657
+ // In V1, field order is index, checksum, txHash
658
+ // and index is a single number value, not an array
659
+ index:
660
+ data.index ||
661
+ (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
662
+ checksum: data.checksum,
663
+ txHash: outPoint.txHash,
664
+ };
665
+ } else {
666
+ // V2 format: Use indexes field (array of numbers)
667
+ newBackLink = {
668
+ // In V2, field order is indexes, checksum, txHash
669
+ // and indexes is an array of numbers
670
+ indexes: data.indexes || (data.index ? [data.index] : []),
671
+ checksum: data.checksum,
672
+ txHash: outPoint.txHash,
673
+ };
674
+ }
675
+
676
+ // Update backlinks - add the new one to the existing backlinks array
677
+ const backLinks = [...(data.backLinks || []), newBackLink];
678
+
679
+ // Define output data based on version
680
+ let outputData: Uint8Array;
681
+
682
+ if (version === ProtocolVersion.V1) {
683
+ // In V1, index is a single number, not an array
684
+ // The first witness index is used (V1 can only reference one witness)
685
+ outputData = CKBFSData.pack(
686
+ {
687
+ index: witnessIndices[0], // Use only the first index as a number
688
+ checksum: contentChecksum,
689
+ contentType: data.contentType,
690
+ filename: data.filename,
691
+ backLinks,
692
+ },
693
+ ProtocolVersion.V1,
694
+ ); // Explicitly use V1 for packing
695
+ } else {
696
+ // In V2, indexes is an array of witness indices
697
+ outputData = CKBFSData.pack(
698
+ {
699
+ indexes: witnessIndices,
700
+ checksum: contentChecksum,
701
+ contentType: data.contentType,
702
+ filename: data.filename,
703
+ backLinks,
704
+ },
705
+ ProtocolVersion.V2,
706
+ ); // Explicitly use V2 for packing
707
+ }
708
+
709
+ // Calculate the required capacity for the output cell
710
+ // This accounts for:
711
+ // 1. The output data size
712
+ // 2. The type script's occupied size
713
+ // 3. The lock script's occupied size
714
+ // 4. A constant of 8 bytes (for header overhead)
715
+ const ckbfsCellSize =
716
+ BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) *
717
+ 100000000n;
718
+
719
+ console.log(
720
+ `Original capacity: ${capacity}, Calculated size: ${ckbfsCellSize}, Data size: ${outputData.length}`,
721
+ );
722
+
723
+ // Use the maximum value between calculated size and original capacity
724
+ // to ensure we have enough capacity but don't decrease capacity unnecessarily
725
+ const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
726
+
727
+ // Create initial transaction with the CKBFS cell input
728
+ const tx = Transaction.from({
729
+ inputs: [
730
+ {
731
+ previousOutput: {
732
+ txHash: outPoint.txHash,
733
+ index: outPoint.index,
734
+ },
735
+ since: "0x0",
736
+ },
737
+ ],
738
+ outputs: [
739
+ {
740
+ lock,
741
+ type,
742
+ capacity: outputCapacity,
743
+ },
744
+ ],
745
+ outputsData: [outputData],
746
+ });
747
+
748
+ // Add the CKBFS dep group cell dependency
749
+ tx.addCellDeps({
750
+ outPoint: {
751
+ txHash: ensureHexPrefix(config.depTxHash),
752
+ index: config.depIndex || 0,
753
+ },
754
+ depType: "depGroup",
755
+ });
756
+
757
+ const inputsBefore = tx.inputs.length;
758
+
759
+ const witnesses: any = [];
760
+ // add empty witness for signer if ckbfs's lock is the same as signer's lock
761
+ if (address.script.hash() === lock.hash()) {
762
+ witnesses.push("0x");
763
+ }
764
+ // add ckbfs witnesses
765
+ witnesses.push(
766
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
767
+ );
768
+
769
+ // Add empty witnesses for signer's input
770
+ // This is to ensure that the transaction is valid and can be signed
771
+ for (let i = inputsBefore; i < tx.inputs.length; i++) {
772
+ witnesses.push("0x");
773
+ }
774
+ tx.witnesses = witnesses;
775
+
776
+ return tx;
777
+ }
778
+
779
+ /**
780
+ * Creates a complete transaction for appending content to a CKBFS file
781
+ * @param signer The signer to use for the transaction
782
+ * @param options Options for appending content
783
+ * @returns Promise resolving to the signed transaction
784
+ */
785
+ export async function appendCKBFS(
786
+ signer: Signer,
787
+ options: AppendOptions,
788
+ ): Promise<Transaction> {
789
+ const tx = await createAppendTransaction(signer, options);
790
+ return signer.signTransaction(tx);
791
+ }