@ckbfs/api 1.4.0 → 1.5.1

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.
package/README.md CHANGED
@@ -14,7 +14,6 @@ CKBFS is a file system protocol built on top of the CKB blockchain that enables
14
14
  - **Protocol Versions**: Support for both V1 and V2 CKBFS protocol versions
15
15
  - **Network Support**: Compatible with CKB mainnet and testnet
16
16
  - **Chunked Storage**: Automatic file chunking for large files
17
- - **TypeScript**: Full type safety with comprehensive type definitions
18
17
 
19
18
  ## Installation
20
19
 
@@ -567,4 +566,4 @@ Contributions are welcome. Please ensure all tests pass and follow the existing
567
566
 
568
567
  ## Support
569
568
 
570
- For issues and questions, please use the GitHub issue tracker.
569
+ For issues and questions, please use the GitHub issue tracker.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Script, Signer, Transaction } from "@ckb-ccc/core";
2
2
  import { calculateChecksum, verifyChecksum, updateChecksum, verifyWitnessChecksum } from "./utils/checksum";
3
- import { createCKBFSCell, createPublishTransaction as utilCreatePublishTransaction, createAppendTransaction as utilCreateAppendTransaction, publishCKBFS as utilPublishCKBFS, appendCKBFS as utilAppendCKBFS, CKBFSCellOptions, PublishOptions, AppendOptions } from "./utils/transaction";
3
+ import { createCKBFSCell, createPublishTransaction as utilCreatePublishTransaction, preparePublishTransaction, createAppendTransaction as utilCreateAppendTransaction, prepareAppendTransaction, createAppendTransactionDry, publishCKBFS as utilPublishCKBFS, appendCKBFS as utilAppendCKBFS, CKBFSCellOptions, PublishOptions, AppendOptions } from "./utils/transaction";
4
4
  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";
5
5
  import { createCKBFSWitness, createTextCKBFSWitness, extractCKBFSWitnessContent, isCKBFSWitness, createChunkedCKBFSWitnesses } from "./utils/witness";
6
6
  import { CKBFSData, BackLinkV1, BackLinkV2, CKBFSDataType, BackLinkType, CKBFS_HEADER, CKBFS_HEADER_STRING } from "./utils/molecule";
@@ -37,6 +37,7 @@ export interface CKBFSOptions {
37
37
  version?: ProtocolVersionType;
38
38
  useTypeID?: boolean;
39
39
  network?: NetworkType;
40
+ rpcUrl?: string;
40
41
  }
41
42
  /**
42
43
  * Main CKBFS SDK class
@@ -47,6 +48,7 @@ export declare class CKBFS {
47
48
  private network;
48
49
  private version;
49
50
  private useTypeID;
51
+ private rpcUrl;
50
52
  /**
51
53
  * Creates a new CKBFS SDK instance
52
54
  * @param signerOrPrivateKey The signer instance or CKB private key to use for signing transactions
@@ -130,4 +132,4 @@ export declare class CKBFS {
130
132
  */
131
133
  createAppendContentTransaction(content: string | Uint8Array, ckbfsCell: AppendOptions["ckbfsCell"], options?: AppendContentOptions): Promise<Transaction>;
132
134
  }
133
- export { calculateChecksum, verifyChecksum, updateChecksum, verifyWitnessChecksum, createCKBFSCell, utilCreatePublishTransaction as createPublishTransaction, utilCreateAppendTransaction as createAppendTransaction, utilPublishCKBFS as publishCKBFS, utilAppendCKBFS as appendCKBFS, readFile, readFileAsText, readFileAsUint8Array, writeFile, getContentType, splitFileIntoChunks, combineChunksToFile, getFileContentFromChain, saveFileFromChain, getFileContentFromChainByTypeId, saveFileFromChainByTypeId, decodeFileFromChainByTypeId, getFileContentFromChainByIdentifier, saveFileFromChainByIdentifier, decodeFileFromChainByIdentifier, parseIdentifier, IdentifierType, decodeWitnessContent, decodeMultipleWitnessContents, extractFileFromWitnesses, decodeFileFromWitnessData, saveFileFromWitnessData, createCKBFSWitness, createTextCKBFSWitness, extractCKBFSWitnessContent, isCKBFSWitness, createChunkedCKBFSWitnesses, CKBFSData, BackLinkV1, BackLinkV2, CKBFSDataType, BackLinkType, CKBFSCellOptions, PublishOptions, AppendOptions, CKBFS_HEADER, CKBFS_HEADER_STRING, 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, };
135
+ export { calculateChecksum, verifyChecksum, updateChecksum, verifyWitnessChecksum, createCKBFSCell, utilCreatePublishTransaction as createPublishTransaction, preparePublishTransaction, utilCreateAppendTransaction as createAppendTransaction, prepareAppendTransaction, utilPublishCKBFS as publishCKBFS, utilAppendCKBFS as appendCKBFS, createAppendTransactionDry, readFile, readFileAsText, readFileAsUint8Array, writeFile, getContentType, splitFileIntoChunks, combineChunksToFile, getFileContentFromChain, saveFileFromChain, getFileContentFromChainByTypeId, saveFileFromChainByTypeId, decodeFileFromChainByTypeId, getFileContentFromChainByIdentifier, saveFileFromChainByIdentifier, decodeFileFromChainByIdentifier, parseIdentifier, IdentifierType, decodeWitnessContent, decodeMultipleWitnessContents, extractFileFromWitnesses, decodeFileFromWitnessData, saveFileFromWitnessData, createCKBFSWitness, createTextCKBFSWitness, extractCKBFSWitnessContent, isCKBFSWitness, createChunkedCKBFSWitnesses, CKBFSData, BackLinkV1, BackLinkV2, CKBFSDataType, BackLinkType, CKBFSCellOptions, PublishOptions, AppendOptions, CKBFS_HEADER, CKBFS_HEADER_STRING, 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, };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ADLER32_TYPE_ID = exports.ADLER32_CODE_HASH = exports.CKBFS_TYPE_ID = exports.CKBFS_CODE_HASH = exports.DEFAULT_VERSION = exports.DEFAULT_NETWORK = exports.ProtocolVersion = exports.NetworkType = exports.CKBFS_HEADER_STRING = exports.CKBFS_HEADER = exports.BackLinkV2 = exports.BackLinkV1 = exports.CKBFSData = exports.createChunkedCKBFSWitnesses = exports.isCKBFSWitness = exports.extractCKBFSWitnessContent = exports.createTextCKBFSWitness = exports.createCKBFSWitness = exports.saveFileFromWitnessData = exports.decodeFileFromWitnessData = exports.extractFileFromWitnesses = exports.decodeMultipleWitnessContents = exports.decodeWitnessContent = exports.IdentifierType = exports.parseIdentifier = exports.decodeFileFromChainByIdentifier = exports.saveFileFromChainByIdentifier = exports.getFileContentFromChainByIdentifier = exports.decodeFileFromChainByTypeId = exports.saveFileFromChainByTypeId = exports.getFileContentFromChainByTypeId = exports.saveFileFromChain = exports.getFileContentFromChain = exports.combineChunksToFile = exports.splitFileIntoChunks = exports.getContentType = exports.writeFile = exports.readFileAsUint8Array = exports.readFileAsText = exports.readFile = exports.appendCKBFS = exports.publishCKBFS = exports.createAppendTransaction = exports.createPublishTransaction = exports.createCKBFSCell = exports.verifyWitnessChecksum = exports.updateChecksum = exports.verifyChecksum = exports.calculateChecksum = exports.CKBFS = void 0;
4
- exports.getCKBFSScriptConfig = exports.DEPLOY_TX_HASH = exports.DEP_GROUP_TX_HASH = void 0;
3
+ exports.CKBFS_CODE_HASH = exports.DEFAULT_VERSION = exports.DEFAULT_NETWORK = exports.ProtocolVersion = exports.NetworkType = exports.CKBFS_HEADER_STRING = exports.CKBFS_HEADER = exports.BackLinkV2 = exports.BackLinkV1 = exports.CKBFSData = exports.createChunkedCKBFSWitnesses = exports.isCKBFSWitness = exports.extractCKBFSWitnessContent = exports.createTextCKBFSWitness = exports.createCKBFSWitness = exports.saveFileFromWitnessData = exports.decodeFileFromWitnessData = exports.extractFileFromWitnesses = exports.decodeMultipleWitnessContents = exports.decodeWitnessContent = exports.IdentifierType = exports.parseIdentifier = exports.decodeFileFromChainByIdentifier = exports.saveFileFromChainByIdentifier = exports.getFileContentFromChainByIdentifier = exports.decodeFileFromChainByTypeId = exports.saveFileFromChainByTypeId = exports.getFileContentFromChainByTypeId = exports.saveFileFromChain = exports.getFileContentFromChain = exports.combineChunksToFile = exports.splitFileIntoChunks = exports.getContentType = exports.writeFile = exports.readFileAsUint8Array = exports.readFileAsText = exports.readFile = exports.createAppendTransactionDry = exports.appendCKBFS = exports.publishCKBFS = exports.prepareAppendTransaction = exports.createAppendTransaction = exports.preparePublishTransaction = exports.createPublishTransaction = exports.createCKBFSCell = exports.verifyWitnessChecksum = exports.updateChecksum = exports.verifyChecksum = exports.calculateChecksum = exports.CKBFS = void 0;
4
+ exports.getCKBFSScriptConfig = exports.DEPLOY_TX_HASH = exports.DEP_GROUP_TX_HASH = exports.ADLER32_TYPE_ID = exports.ADLER32_CODE_HASH = exports.CKBFS_TYPE_ID = void 0;
5
5
  const core_1 = require("@ckb-ccc/core");
6
6
  const checksum_1 = require("./utils/checksum");
7
7
  Object.defineProperty(exports, "calculateChecksum", { enumerable: true, get: function () { return checksum_1.calculateChecksum; } });
@@ -11,7 +11,10 @@ Object.defineProperty(exports, "verifyWitnessChecksum", { enumerable: true, get:
11
11
  const transaction_1 = require("./utils/transaction");
12
12
  Object.defineProperty(exports, "createCKBFSCell", { enumerable: true, get: function () { return transaction_1.createCKBFSCell; } });
13
13
  Object.defineProperty(exports, "createPublishTransaction", { enumerable: true, get: function () { return transaction_1.createPublishTransaction; } });
14
+ Object.defineProperty(exports, "preparePublishTransaction", { enumerable: true, get: function () { return transaction_1.preparePublishTransaction; } });
14
15
  Object.defineProperty(exports, "createAppendTransaction", { enumerable: true, get: function () { return transaction_1.createAppendTransaction; } });
16
+ Object.defineProperty(exports, "prepareAppendTransaction", { enumerable: true, get: function () { return transaction_1.prepareAppendTransaction; } });
17
+ Object.defineProperty(exports, "createAppendTransactionDry", { enumerable: true, get: function () { return transaction_1.createAppendTransactionDry; } });
15
18
  Object.defineProperty(exports, "publishCKBFS", { enumerable: true, get: function () { return transaction_1.publishCKBFS; } });
16
19
  Object.defineProperty(exports, "appendCKBFS", { enumerable: true, get: function () { return transaction_1.appendCKBFS; } });
17
20
  const file_1 = require("./utils/file");
@@ -85,13 +88,18 @@ class CKBFS {
85
88
  const opts = options ||
86
89
  (typeof networkOrOptions === "object" ? networkOrOptions : {});
87
90
  const client = network === "mainnet"
88
- ? new core_1.ClientPublicMainnet()
89
- : new core_1.ClientPublicTestnet();
91
+ ? new core_1.ClientPublicMainnet({
92
+ url: opts.rpcUrl,
93
+ })
94
+ : new core_1.ClientPublicTestnet({
95
+ url: opts.rpcUrl,
96
+ });
90
97
  this.signer = new core_1.SignerCkbPrivateKey(client, privateKey);
91
98
  this.network = network;
92
99
  this.chunkSize = opts.chunkSize || 30 * 1024;
93
100
  this.version = opts.version || constants_1.DEFAULT_VERSION;
94
101
  this.useTypeID = opts.useTypeID || false;
102
+ this.rpcUrl = opts.rpcUrl || client.url;
95
103
  }
96
104
  else {
97
105
  // Initialize with signer
@@ -101,6 +109,7 @@ class CKBFS {
101
109
  this.chunkSize = opts.chunkSize || 30 * 1024;
102
110
  this.version = opts.version || constants_1.DEFAULT_VERSION;
103
111
  this.useTypeID = opts.useTypeID || false;
112
+ this.rpcUrl = opts.rpcUrl || this.signer.client.url;
104
113
  }
105
114
  }
106
115
  /**
@@ -22,6 +22,7 @@ export interface CKBFSCellOptions {
22
22
  export interface PublishOptions extends CKBFSCellOptions {
23
23
  contentChunks: Uint8Array[];
24
24
  feeRate?: number;
25
+ from?: Transaction;
25
26
  }
26
27
  /**
27
28
  * Options for appending content to a CKBFS file
@@ -41,6 +42,7 @@ export interface AppendOptions {
41
42
  feeRate?: number;
42
43
  network?: NetworkType;
43
44
  version?: ProtocolVersionType;
45
+ from?: Transaction;
44
46
  }
45
47
  /**
46
48
  * Ensures a string is prefixed with '0x'
@@ -58,6 +60,17 @@ export declare function createCKBFSCell(options: CKBFSCellOptions): {
58
60
  type: ccc.Script;
59
61
  capacity: bigint;
60
62
  };
63
+ /**
64
+ * Prepares a transaction for publishing a file to CKBFS without fee and change handling
65
+ * You will need to manually set the typeID if you did not provide inputs, or just check is return value emptyTypeID is true
66
+ * @param options Options for publishing the file
67
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
68
+ */
69
+ export declare function preparePublishTransaction(options: PublishOptions): Promise<{
70
+ tx: Transaction;
71
+ outputIndex: number;
72
+ emptyTypeID: boolean;
73
+ }>;
61
74
  /**
62
75
  * Creates a transaction for publishing a file to CKBFS
63
76
  * @param signer The signer to use for the transaction
@@ -65,6 +78,15 @@ export declare function createCKBFSCell(options: CKBFSCellOptions): {
65
78
  * @returns Promise resolving to the created transaction
66
79
  */
67
80
  export declare function createPublishTransaction(signer: Signer, options: PublishOptions): Promise<Transaction>;
81
+ /**
82
+ * Prepares a transaction for appending content to a CKBFS file without fee and change handling
83
+ * @param options Options for appending content
84
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
85
+ */
86
+ export declare function prepareAppendTransaction(options: AppendOptions): Promise<{
87
+ tx: Transaction;
88
+ outputIndex: number;
89
+ }>;
68
90
  /**
69
91
  * Creates a transaction for appending content to a CKBFS file
70
92
  * @param signer The signer to use for the transaction
@@ -72,6 +94,13 @@ export declare function createPublishTransaction(signer: Signer, options: Publis
72
94
  * @returns Promise resolving to the created transaction
73
95
  */
74
96
  export declare function createAppendTransaction(signer: Signer, options: AppendOptions): Promise<Transaction>;
97
+ /**
98
+ * Creates a transaction for appending content to a CKBFS file
99
+ * @param signer The signer to use for the transaction
100
+ * @param options Options for appending content
101
+ * @returns Promise resolving to the created transaction
102
+ */
103
+ export declare function createAppendTransactionDry(signer: Signer, options: AppendOptions): Promise<Transaction>;
75
104
  /**
76
105
  * Creates a complete transaction for publishing a file to CKBFS
77
106
  * @param signer The signer to use for the transaction
@@ -2,8 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureHexPrefix = ensureHexPrefix;
4
4
  exports.createCKBFSCell = createCKBFSCell;
5
+ exports.preparePublishTransaction = preparePublishTransaction;
5
6
  exports.createPublishTransaction = createPublishTransaction;
7
+ exports.prepareAppendTransaction = prepareAppendTransaction;
6
8
  exports.createAppendTransaction = createAppendTransaction;
9
+ exports.createAppendTransactionDry = createAppendTransactionDry;
7
10
  exports.publishCKBFS = publishCKBFS;
8
11
  exports.appendCKBFS = appendCKBFS;
9
12
  const core_1 = require("@ckb-ccc/core");
@@ -41,15 +44,14 @@ function createCKBFSCell(options) {
41
44
  };
42
45
  }
43
46
  /**
44
- * Creates a transaction for publishing a file to CKBFS
45
- * @param signer The signer to use for the transaction
47
+ * Prepares a transaction for publishing a file to CKBFS without fee and change handling
48
+ * You will need to manually set the typeID if you did not provide inputs, or just check is return value emptyTypeID is true
46
49
  * @param options Options for publishing the file
47
- * @returns Promise resolving to the created transaction
50
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
48
51
  */
49
- async function createPublishTransaction(signer, options) {
50
- const { contentChunks, contentType, filename, lock, capacity, feeRate, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, useTypeID = false, } = options;
52
+ async function preparePublishTransaction(options) {
53
+ const { from, contentChunks, contentType, filename, lock, capacity, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, useTypeID = false, } = options;
51
54
  // Calculate checksum for the combined content
52
- const textEncoder = new TextEncoder();
53
55
  const combinedContent = Buffer.concat(contentChunks);
54
56
  const checksum = await (0, checksum_1.calculateChecksum)(combinedContent);
55
57
  // Create CKBFS witnesses - each chunk already includes the CKBFS header
@@ -57,9 +59,7 @@ async function createPublishTransaction(signer, options) {
57
59
  // not to be confused with the Protocol Version (V1 vs V2)
58
60
  const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
59
61
  // Calculate the actual witness indices where our content is placed
60
- // Index 0 is reserved for the secp256k1 witness for signing
61
- // So our CKBFS data starts at index 1
62
- const contentStartIndex = 1;
62
+ const contentStartIndex = from?.witnesses.length || 1;
63
63
  const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
64
64
  // Create CKBFS cell output data based on version
65
65
  let outputData;
@@ -93,24 +93,72 @@ async function createPublishTransaction(signer, options) {
93
93
  lock.occupiedSize +
94
94
  8) * 100000000n;
95
95
  // Create pre transaction without cell deps initially
96
- const preTx = core_1.Transaction.from({
97
- outputs: [
98
- createCKBFSCell({
99
- contentType,
100
- filename,
101
- lock,
102
- network,
103
- version,
104
- useTypeID,
105
- capacity: ckbfsCellSize || capacity,
106
- }),
107
- ],
108
- witnesses: [
109
- [], // Empty secp witness for signing
110
- ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
111
- ],
112
- outputsData: [outputData],
113
- });
96
+ let preTx;
97
+ if (from) {
98
+ // If from is not empty, inject/merge the fields
99
+ preTx = core_1.Transaction.from({
100
+ ...from,
101
+ outputs: from.outputs.length === 0
102
+ ? [
103
+ createCKBFSCell({
104
+ contentType,
105
+ filename,
106
+ lock,
107
+ network,
108
+ version,
109
+ useTypeID,
110
+ capacity: ckbfsCellSize || capacity,
111
+ }),
112
+ ]
113
+ : [
114
+ ...from.outputs,
115
+ createCKBFSCell({
116
+ contentType,
117
+ filename,
118
+ lock,
119
+ network,
120
+ version,
121
+ useTypeID,
122
+ capacity: ckbfsCellSize || capacity,
123
+ }),
124
+ ],
125
+ witnesses: from.witnesses.length === 0
126
+ ? [
127
+ [], // Empty secp witness for signing if not provided
128
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
129
+ ]
130
+ : [
131
+ ...from.witnesses,
132
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
133
+ ],
134
+ outputsData: from.outputsData.length === 0
135
+ ? [outputData]
136
+ : [
137
+ ...from.outputsData,
138
+ outputData,
139
+ ],
140
+ });
141
+ }
142
+ else {
143
+ preTx = core_1.Transaction.from({
144
+ outputs: [
145
+ createCKBFSCell({
146
+ contentType,
147
+ filename,
148
+ lock,
149
+ network,
150
+ version,
151
+ useTypeID,
152
+ capacity: ckbfsCellSize || capacity,
153
+ }),
154
+ ],
155
+ witnesses: [
156
+ [], // Empty secp witness for signing
157
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
158
+ ],
159
+ outputsData: [outputData],
160
+ });
161
+ }
114
162
  // Add the CKBFS dep group cell dependency
115
163
  preTx.addCellDeps({
116
164
  outPoint: {
@@ -119,35 +167,261 @@ async function createPublishTransaction(signer, options) {
119
167
  },
120
168
  depType: "depGroup",
121
169
  });
122
- // Get the recommended address to ensure lock script cell deps are included
123
- const address = await signer.getRecommendedAddressObj();
124
- // Complete inputs by capacity
125
- await preTx.completeInputsByCapacity(signer);
126
- // Complete fee change to lock
127
- await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
128
170
  // Create type ID args
129
- const args = core_1.ccc.hashTypeId(preTx.inputs[0], 0x0);
171
+ const outputIndex = from ? from.outputs.length : 0;
172
+ const args = preTx.inputs.length > 0 ? core_1.ccc.hashTypeId(preTx.inputs[0], outputIndex) : "0x0000000000000000000000000000000000000000000000000000000000000000";
130
173
  // Create CKBFS type script with type ID
131
174
  const ckbfsTypeScript = new core_1.Script(ensureHexPrefix(config.codeHash), config.hashType, args);
132
175
  // Create final transaction with same cell deps as preTx
133
176
  const tx = core_1.Transaction.from({
134
177
  cellDeps: preTx.cellDeps,
135
- witnesses: [
136
- [], // Reset first witness for signing
137
- ...preTx.witnesses.slice(1),
138
- ],
178
+ witnesses: preTx.witnesses,
139
179
  outputsData: preTx.outputsData,
140
180
  inputs: preTx.inputs,
141
- outputs: [
142
- {
143
- lock,
144
- type: ckbfsTypeScript,
145
- capacity: preTx.outputs[0].capacity,
146
- },
147
- ...preTx.outputs.slice(1), // Include rest of outputs (e.g., change)
148
- ],
181
+ outputs: outputIndex === 0
182
+ ? [
183
+ {
184
+ lock,
185
+ type: ckbfsTypeScript,
186
+ capacity: preTx.outputs[outputIndex].capacity,
187
+ },
188
+ ]
189
+ : [
190
+ ...preTx.outputs.slice(0, outputIndex), // Include rest of outputs (e.g., change)
191
+ {
192
+ lock,
193
+ type: ckbfsTypeScript,
194
+ capacity: preTx.outputs[outputIndex].capacity,
195
+ },
196
+ ],
149
197
  });
150
- return tx;
198
+ return { tx, outputIndex, emptyTypeID: args === "0x0000000000000000000000000000000000000000000000000000000000000000" };
199
+ }
200
+ /**
201
+ * Creates a transaction for publishing a file to CKBFS
202
+ * @param signer The signer to use for the transaction
203
+ * @param options Options for publishing the file
204
+ * @returns Promise resolving to the created transaction
205
+ */
206
+ async function createPublishTransaction(signer, options) {
207
+ const { feeRate, lock, } = options;
208
+ // Use preparePublishTransaction to create the base transaction
209
+ const { tx: preTx, outputIndex, emptyTypeID } = await preparePublishTransaction(options);
210
+ // Complete inputs by capacity
211
+ await preTx.completeInputsByCapacity(signer);
212
+ // Complete fee change to lock
213
+ await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
214
+ // If emptyTypeID is true, we need to create the proper type ID args
215
+ if (emptyTypeID) {
216
+ // Get CKBFS script config
217
+ const config = (0, constants_1.getCKBFSScriptConfig)(options.network || constants_1.DEFAULT_NETWORK, options.version || constants_1.DEFAULT_VERSION, options.useTypeID || false);
218
+ // Create type ID args
219
+ const args = core_1.ccc.hashTypeId(preTx.inputs[0], outputIndex);
220
+ // Create CKBFS type script with type ID
221
+ const ckbfsTypeScript = new core_1.Script(ensureHexPrefix(config.codeHash), config.hashType, args);
222
+ // Create final transaction with updated type script
223
+ const tx = core_1.Transaction.from({
224
+ cellDeps: preTx.cellDeps,
225
+ witnesses: preTx.witnesses,
226
+ outputsData: preTx.outputsData,
227
+ inputs: preTx.inputs,
228
+ outputs: preTx.outputs.map((output, index) => index === outputIndex
229
+ ? {
230
+ lock,
231
+ type: ckbfsTypeScript,
232
+ capacity: output.capacity,
233
+ }
234
+ : output),
235
+ });
236
+ return tx;
237
+ }
238
+ else {
239
+ // If typeID was already set properly, just reset the first witness for signing
240
+ const tx = core_1.Transaction.from({
241
+ cellDeps: preTx.cellDeps,
242
+ witnesses: preTx.witnesses,
243
+ outputsData: preTx.outputsData,
244
+ inputs: preTx.inputs,
245
+ outputs: preTx.outputs,
246
+ });
247
+ return tx;
248
+ }
249
+ }
250
+ /**
251
+ * Prepares a transaction for appending content to a CKBFS file without fee and change handling
252
+ * @param options Options for appending content
253
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
254
+ */
255
+ async function prepareAppendTransaction(options) {
256
+ const { from, ckbfsCell, contentChunks, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, } = options;
257
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
258
+ // Get CKBFS script config early to use version info
259
+ const config = (0, constants_1.getCKBFSScriptConfig)(network, version);
260
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
261
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
262
+ // not to be confused with the Protocol Version (V1 vs V2)
263
+ const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
264
+ // Combine the new content chunks for checksum calculation
265
+ const combinedContent = Buffer.concat(contentChunks);
266
+ // Update the existing checksum with the new content - this matches Adler32's
267
+ // cumulative nature as required by Rule 11 in the RFC
268
+ const contentChecksum = await (0, checksum_1.updateChecksum)(data.checksum, combinedContent);
269
+ // Calculate the actual witness indices where our content is placed
270
+ const contentStartIndex = from?.witnesses.length || 1;
271
+ const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
272
+ // Create backlink for the current state based on version
273
+ let newBackLink;
274
+ if (version === constants_1.ProtocolVersion.V1) {
275
+ // V1 format: Use index field (single number)
276
+ newBackLink = {
277
+ // In V1, field order is index, checksum, txHash
278
+ // and index is a single number value, not an array
279
+ index: data.index ||
280
+ (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
281
+ checksum: data.checksum,
282
+ txHash: outPoint.txHash,
283
+ };
284
+ }
285
+ else {
286
+ // V2 format: Use indexes field (array of numbers)
287
+ newBackLink = {
288
+ // In V2, field order is indexes, checksum, txHash
289
+ // and indexes is an array of numbers
290
+ indexes: data.indexes || (data.index ? [data.index] : []),
291
+ checksum: data.checksum,
292
+ txHash: outPoint.txHash,
293
+ };
294
+ }
295
+ // Update backlinks - add the new one to the existing backlinks array
296
+ const backLinks = [...(data.backLinks || []), newBackLink];
297
+ // Define output data based on version
298
+ let outputData;
299
+ if (version === constants_1.ProtocolVersion.V1) {
300
+ // In V1, index is a single number, not an array
301
+ // The first witness index is used (V1 can only reference one witness)
302
+ outputData = molecule_1.CKBFSData.pack({
303
+ index: witnessIndices[0], // Use only the first index as a number
304
+ checksum: contentChecksum,
305
+ contentType: data.contentType,
306
+ filename: data.filename,
307
+ backLinks,
308
+ }, constants_1.ProtocolVersion.V1); // Explicitly use V1 for packing
309
+ }
310
+ else {
311
+ // In V2, indexes is an array of witness indices
312
+ outputData = molecule_1.CKBFSData.pack({
313
+ indexes: witnessIndices,
314
+ checksum: contentChecksum,
315
+ contentType: data.contentType,
316
+ filename: data.filename,
317
+ backLinks,
318
+ }, constants_1.ProtocolVersion.V2); // Explicitly use V2 for packing
319
+ }
320
+ // Calculate the required capacity for the output cell
321
+ // This accounts for:
322
+ // 1. The output data size
323
+ // 2. The type script's occupied size
324
+ // 3. The lock script's occupied size
325
+ // 4. A constant of 8 bytes (for header overhead)
326
+ const ckbfsCellSize = BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) *
327
+ 100000000n;
328
+ // Use the maximum value between calculated size and original capacity
329
+ // to ensure we have enough capacity but don't decrease capacity unnecessarily
330
+ const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
331
+ // Create initial transaction with the CKBFS cell input
332
+ let preTx;
333
+ if (from) {
334
+ // If from is not empty, inject/merge the fields
335
+ preTx = core_1.Transaction.from({
336
+ ...from,
337
+ inputs: from.inputs.length === 0
338
+ ? [
339
+ {
340
+ previousOutput: {
341
+ txHash: outPoint.txHash,
342
+ index: outPoint.index,
343
+ },
344
+ since: "0x0",
345
+ },
346
+ ]
347
+ : [
348
+ ...from.inputs,
349
+ {
350
+ previousOutput: {
351
+ txHash: outPoint.txHash,
352
+ index: outPoint.index,
353
+ },
354
+ since: "0x0",
355
+ },
356
+ ],
357
+ outputs: from.outputs.length === 0
358
+ ? [
359
+ {
360
+ lock,
361
+ type,
362
+ capacity: outputCapacity,
363
+ },
364
+ ]
365
+ : [
366
+ ...from.outputs,
367
+ {
368
+ lock,
369
+ type,
370
+ capacity: outputCapacity,
371
+ },
372
+ ],
373
+ outputsData: from.outputsData.length === 0
374
+ ? [outputData]
375
+ : [
376
+ ...from.outputsData,
377
+ outputData,
378
+ ],
379
+ witnesses: from.witnesses.length === 0
380
+ ? [
381
+ [], // Empty secp witness for signing if not provided
382
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
383
+ ]
384
+ : [
385
+ ...from.witnesses,
386
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
387
+ ],
388
+ });
389
+ }
390
+ else {
391
+ preTx = core_1.Transaction.from({
392
+ inputs: [
393
+ {
394
+ previousOutput: {
395
+ txHash: outPoint.txHash,
396
+ index: outPoint.index,
397
+ },
398
+ since: "0x0",
399
+ },
400
+ ],
401
+ outputs: [
402
+ {
403
+ lock,
404
+ type,
405
+ capacity: outputCapacity,
406
+ },
407
+ ],
408
+ outputsData: [outputData],
409
+ witnesses: [
410
+ [], // Empty secp witness for signing
411
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
412
+ ],
413
+ });
414
+ }
415
+ // Add the CKBFS dep group cell dependency
416
+ preTx.addCellDeps({
417
+ outPoint: {
418
+ txHash: ensureHexPrefix(config.depTxHash),
419
+ index: config.depIndex || 0,
420
+ },
421
+ depType: "depGroup",
422
+ });
423
+ const outputIndex = from ? from.outputs.length : 0;
424
+ return { tx: preTx, outputIndex };
151
425
  }
152
426
  /**
153
427
  * Creates a transaction for appending content to a CKBFS file
@@ -156,7 +430,44 @@ async function createPublishTransaction(signer, options) {
156
430
  * @returns Promise resolving to the created transaction
157
431
  */
158
432
  async function createAppendTransaction(signer, options) {
159
- const { ckbfsCell, contentChunks, feeRate, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, } = options;
433
+ const { ckbfsCell, feeRate, } = options;
434
+ const { lock } = ckbfsCell;
435
+ // Use prepareAppendTransaction to create the base transaction
436
+ const { tx: preTx, outputIndex } = await prepareAppendTransaction(options);
437
+ // Get the recommended address to ensure lock script cell deps are included
438
+ const address = await signer.getRecommendedAddressObj();
439
+ const inputsBefore = preTx.inputs.length;
440
+ // If we need more capacity than the original cell had, add additional inputs
441
+ if (preTx.outputs[outputIndex].capacity > ckbfsCell.capacity) {
442
+ console.log(`Need additional capacity: ${preTx.outputs[outputIndex].capacity - ckbfsCell.capacity} shannons`);
443
+ // Add more inputs to cover the increased capacity
444
+ await preTx.completeInputsByCapacity(signer);
445
+ }
446
+ const witnesses = [];
447
+ // add empty witness for signer if ckbfs's lock is the same as signer's lock
448
+ if (address.script.hash() === lock.hash()) {
449
+ witnesses.push("0x");
450
+ }
451
+ // add ckbfs witnesses (skip the first witness which is for signing)
452
+ witnesses.push(...preTx.witnesses.slice(1));
453
+ // Add empty witnesses for additional signer inputs
454
+ // This is to ensure that the transaction is valid and can be signed
455
+ for (let i = inputsBefore; i < preTx.inputs.length; i++) {
456
+ witnesses.push("0x");
457
+ }
458
+ preTx.witnesses = witnesses;
459
+ // Complete fee
460
+ await preTx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
461
+ return preTx;
462
+ }
463
+ /**
464
+ * Creates a transaction for appending content to a CKBFS file
465
+ * @param signer The signer to use for the transaction
466
+ * @param options Options for appending content
467
+ * @returns Promise resolving to the created transaction
468
+ */
469
+ async function createAppendTransactionDry(signer, options) {
470
+ const { ckbfsCell, contentChunks, network = constants_1.DEFAULT_NETWORK, version = constants_1.DEFAULT_VERSION, } = options;
160
471
  const { outPoint, data, type, lock, capacity } = ckbfsCell;
161
472
  // Get CKBFS script config early to use version info
162
473
  const config = (0, constants_1.getCKBFSScriptConfig)(network, version);
@@ -225,11 +536,6 @@ async function createAppendTransaction(signer, options) {
225
536
  backLinks,
226
537
  }, constants_1.ProtocolVersion.V2); // Explicitly use V2 for packing
227
538
  }
228
- // Pack the original data to get its size - use the appropriate version
229
- const originalData = molecule_1.CKBFSData.pack(data, version);
230
- const originalDataSize = originalData.length;
231
- // Get sizes and calculate capacity requirements
232
- const newDataSize = outputData.length;
233
539
  // Calculate the required capacity for the output cell
234
540
  // This accounts for:
235
541
  // 1. The output data size
@@ -271,12 +577,14 @@ async function createAppendTransaction(signer, options) {
271
577
  depType: "depGroup",
272
578
  });
273
579
  const inputsBefore = tx.inputs.length;
274
- // If we need more capacity than the original cell had, add additional inputs
275
- if (outputCapacity > capacity) {
276
- console.log(`Need additional capacity: ${outputCapacity - capacity} shannons`);
277
- // Add more inputs to cover the increased capacity
278
- await tx.completeInputsByCapacity(signer);
279
- }
580
+ // // If we need more capacity than the original cell had, add additional inputs
581
+ // if (outputCapacity > capacity) {
582
+ // console.log(
583
+ // `Need additional capacity: ${outputCapacity - capacity} shannons`,
584
+ // );
585
+ // // Add more inputs to cover the increased capacity
586
+ // await tx.completeInputsByCapacity(signer);
587
+ // }
280
588
  const witnesses = [];
281
589
  // add empty witness for signer if ckbfs's lock is the same as signer's lock
282
590
  if (address.script.hash() === lock.hash()) {
@@ -291,7 +599,7 @@ async function createAppendTransaction(signer, options) {
291
599
  }
292
600
  tx.witnesses = witnesses;
293
601
  // Complete fee
294
- await tx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
602
+ //await tx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
295
603
  return tx;
296
604
  }
297
605
  /**
@@ -6,11 +6,11 @@ const privateKey = process.env.CKB_PRIVATE_KEY || 'your-private-key-here';
6
6
  // Initialize the SDK with network and version options
7
7
  const ckbfs = new CKBFS(
8
8
  privateKey,
9
- NetworkType.Mainnet, // Use testnet
9
+ NetworkType.Testnet, // Use testnet
10
10
  {
11
11
  version: ProtocolVersion.V2, // Use the latest version (V2)
12
12
  chunkSize: 30 * 1024, // 30KB chunks
13
- useTypeID: false // Use code hash instead of type ID
13
+ useTypeID: false, // Use code hash instead of type ID
14
14
  }
15
15
  );
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckbfs/api",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "SDK for CKBFS protocol on CKB",
5
5
  "license": "MIT",
6
6
  "author": "Code Monad<code@lab-11.org>",
package/src/index.ts CHANGED
@@ -15,7 +15,10 @@ import {
15
15
  import {
16
16
  createCKBFSCell,
17
17
  createPublishTransaction as utilCreatePublishTransaction,
18
+ preparePublishTransaction,
18
19
  createAppendTransaction as utilCreateAppendTransaction,
20
+ prepareAppendTransaction,
21
+ createAppendTransactionDry,
19
22
  publishCKBFS as utilPublishCKBFS,
20
23
  appendCKBFS as utilAppendCKBFS,
21
24
  CKBFSCellOptions,
@@ -122,6 +125,7 @@ export interface CKBFSOptions {
122
125
  version?: ProtocolVersionType;
123
126
  useTypeID?: boolean;
124
127
  network?: NetworkType;
128
+ rpcUrl?: string;
125
129
  }
126
130
 
127
131
  /**
@@ -133,6 +137,7 @@ export class CKBFS {
133
137
  private network: NetworkType;
134
138
  private version: ProtocolVersionType;
135
139
  private useTypeID: boolean;
140
+ private rpcUrl: string;
136
141
 
137
142
  /**
138
143
  * Creates a new CKBFS SDK instance
@@ -159,13 +164,18 @@ export class CKBFS {
159
164
 
160
165
  const client =
161
166
  network === "mainnet"
162
- ? new ClientPublicMainnet()
163
- : new ClientPublicTestnet();
167
+ ? new ClientPublicMainnet({
168
+ url: opts.rpcUrl,
169
+ })
170
+ : new ClientPublicTestnet({
171
+ url: opts.rpcUrl,
172
+ });
164
173
  this.signer = new SignerCkbPrivateKey(client, privateKey);
165
174
  this.network = network;
166
175
  this.chunkSize = opts.chunkSize || 30 * 1024;
167
176
  this.version = opts.version || DEFAULT_VERSION;
168
177
  this.useTypeID = opts.useTypeID || false;
178
+ this.rpcUrl = opts.rpcUrl || client.url;
169
179
  } else {
170
180
  // Initialize with signer
171
181
  this.signer = signerOrPrivateKey;
@@ -175,6 +185,7 @@ export class CKBFS {
175
185
  this.chunkSize = opts.chunkSize || 30 * 1024;
176
186
  this.version = opts.version || DEFAULT_VERSION;
177
187
  this.useTypeID = opts.useTypeID || false;
188
+ this.rpcUrl = opts.rpcUrl || this.signer.client.url;
178
189
  }
179
190
  }
180
191
 
@@ -519,10 +530,12 @@ export {
519
530
  // Transaction utilities (Exporting original names from transaction.ts)
520
531
  createCKBFSCell,
521
532
  utilCreatePublishTransaction as createPublishTransaction,
533
+ preparePublishTransaction,
522
534
  utilCreateAppendTransaction as createAppendTransaction,
535
+ prepareAppendTransaction,
523
536
  utilPublishCKBFS as publishCKBFS,
524
537
  utilAppendCKBFS as appendCKBFS,
525
-
538
+ createAppendTransactionDry,
526
539
  // File utilities
527
540
  readFile,
528
541
  readFileAsText,
@@ -34,6 +34,7 @@ export interface CKBFSCellOptions {
34
34
  export interface PublishOptions extends CKBFSCellOptions {
35
35
  contentChunks: Uint8Array[];
36
36
  feeRate?: number;
37
+ from?: Transaction;
37
38
  }
38
39
 
39
40
  /**
@@ -51,6 +52,7 @@ export interface AppendOptions {
51
52
  feeRate?: number;
52
53
  network?: NetworkType;
53
54
  version?: ProtocolVersionType;
55
+ from?: Transaction;
54
56
  }
55
57
 
56
58
  /**
@@ -100,29 +102,27 @@ export function createCKBFSCell(options: CKBFSCellOptions) {
100
102
  }
101
103
 
102
104
  /**
103
- * Creates a transaction for publishing a file to CKBFS
104
- * @param signer The signer to use for the transaction
105
+ * Prepares a transaction for publishing a file to CKBFS without fee and change handling
106
+ * You will need to manually set the typeID if you did not provide inputs, or just check is return value emptyTypeID is true
105
107
  * @param options Options for publishing the file
106
- * @returns Promise resolving to the created transaction
108
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
107
109
  */
108
- export async function createPublishTransaction(
109
- signer: Signer,
110
+ export async function preparePublishTransaction(
110
111
  options: PublishOptions,
111
- ): Promise<Transaction> {
112
+ ): Promise<{tx: Transaction, outputIndex: number, emptyTypeID: boolean}> { // if emptyTypeID is true, you shall manually set the typeID after
112
113
  const {
114
+ from,
113
115
  contentChunks,
114
116
  contentType,
115
117
  filename,
116
118
  lock,
117
119
  capacity,
118
- feeRate,
119
120
  network = DEFAULT_NETWORK,
120
121
  version = DEFAULT_VERSION,
121
122
  useTypeID = false,
122
123
  } = options;
123
124
 
124
125
  // Calculate checksum for the combined content
125
- const textEncoder = new TextEncoder();
126
126
  const combinedContent = Buffer.concat(contentChunks);
127
127
  const checksum = await calculateChecksum(combinedContent);
128
128
 
@@ -132,9 +132,8 @@ export async function createPublishTransaction(
132
132
  const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
133
133
 
134
134
  // Calculate the actual witness indices where our content is placed
135
- // Index 0 is reserved for the secp256k1 witness for signing
136
- // So our CKBFS data starts at index 1
137
- const contentStartIndex = 1;
135
+
136
+ const contentStartIndex = from?.witnesses.length || 1;
138
137
  const witnessIndices = Array.from(
139
138
  { length: contentChunks.length },
140
139
  (_, i) => contentStartIndex + i,
@@ -187,24 +186,71 @@ export async function createPublishTransaction(
187
186
  8,
188
187
  ) * 100000000n;
189
188
  // Create pre transaction without cell deps initially
190
- const preTx = Transaction.from({
191
- outputs: [
192
- createCKBFSCell({
193
- contentType,
194
- filename,
195
- lock,
196
- network,
197
- version,
198
- useTypeID,
199
- capacity: ckbfsCellSize || capacity,
200
- }),
201
- ],
202
- witnesses: [
203
- [], // Empty secp witness for signing
204
- ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
205
- ],
206
- outputsData: [outputData],
207
- });
189
+ let preTx: Transaction;
190
+ if(from) {
191
+ // If from is not empty, inject/merge the fields
192
+ preTx = Transaction.from({
193
+ ...from,
194
+ outputs: from.outputs.length === 0
195
+ ? [
196
+ createCKBFSCell({
197
+ contentType,
198
+ filename,
199
+ lock,
200
+ network,
201
+ version,
202
+ useTypeID,
203
+ capacity: ckbfsCellSize || capacity,
204
+ }),
205
+ ]
206
+ : [
207
+ ...from.outputs,
208
+ createCKBFSCell({
209
+ contentType,
210
+ filename,
211
+ lock,
212
+ network,
213
+ version,
214
+ useTypeID,
215
+ capacity: ckbfsCellSize || capacity,
216
+ }),
217
+ ],
218
+ witnesses: from.witnesses.length === 0
219
+ ? [
220
+ [], // Empty secp witness for signing if not provided
221
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
222
+ ]
223
+ : [
224
+ ...from.witnesses,
225
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
226
+ ],
227
+ outputsData: from.outputsData.length === 0
228
+ ? [outputData]
229
+ : [
230
+ ...from.outputsData,
231
+ outputData,
232
+ ],
233
+ });
234
+ } else {
235
+ preTx = Transaction.from({
236
+ outputs: [
237
+ createCKBFSCell({
238
+ contentType,
239
+ filename,
240
+ lock,
241
+ network,
242
+ version,
243
+ useTypeID,
244
+ capacity: ckbfsCellSize || capacity,
245
+ }),
246
+ ],
247
+ witnesses: [
248
+ [], // Empty secp witness for signing
249
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
250
+ ],
251
+ outputsData: [outputData],
252
+ });
253
+ }
208
254
 
209
255
  // Add the CKBFS dep group cell dependency
210
256
  preTx.addCellDeps({
@@ -215,17 +261,9 @@ export async function createPublishTransaction(
215
261
  depType: "depGroup",
216
262
  });
217
263
 
218
- // Get the recommended address to ensure lock script cell deps are included
219
- const address = await signer.getRecommendedAddressObj();
220
-
221
- // Complete inputs by capacity
222
- await preTx.completeInputsByCapacity(signer);
223
-
224
- // Complete fee change to lock
225
- await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
226
-
227
264
  // Create type ID args
228
- const args = ccc.hashTypeId(preTx.inputs[0], 0x0);
265
+ const outputIndex = from ? from.outputs.length : 0;
266
+ const args = preTx.inputs.length > 0 ? ccc.hashTypeId(preTx.inputs[0], outputIndex) : "0x0000000000000000000000000000000000000000000000000000000000000000";
229
267
 
230
268
  // Create CKBFS type script with type ID
231
269
  const ckbfsTypeScript = new Script(
@@ -237,23 +275,308 @@ export async function createPublishTransaction(
237
275
  // Create final transaction with same cell deps as preTx
238
276
  const tx = Transaction.from({
239
277
  cellDeps: preTx.cellDeps,
240
- witnesses: [
241
- [], // Reset first witness for signing
242
- ...preTx.witnesses.slice(1),
243
- ],
278
+ witnesses: preTx.witnesses,
244
279
  outputsData: preTx.outputsData,
245
280
  inputs: preTx.inputs,
246
- outputs: [
281
+ outputs: outputIndex === 0
282
+ ? [
283
+ {
284
+ lock,
285
+ type: ckbfsTypeScript,
286
+ capacity: preTx.outputs[outputIndex].capacity,
287
+ },
288
+ ]
289
+ : [
290
+ ...preTx.outputs.slice(0, outputIndex), // Include rest of outputs (e.g., change)
291
+ {
292
+ lock,
293
+ type: ckbfsTypeScript,
294
+ capacity: preTx.outputs[outputIndex].capacity,
295
+ },
296
+ ],
297
+ });
298
+
299
+ return {tx, outputIndex, emptyTypeID: args === "0x0000000000000000000000000000000000000000000000000000000000000000"};
300
+ }
301
+
302
+ /**
303
+ * Creates a transaction for publishing a file to CKBFS
304
+ * @param signer The signer to use for the transaction
305
+ * @param options Options for publishing the file
306
+ * @returns Promise resolving to the created transaction
307
+ */
308
+ export async function createPublishTransaction(
309
+ signer: Signer,
310
+ options: PublishOptions,
311
+ ): Promise<Transaction> {
312
+ const {
313
+ feeRate,
314
+ lock,
315
+ } = options;
316
+
317
+ // Use preparePublishTransaction to create the base transaction
318
+ const { tx: preTx, outputIndex, emptyTypeID } = await preparePublishTransaction(options);
319
+
320
+ // Complete inputs by capacity
321
+ await preTx.completeInputsByCapacity(signer);
322
+
323
+ // Complete fee change to lock
324
+ await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
325
+
326
+ // If emptyTypeID is true, we need to create the proper type ID args
327
+ if (emptyTypeID) {
328
+ // Get CKBFS script config
329
+ const config = getCKBFSScriptConfig(options.network || DEFAULT_NETWORK, options.version || DEFAULT_VERSION, options.useTypeID || false);
330
+
331
+ // Create type ID args
332
+ const args = ccc.hashTypeId(preTx.inputs[0], outputIndex);
333
+
334
+ // Create CKBFS type script with type ID
335
+ const ckbfsTypeScript = new Script(
336
+ ensureHexPrefix(config.codeHash),
337
+ config.hashType as any,
338
+ args,
339
+ );
340
+
341
+ // Create final transaction with updated type script
342
+ const tx = Transaction.from({
343
+ cellDeps: preTx.cellDeps,
344
+ witnesses: preTx.witnesses,
345
+ outputsData: preTx.outputsData,
346
+ inputs: preTx.inputs,
347
+ outputs: preTx.outputs.map((output, index) =>
348
+ index === outputIndex
349
+ ? {
350
+ lock,
351
+ type: ckbfsTypeScript,
352
+ capacity: output.capacity,
353
+ }
354
+ : output
355
+ ),
356
+ });
357
+
358
+ return tx;
359
+ } else {
360
+ // If typeID was already set properly, just reset the first witness for signing
361
+ const tx = Transaction.from({
362
+ cellDeps: preTx.cellDeps,
363
+ witnesses: preTx.witnesses,
364
+ outputsData: preTx.outputsData,
365
+ inputs: preTx.inputs,
366
+ outputs: preTx.outputs,
367
+ });
368
+
369
+ return tx;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Prepares a transaction for appending content to a CKBFS file without fee and change handling
375
+ * @param options Options for appending content
376
+ * @returns Promise resolving to the prepared transaction and the output index of CKBFS Cell
377
+ */
378
+ export async function prepareAppendTransaction(
379
+ options: AppendOptions,
380
+ ): Promise<{tx: Transaction, outputIndex: number}> {
381
+ const {
382
+ from,
383
+ ckbfsCell,
384
+ contentChunks,
385
+ network = DEFAULT_NETWORK,
386
+ version = DEFAULT_VERSION,
387
+ } = options;
388
+ const { outPoint, data, type, lock, capacity } = ckbfsCell;
389
+
390
+ // Get CKBFS script config early to use version info
391
+ const config = getCKBFSScriptConfig(network, version);
392
+
393
+ // Create CKBFS witnesses - each chunk already includes the CKBFS header
394
+ // Pass 0 as version byte - this is the protocol version byte in the witness header
395
+ // not to be confused with the Protocol Version (V1 vs V2)
396
+ const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
397
+
398
+ // Combine the new content chunks for checksum calculation
399
+ const combinedContent = Buffer.concat(contentChunks);
400
+
401
+ // Update the existing checksum with the new content - this matches Adler32's
402
+ // cumulative nature as required by Rule 11 in the RFC
403
+ const contentChecksum = await updateChecksum(data.checksum, combinedContent);
404
+
405
+ // Calculate the actual witness indices where our content is placed
406
+ const contentStartIndex = from?.witnesses.length || 1;
407
+ const witnessIndices = Array.from(
408
+ { length: contentChunks.length },
409
+ (_, i) => contentStartIndex + i,
410
+ );
411
+
412
+ // Create backlink for the current state based on version
413
+ let newBackLink: any;
414
+
415
+ if (version === ProtocolVersion.V1) {
416
+ // V1 format: Use index field (single number)
417
+ newBackLink = {
418
+ // In V1, field order is index, checksum, txHash
419
+ // and index is a single number value, not an array
420
+ index:
421
+ data.index ||
422
+ (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
423
+ checksum: data.checksum,
424
+ txHash: outPoint.txHash,
425
+ };
426
+ } else {
427
+ // V2 format: Use indexes field (array of numbers)
428
+ newBackLink = {
429
+ // In V2, field order is indexes, checksum, txHash
430
+ // and indexes is an array of numbers
431
+ indexes: data.indexes || (data.index ? [data.index] : []),
432
+ checksum: data.checksum,
433
+ txHash: outPoint.txHash,
434
+ };
435
+ }
436
+
437
+ // Update backlinks - add the new one to the existing backlinks array
438
+ const backLinks = [...(data.backLinks || []), newBackLink];
439
+
440
+ // Define output data based on version
441
+ let outputData: Uint8Array;
442
+
443
+ if (version === ProtocolVersion.V1) {
444
+ // In V1, index is a single number, not an array
445
+ // The first witness index is used (V1 can only reference one witness)
446
+ outputData = CKBFSData.pack(
247
447
  {
248
- lock,
249
- type: ckbfsTypeScript,
250
- capacity: preTx.outputs[0].capacity,
448
+ index: witnessIndices[0], // Use only the first index as a number
449
+ checksum: contentChecksum,
450
+ contentType: data.contentType,
451
+ filename: data.filename,
452
+ backLinks,
251
453
  },
252
- ...preTx.outputs.slice(1), // Include rest of outputs (e.g., change)
253
- ],
454
+ ProtocolVersion.V1,
455
+ ); // Explicitly use V1 for packing
456
+ } else {
457
+ // In V2, indexes is an array of witness indices
458
+ outputData = CKBFSData.pack(
459
+ {
460
+ indexes: witnessIndices,
461
+ checksum: contentChecksum,
462
+ contentType: data.contentType,
463
+ filename: data.filename,
464
+ backLinks,
465
+ },
466
+ ProtocolVersion.V2,
467
+ ); // Explicitly use V2 for packing
468
+ }
469
+
470
+ // Calculate the required capacity for the output cell
471
+ // This accounts for:
472
+ // 1. The output data size
473
+ // 2. The type script's occupied size
474
+ // 3. The lock script's occupied size
475
+ // 4. A constant of 8 bytes (for header overhead)
476
+ const ckbfsCellSize =
477
+ BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) *
478
+ 100000000n;
479
+
480
+ // Use the maximum value between calculated size and original capacity
481
+ // to ensure we have enough capacity but don't decrease capacity unnecessarily
482
+ const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
483
+
484
+ // Create initial transaction with the CKBFS cell input
485
+ let preTx: Transaction;
486
+ if (from) {
487
+ // If from is not empty, inject/merge the fields
488
+ preTx = Transaction.from({
489
+ ...from,
490
+ inputs: from.inputs.length === 0
491
+ ? [
492
+ {
493
+ previousOutput: {
494
+ txHash: outPoint.txHash,
495
+ index: outPoint.index,
496
+ },
497
+ since: "0x0",
498
+ },
499
+ ]
500
+ : [
501
+ ...from.inputs,
502
+ {
503
+ previousOutput: {
504
+ txHash: outPoint.txHash,
505
+ index: outPoint.index,
506
+ },
507
+ since: "0x0",
508
+ },
509
+ ],
510
+ outputs: from.outputs.length === 0
511
+ ? [
512
+ {
513
+ lock,
514
+ type,
515
+ capacity: outputCapacity,
516
+ },
517
+ ]
518
+ : [
519
+ ...from.outputs,
520
+ {
521
+ lock,
522
+ type,
523
+ capacity: outputCapacity,
524
+ },
525
+ ],
526
+ outputsData: from.outputsData.length === 0
527
+ ? [outputData]
528
+ : [
529
+ ...from.outputsData,
530
+ outputData,
531
+ ],
532
+ witnesses: from.witnesses.length === 0
533
+ ? [
534
+ [], // Empty secp witness for signing if not provided
535
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
536
+ ]
537
+ : [
538
+ ...from.witnesses,
539
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
540
+ ],
541
+ });
542
+ } else {
543
+ preTx = Transaction.from({
544
+ inputs: [
545
+ {
546
+ previousOutput: {
547
+ txHash: outPoint.txHash,
548
+ index: outPoint.index,
549
+ },
550
+ since: "0x0",
551
+ },
552
+ ],
553
+ outputs: [
554
+ {
555
+ lock,
556
+ type,
557
+ capacity: outputCapacity,
558
+ },
559
+ ],
560
+ outputsData: [outputData],
561
+ witnesses: [
562
+ [], // Empty secp witness for signing
563
+ ...ckbfsWitnesses.map((w) => `0x${Buffer.from(w).toString("hex")}`),
564
+ ],
565
+ });
566
+ }
567
+
568
+ // Add the CKBFS dep group cell dependency
569
+ preTx.addCellDeps({
570
+ outPoint: {
571
+ txHash: ensureHexPrefix(config.depTxHash),
572
+ index: config.depIndex || 0,
573
+ },
574
+ depType: "depGroup",
254
575
  });
255
576
 
256
- return tx;
577
+ const outputIndex = from ? from.outputs.length : 0;
578
+
579
+ return {tx: preTx, outputIndex};
257
580
  }
258
581
 
259
582
  /**
@@ -268,8 +591,60 @@ export async function createAppendTransaction(
268
591
  ): Promise<Transaction> {
269
592
  const {
270
593
  ckbfsCell,
271
- contentChunks,
272
594
  feeRate,
595
+ } = options;
596
+ const { lock } = ckbfsCell;
597
+
598
+ // Use prepareAppendTransaction to create the base transaction
599
+ const { tx: preTx, outputIndex } = await prepareAppendTransaction(options);
600
+
601
+ // Get the recommended address to ensure lock script cell deps are included
602
+ const address = await signer.getRecommendedAddressObj();
603
+
604
+ const inputsBefore = preTx.inputs.length;
605
+ // If we need more capacity than the original cell had, add additional inputs
606
+ if (preTx.outputs[outputIndex].capacity > ckbfsCell.capacity) {
607
+ console.log(
608
+ `Need additional capacity: ${preTx.outputs[outputIndex].capacity - ckbfsCell.capacity} shannons`,
609
+ );
610
+ // Add more inputs to cover the increased capacity
611
+ await preTx.completeInputsByCapacity(signer);
612
+ }
613
+
614
+ const witnesses: any = [];
615
+ // add empty witness for signer if ckbfs's lock is the same as signer's lock
616
+ if (address.script.hash() === lock.hash()) {
617
+ witnesses.push("0x");
618
+ }
619
+ // add ckbfs witnesses (skip the first witness which is for signing)
620
+ witnesses.push(...preTx.witnesses.slice(1));
621
+
622
+ // Add empty witnesses for additional signer inputs
623
+ // This is to ensure that the transaction is valid and can be signed
624
+ for (let i = inputsBefore; i < preTx.inputs.length; i++) {
625
+ witnesses.push("0x");
626
+ }
627
+ preTx.witnesses = witnesses;
628
+
629
+ // Complete fee
630
+ await preTx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
631
+
632
+ return preTx;
633
+ }
634
+
635
+ /**
636
+ * Creates a transaction for appending content to a CKBFS file
637
+ * @param signer The signer to use for the transaction
638
+ * @param options Options for appending content
639
+ * @returns Promise resolving to the created transaction
640
+ */
641
+ export async function createAppendTransactionDry(
642
+ signer: Signer,
643
+ options: AppendOptions,
644
+ ): Promise<Transaction> {
645
+ const {
646
+ ckbfsCell,
647
+ contentChunks,
273
648
  network = DEFAULT_NETWORK,
274
649
  version = DEFAULT_VERSION,
275
650
  } = options;
@@ -363,12 +738,6 @@ export async function createAppendTransaction(
363
738
  ); // Explicitly use V2 for packing
364
739
  }
365
740
 
366
- // Pack the original data to get its size - use the appropriate version
367
- const originalData = CKBFSData.pack(data, version);
368
- const originalDataSize = originalData.length;
369
-
370
- // Get sizes and calculate capacity requirements
371
- const newDataSize = outputData.length;
372
741
 
373
742
  // Calculate the required capacity for the output cell
374
743
  // This accounts for:
@@ -419,14 +788,14 @@ export async function createAppendTransaction(
419
788
  });
420
789
 
421
790
  const inputsBefore = tx.inputs.length;
422
- // If we need more capacity than the original cell had, add additional inputs
423
- if (outputCapacity > capacity) {
424
- console.log(
425
- `Need additional capacity: ${outputCapacity - capacity} shannons`,
426
- );
427
- // Add more inputs to cover the increased capacity
428
- await tx.completeInputsByCapacity(signer);
429
- }
791
+ // // If we need more capacity than the original cell had, add additional inputs
792
+ // if (outputCapacity > capacity) {
793
+ // console.log(
794
+ // `Need additional capacity: ${outputCapacity - capacity} shannons`,
795
+ // );
796
+ // // Add more inputs to cover the increased capacity
797
+ // await tx.completeInputsByCapacity(signer);
798
+ // }
430
799
 
431
800
  const witnesses: any = [];
432
801
  // add empty witness for signer if ckbfs's lock is the same as signer's lock
@@ -446,7 +815,7 @@ export async function createAppendTransaction(
446
815
  tx.witnesses = witnesses;
447
816
 
448
817
  // Complete fee
449
- await tx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
818
+ //await tx.completeFeeChangeToLock(signer, address.script, feeRate || 2000);
450
819
 
451
820
  return tx;
452
821
  }