@ckbfs/api 1.5.0 → 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 +440 -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 -105
  16. package/dist/utils/transaction.js +45 -565
  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,592 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCKBFSCell = createCKBFSCell;
4
+ exports.preparePublishTransaction = preparePublishTransaction;
5
+ exports.createPublishTransaction = createPublishTransaction;
6
+ exports.prepareAppendTransaction = prepareAppendTransaction;
7
+ exports.createAppendTransaction = createAppendTransaction;
8
+ exports.publishCKBFS = publishCKBFS;
9
+ exports.createAppendTransactionDry = createAppendTransactionDry;
10
+ exports.appendCKBFS = appendCKBFS;
11
+ const core_1 = require("@ckb-ccc/core");
12
+ const checksum_1 = require("../checksum");
13
+ const molecule_1 = require("../molecule");
14
+ const witness_1 = require("../witness");
15
+ const constants_1 = require("../constants");
16
+ const shared_1 = require("./shared");
17
+ /**
18
+ * Creates a CKBFS cell
19
+ * @param options Options for creating the CKBFS cell
20
+ * @returns The created cell output
21
+ */
22
+ function createCKBFSCell(options) {
23
+ const { contentType, filename, capacity, lock, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, useTypeID = false, } = options;
24
+ // Get CKBFS script config
25
+ const config = (0, constants_1.getCKBFSScriptConfig)(network, version, useTypeID);
26
+ // Create pre CKBFS type script
27
+ const preCkbfsTypeScript = new core_1.Script((0, shared_1.ensureHexPrefix)(config.codeHash), config.hashType, "0x0000000000000000000000000000000000000000000000000000000000000000");
28
+ // Return the cell output
29
+ return {
30
+ lock,
31
+ type: preCkbfsTypeScript,
32
+ capacity: capacity || 200n * 100000000n, // Default 200 CKB
33
+ };
34
+ }
35
+ /**
36
+ * Prepares a transaction for publishing a file to CKBFS without fee and change handling
37
+ * You will need to manually set the typeID if you did not provide inputs, or just check is return value emptyTypeID is true
38
+ * @param options Options for publishing the file
39
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
40
+ */
41
+ async function preparePublishTransaction(options) {
42
+ const { from, contentChunks, contentType, filename, lock, capacity, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, useTypeID = false, } = options;
43
+ // Calculate checksum for the combined content
44
+ const combinedContent = Buffer.concat(contentChunks);
45
+ const checksum = await (0, checksum_1.calculateChecksum)(combinedContent);
46
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
47
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
48
+ // not to be confused with the Protocol Version (V1 vs V2)
49
+ const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
50
+ // Calculate the actual witness indices where our content is placed
51
+ const contentStartIndex = from?.witnesses.length || 1;
52
+ const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
53
+ // Create CKBFS cell output data based on version
54
+ let outputData;
55
+ if (version === constants_1.ProtocolVersion.V1) {
56
+ // V1 format: Single index field (a single number, not an array)
57
+ // For V1, use the first index where content is placed
58
+ outputData = molecule_1.CKBFSData.pack({
59
+ index: contentStartIndex,
60
+ checksum,
61
+ contentType: contentType,
62
+ filename: filename,
63
+ backLinks: [],
64
+ }, version);
65
+ }
66
+ else {
67
+ // V2 format: Multiple indexes (array of numbers)
68
+ // For V2, use all the indices where content is placed
69
+ outputData = molecule_1.CKBFSData.pack({
70
+ indexes: witnessIndices,
71
+ checksum,
72
+ contentType,
73
+ filename,
74
+ backLinks: [],
75
+ }, version);
76
+ }
77
+ // Get CKBFS script config
78
+ const config = (0, constants_1.getCKBFSScriptConfig)(network, version, useTypeID);
79
+ const preCkbfsTypeScript = new core_1.Script((0, shared_1.ensureHexPrefix)(config.codeHash), config.hashType, "0x0000000000000000000000000000000000000000000000000000000000000000");
80
+ const ckbfsCellSize = BigInt(outputData.length +
81
+ preCkbfsTypeScript.occupiedSize +
82
+ lock.occupiedSize +
83
+ 8) * 100000000n;
84
+ // Create pre transaction without cell deps initially
85
+ let preTx;
86
+ if (from) {
87
+ // If from is not empty, inject/merge the fields
88
+ preTx = core_1.Transaction.from({
89
+ ...from,
90
+ outputs: from.outputs.length === 0
91
+ ? [
92
+ createCKBFSCell({
93
+ contentType,
94
+ filename,
95
+ lock,
96
+ network,
97
+ version,
98
+ useTypeID,
99
+ capacity: ckbfsCellSize || capacity,
100
+ }),
101
+ ]
102
+ : [
103
+ ...from.outputs,
104
+ createCKBFSCell({
105
+ contentType,
106
+ filename,
107
+ lock,
108
+ network,
109
+ version,
110
+ useTypeID,
111
+ capacity: ckbfsCellSize || capacity,
112
+ }),
113
+ ],
114
+ witnesses: from.witnesses.length === 0
115
+ ? [
116
+ [], // Empty secp witness for signing if not provided
117
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
118
+ ]
119
+ : [
120
+ ...from.witnesses,
121
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
122
+ ],
123
+ outputsData: from.outputsData.length === 0
124
+ ? [outputData]
125
+ : [
126
+ ...from.outputsData,
127
+ outputData,
128
+ ],
129
+ });
130
+ }
131
+ else {
132
+ preTx = core_1.Transaction.from({
133
+ outputs: [
134
+ createCKBFSCell({
135
+ contentType,
136
+ filename,
137
+ lock,
138
+ network,
139
+ version,
140
+ useTypeID,
141
+ capacity: ckbfsCellSize || capacity,
142
+ }),
143
+ ],
144
+ witnesses: [
145
+ [], // Empty secp witness for signing
146
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
147
+ ],
148
+ outputsData: [outputData],
149
+ });
150
+ }
151
+ // Add the CKBFS dep group cell dependency
152
+ preTx.addCellDeps({
153
+ outPoint: {
154
+ txHash: (0, shared_1.ensureHexPrefix)(config.depTxHash),
155
+ index: config.depIndex || 0,
156
+ },
157
+ depType: "depGroup",
158
+ });
159
+ // Create type ID args
160
+ const outputIndex = from ? from.outputs.length : 0;
161
+ const args = preTx.inputs.length > 0 ? core_1.ccc.hashTypeId(preTx.inputs[0], outputIndex) : "0x0000000000000000000000000000000000000000000000000000000000000000";
162
+ // Create CKBFS type script with type ID
163
+ const ckbfsTypeScript = new core_1.Script((0, shared_1.ensureHexPrefix)(config.codeHash), config.hashType, args);
164
+ // Create final transaction with same cell deps as preTx
165
+ const tx = core_1.Transaction.from({
166
+ cellDeps: preTx.cellDeps,
167
+ witnesses: preTx.witnesses,
168
+ outputsData: preTx.outputsData,
169
+ inputs: preTx.inputs,
170
+ outputs: outputIndex === 0
171
+ ? [
172
+ {
173
+ lock,
174
+ type: ckbfsTypeScript,
175
+ capacity: preTx.outputs[outputIndex].capacity,
176
+ },
177
+ ]
178
+ : [
179
+ ...preTx.outputs.slice(0, outputIndex), // Include rest of outputs (e.g., change)
180
+ {
181
+ lock,
182
+ type: ckbfsTypeScript,
183
+ capacity: preTx.outputs[outputIndex].capacity,
184
+ },
185
+ ],
186
+ });
187
+ return { tx, outputIndex, emptyTypeID: args === "0x0000000000000000000000000000000000000000000000000000000000000000" };
188
+ }
189
+ /**
190
+ * Creates a transaction for publishing a file to CKBFS
191
+ * @param signer The signer to use for the transaction
192
+ * @param options Options for publishing the file
193
+ * @returns Promise resolving to the created transaction
194
+ */
195
+ async function createPublishTransaction(signer, options) {
196
+ const { feeRate, lock, } = options;
197
+ // Use preparePublishTransaction to create the base transaction
198
+ const { tx: preTx, outputIndex, emptyTypeID } = await preparePublishTransaction(options);
199
+ // Complete inputs by capacity
200
+ await preTx.completeInputsByCapacity(signer);
201
+ // Complete fee change to lock
202
+ await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
203
+ // If emptyTypeID is true, we need to create the proper type ID args
204
+ if (emptyTypeID) {
205
+ // Get CKBFS script config
206
+ const config = (0, constants_1.getCKBFSScriptConfig)(options.network || constants_1.DEFAULT_NETWORK, options.version || constants_1.DEFAULT_VERSION, options.useTypeID || false);
207
+ // Create type ID args
208
+ const args = core_1.ccc.hashTypeId(preTx.inputs[0], outputIndex);
209
+ // Create CKBFS type script with type ID
210
+ const ckbfsTypeScript = new core_1.Script((0, shared_1.ensureHexPrefix)(config.codeHash), config.hashType, args);
211
+ // Create final transaction with updated type script
212
+ const tx = core_1.Transaction.from({
213
+ cellDeps: preTx.cellDeps,
214
+ witnesses: preTx.witnesses,
215
+ outputsData: preTx.outputsData,
216
+ inputs: preTx.inputs,
217
+ outputs: preTx.outputs.map((output, index) => index === outputIndex
218
+ ? {
219
+ ...output,
220
+ type: ckbfsTypeScript,
221
+ }
222
+ : output),
223
+ });
224
+ return tx;
225
+ }
226
+ return preTx;
227
+ }
228
+ /**
229
+ * Prepares a transaction for appending content to a CKBFS file without fee and change handling
230
+ * @param options Options for appending content
231
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
232
+ */
233
+ async function prepareAppendTransaction(options) {
234
+ const { from, ckbfsCell, contentChunks, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, } = options;
235
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
236
+ // Get CKBFS script config early to use version info
237
+ const config = (0, constants_1.getCKBFSScriptConfig)(network, version);
238
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
239
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
240
+ // not to be confused with the Protocol Version (V1 vs V2)
241
+ const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
242
+ // Combine the new content chunks for checksum calculation
243
+ const combinedContent = Buffer.concat(contentChunks);
244
+ // Update the existing checksum with the new content - this matches Adler32's
245
+ // cumulative nature as required by Rule 11 in the RFC
246
+ const contentChecksum = await (0, checksum_1.updateChecksum)(data.checksum, combinedContent);
247
+ // Calculate the actual witness indices where our content is placed
248
+ const contentStartIndex = from?.witnesses.length || 1;
249
+ const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
250
+ // Create backlink for the current state based on version
251
+ let newBackLink;
252
+ if (version === constants_1.ProtocolVersion.V1) {
253
+ // V1 format: Use index field (single number)
254
+ newBackLink = {
255
+ // In V1, field order is index, checksum, txHash
256
+ // and index is a single number value, not an array
257
+ index: data.index ||
258
+ (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
259
+ checksum: data.checksum,
260
+ txHash: outPoint.txHash,
261
+ };
262
+ }
263
+ else {
264
+ // V2 format: Use indexes field (array of numbers)
265
+ newBackLink = {
266
+ // In V2, field order is indexes, checksum, txHash
267
+ // and indexes is an array of numbers
268
+ indexes: data.indexes || (data.index ? [data.index] : []),
269
+ checksum: data.checksum,
270
+ txHash: outPoint.txHash,
271
+ };
272
+ }
273
+ // Update backlinks - add the new one to the existing backlinks array
274
+ const backLinks = [...(data.backLinks || []), newBackLink];
275
+ // Define output data based on version
276
+ let outputData;
277
+ if (version === constants_1.ProtocolVersion.V1) {
278
+ // In V1, index is a single number, not an array
279
+ // The first witness index is used (V1 can only reference one witness)
280
+ outputData = molecule_1.CKBFSData.pack({
281
+ index: witnessIndices[0], // Use only the first index as a number
282
+ checksum: contentChecksum,
283
+ contentType: data.contentType,
284
+ filename: data.filename,
285
+ backLinks,
286
+ }, constants_1.ProtocolVersion.V1); // Explicitly use V1 for packing
287
+ }
288
+ else {
289
+ // In V2, indexes is an array of witness indices
290
+ outputData = molecule_1.CKBFSData.pack({
291
+ indexes: witnessIndices,
292
+ checksum: contentChecksum,
293
+ contentType: data.contentType,
294
+ filename: data.filename,
295
+ backLinks,
296
+ }, constants_1.ProtocolVersion.V2); // Explicitly use V2 for packing
297
+ }
298
+ // Calculate the required capacity for the output cell
299
+ // This accounts for:
300
+ // 1. The output data size
301
+ // 2. The type script's occupied size
302
+ // 3. The lock script's occupied size
303
+ // 4. A constant of 8 bytes (for header overhead)
304
+ const ckbfsCellSize = BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) *
305
+ 100000000n;
306
+ // Use the maximum value between calculated size and original capacity
307
+ // to ensure we have enough capacity but don't decrease capacity unnecessarily
308
+ const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
309
+ // Create initial transaction with the CKBFS cell input
310
+ let preTx;
311
+ if (from) {
312
+ // If from is not empty, inject/merge the fields
313
+ preTx = core_1.Transaction.from({
314
+ ...from,
315
+ inputs: from.inputs.length === 0
316
+ ? [
317
+ {
318
+ previousOutput: {
319
+ txHash: outPoint.txHash,
320
+ index: outPoint.index,
321
+ },
322
+ since: "0x0",
323
+ },
324
+ ]
325
+ : [
326
+ ...from.inputs,
327
+ {
328
+ previousOutput: {
329
+ txHash: outPoint.txHash,
330
+ index: outPoint.index,
331
+ },
332
+ since: "0x0",
333
+ },
334
+ ],
335
+ outputs: from.outputs.length === 0
336
+ ? [
337
+ {
338
+ lock,
339
+ type,
340
+ capacity: outputCapacity,
341
+ },
342
+ ]
343
+ : [
344
+ ...from.outputs,
345
+ {
346
+ lock,
347
+ type,
348
+ capacity: outputCapacity,
349
+ },
350
+ ],
351
+ outputsData: from.outputsData.length === 0
352
+ ? [outputData]
353
+ : [
354
+ ...from.outputsData,
355
+ outputData,
356
+ ],
357
+ witnesses: from.witnesses.length === 0
358
+ ? [
359
+ [], // Empty secp witness for signing if not provided
360
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
361
+ ]
362
+ : [
363
+ ...from.witnesses,
364
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
365
+ ],
366
+ });
367
+ }
368
+ else {
369
+ preTx = core_1.Transaction.from({
370
+ inputs: [
371
+ {
372
+ previousOutput: {
373
+ txHash: outPoint.txHash,
374
+ index: outPoint.index,
375
+ },
376
+ since: "0x0",
377
+ },
378
+ ],
379
+ outputs: [
380
+ {
381
+ lock,
382
+ type,
383
+ capacity: outputCapacity,
384
+ },
385
+ ],
386
+ outputsData: [outputData],
387
+ witnesses: [
388
+ [], // Empty secp witness for signing
389
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
390
+ ],
391
+ });
392
+ }
393
+ // Add the CKBFS dep group cell dependency
394
+ preTx.addCellDeps({
395
+ outPoint: {
396
+ txHash: (0, shared_1.ensureHexPrefix)(config.depTxHash),
397
+ index: config.depIndex || 0,
398
+ },
399
+ depType: "depGroup",
400
+ });
401
+ const outputIndex = from ? from.outputs.length : 0;
402
+ return { tx: preTx, outputIndex };
403
+ }
404
+ /**
405
+ * Creates a transaction for appending content to a CKBFS file
406
+ * @param signer The signer to use for the transaction
407
+ * @param options Options for appending content
408
+ * @returns Promise resolving to the created transaction
409
+ */
410
+ async function createAppendTransaction(signer, options) {
411
+ const { ckbfsCell, feeRate, } = options;
412
+ const { lock } = ckbfsCell;
413
+ // Use prepareAppendTransaction to create the base transaction
414
+ const { tx: preTx, outputIndex } = await prepareAppendTransaction(options);
415
+ // Get the recommended address to ensure lock script cell deps are included
416
+ const address = await signer.getRecommendedAddressObj();
417
+ const inputsBefore = preTx.inputs.length;
418
+ // If we need more capacity than the original cell had, add additional inputs
419
+ if (preTx.outputs[outputIndex].capacity > ckbfsCell.capacity) {
420
+ console.log(`Need additional capacity: ${preTx.outputs[outputIndex].capacity - ckbfsCell.capacity} shannons`);
421
+ // Add more inputs to cover the increased capacity
422
+ await preTx.completeInputsByCapacity(signer);
423
+ }
424
+ const witnesses = [];
425
+ // add empty witness for signer if ckbfs's lock is the same as signer's lock
426
+ if (address.script.hash() === lock.hash()) {
427
+ witnesses.push("0x");
428
+ }
429
+ // add ckbfs witnesses (skip the first witness which is for signing)
430
+ witnesses.push(...preTx.witnesses.slice(1));
431
+ // Add empty witnesses for additional signer inputs
432
+ // This is to ensure that the transaction is valid and can be signed
433
+ for (let i = inputsBefore; i < preTx.inputs.length; i++) {
434
+ witnesses.push("0x");
435
+ }
436
+ preTx.witnesses = witnesses;
437
+ // Complete fee
438
+ await preTx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
439
+ return preTx;
440
+ }
441
+ /**
442
+ * Creates a complete transaction for publishing a file to CKBFS
443
+ * @param signer The signer to use for the transaction
444
+ * @param options Options for publishing the file
445
+ * @returns Promise resolving to the signed transaction
446
+ */
447
+ async function publishCKBFS(signer, options) {
448
+ const tx = await createPublishTransaction(signer, options);
449
+ return signer.signTransaction(tx);
450
+ }
451
+ /**
452
+ * Creates a transaction for appending content to a CKBFS file (dry run without fee completion)
453
+ * @param signer The signer to use for the transaction
454
+ * @param options Options for appending content
455
+ * @returns Promise resolving to the created transaction
456
+ */
457
+ async function createAppendTransactionDry(signer, options) {
458
+ const { ckbfsCell, contentChunks, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, } = options;
459
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
460
+ // Get CKBFS script config early to use version info
461
+ const config = (0, constants_1.getCKBFSScriptConfig)(network, version);
462
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
463
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
464
+ // not to be confused with the Protocol Version (V1 vs V2)
465
+ const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
466
+ // Combine the new content chunks for checksum calculation
467
+ const combinedContent = Buffer.concat(contentChunks);
468
+ // Update the existing checksum with the new content - this matches Adler32's
469
+ // cumulative nature as required by Rule 11 in the RFC
470
+ const contentChecksum = await (0, checksum_1.updateChecksum)(data.checksum, combinedContent);
471
+ console.log(`Updated checksum from ${data.checksum} to ${contentChecksum} for appended content`);
472
+ // Get the recommended address to ensure lock script cell deps are included
473
+ const address = await signer.getRecommendedAddressObj();
474
+ // Calculate the actual witness indices where our content is placed
475
+ // CKBFS data starts at index 1 if signer's lock script is the same as ckbfs's lock script
476
+ // else CKBFS data starts at index 0
477
+ const contentStartIndex = address.script.hash() === lock.hash() ? 1 : 0;
478
+ const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
479
+ // Create backlink for the current state based on version
480
+ let newBackLink;
481
+ if (version === constants_1.ProtocolVersion.V1) {
482
+ // V1 format: Use index field (single number)
483
+ newBackLink = {
484
+ // In V1, field order is index, checksum, txHash
485
+ // and index is a single number value, not an array
486
+ index: data.index ||
487
+ (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
488
+ checksum: data.checksum,
489
+ txHash: outPoint.txHash,
490
+ };
491
+ }
492
+ else {
493
+ // V2 format: Use indexes field (array of numbers)
494
+ newBackLink = {
495
+ // In V2, field order is indexes, checksum, txHash
496
+ // and indexes is an array of numbers
497
+ indexes: data.indexes || (data.index ? [data.index] : []),
498
+ checksum: data.checksum,
499
+ txHash: outPoint.txHash,
500
+ };
501
+ }
502
+ // Update backlinks - add the new one to the existing backlinks array
503
+ const backLinks = [...(data.backLinks || []), newBackLink];
504
+ // Define output data based on version
505
+ let outputData;
506
+ if (version === constants_1.ProtocolVersion.V1) {
507
+ // In V1, index is a single number, not an array
508
+ // The first witness index is used (V1 can only reference one witness)
509
+ outputData = molecule_1.CKBFSData.pack({
510
+ index: witnessIndices[0], // Use only the first index as a number
511
+ checksum: contentChecksum,
512
+ contentType: data.contentType,
513
+ filename: data.filename,
514
+ backLinks,
515
+ }, constants_1.ProtocolVersion.V1); // Explicitly use V1 for packing
516
+ }
517
+ else {
518
+ // In V2, indexes is an array of witness indices
519
+ outputData = molecule_1.CKBFSData.pack({
520
+ indexes: witnessIndices,
521
+ checksum: contentChecksum,
522
+ contentType: data.contentType,
523
+ filename: data.filename,
524
+ backLinks,
525
+ }, constants_1.ProtocolVersion.V2); // Explicitly use V2 for packing
526
+ }
527
+ // Calculate the required capacity for the output cell
528
+ // This accounts for:
529
+ // 1. The output data size
530
+ // 2. The type script's occupied size
531
+ // 3. The lock script's occupied size
532
+ // 4. A constant of 8 bytes (for header overhead)
533
+ const ckbfsCellSize = BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) *
534
+ 100000000n;
535
+ console.log(`Original capacity: ${capacity}, Calculated size: ${ckbfsCellSize}, Data size: ${outputData.length}`);
536
+ // Use the maximum value between calculated size and original capacity
537
+ // to ensure we have enough capacity but don't decrease capacity unnecessarily
538
+ const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
539
+ // Create initial transaction with the CKBFS cell input
540
+ const tx = core_1.Transaction.from({
541
+ inputs: [
542
+ {
543
+ previousOutput: {
544
+ txHash: outPoint.txHash,
545
+ index: outPoint.index,
546
+ },
547
+ since: "0x0",
548
+ },
549
+ ],
550
+ outputs: [
551
+ {
552
+ lock,
553
+ type,
554
+ capacity: outputCapacity,
555
+ },
556
+ ],
557
+ outputsData: [outputData],
558
+ });
559
+ // Add the CKBFS dep group cell dependency
560
+ tx.addCellDeps({
561
+ outPoint: {
562
+ txHash: (0, shared_1.ensureHexPrefix)(config.depTxHash),
563
+ index: config.depIndex || 0,
564
+ },
565
+ depType: "depGroup",
566
+ });
567
+ const inputsBefore = tx.inputs.length;
568
+ const witnesses = [];
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
574
+ witnesses.push(...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`));
575
+ // Add empty witnesses for signer's input
576
+ // This is to ensure that the transaction is valid and can be signed
577
+ for (let i = inputsBefore; i < tx.inputs.length; i++) {
578
+ witnesses.push("0x");
579
+ }
580
+ tx.witnesses = witnesses;
581
+ return tx;
582
+ }
583
+ /**
584
+ * Creates a complete transaction for appending content to a CKBFS file
585
+ * @param signer The signer to use for the transaction
586
+ * @param options Options for appending content
587
+ * @returns Promise resolving to the signed transaction
588
+ */
589
+ async function appendCKBFS(signer, options) {
590
+ const tx = await createAppendTransaction(signer, options);
591
+ return signer.signTransaction(tx);
592
+ }