@ckbfs/api 1.1.0 → 1.2.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 +6 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/utils/checksum.d.ts +1 -1
- package/dist/utils/checksum.js +27 -10
- package/dist/utils/file.d.ts +19 -0
- package/dist/utils/file.js +125 -0
- package/dist/utils/molecule.d.ts +13 -13
- package/dist/utils/molecule.js +61 -93
- package/dist/utils/transaction.js +53 -81
- package/dist/utils/witness.d.ts +3 -4
- package/dist/utils/witness.js +7 -7
- package/examples/retrieve.ts +115 -0
- package/package.json +3 -2
- package/src/index.ts +5 -1
- package/src/utils/checksum.ts +9 -4
- package/src/utils/file.ts +143 -0
- package/test-download.txt +2 -0
package/README.md
CHANGED
@@ -108,12 +108,18 @@ npm run example:publish
|
|
108
108
|
npm run example:append -- --txhash=0x123456...
|
109
109
|
# OR
|
110
110
|
PUBLISH_TX_HASH=0x123456... npm run example:append
|
111
|
+
|
112
|
+
# Run the retrieve file example (download a file from blockchain)
|
113
|
+
npm run example:retrieve -- --txhash=0x123456... --output=./downloaded-file.txt
|
114
|
+
# OR
|
115
|
+
CKBFS_TX_HASH=0x123456... npm run example:retrieve
|
111
116
|
```
|
112
117
|
|
113
118
|
### Example Files
|
114
119
|
|
115
120
|
- `examples/publish.ts` - Shows how to publish a file to CKBFS
|
116
121
|
- `examples/append.ts` - Shows how to append to a previously published file
|
122
|
+
- `examples/retrieve.ts` - Shows how to retrieve a complete file from the blockchain
|
117
123
|
|
118
124
|
To run the examples, first set your CKB private key:
|
119
125
|
|
package/dist/index.d.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Script, Signer, Transaction } from "@ckb-ccc/core";
|
2
2
|
import { calculateChecksum, verifyChecksum, updateChecksum, verifyWitnessChecksum } from './utils/checksum';
|
3
3
|
import { createCKBFSCell, createPublishTransaction, createAppendTransaction, publishCKBFS, appendCKBFS, CKBFSCellOptions, PublishOptions, AppendOptions } from './utils/transaction';
|
4
|
-
import { readFile, readFileAsText, readFileAsUint8Array, writeFile, getContentType, splitFileIntoChunks, combineChunksToFile } from './utils/file';
|
4
|
+
import { readFile, readFileAsText, readFileAsUint8Array, writeFile, getContentType, splitFileIntoChunks, combineChunksToFile, getFileContentFromChain, saveFileFromChain } 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';
|
7
7
|
import { NetworkType, ProtocolVersion, 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 } from './utils/constants';
|
@@ -88,4 +88,4 @@ export declare class CKBFS {
|
|
88
88
|
*/
|
89
89
|
createAppendTransaction(filePath: string, ckbfsCell: AppendOptions['ckbfsCell'], options?: Omit<FileOptions, 'contentType' | 'filename'>): Promise<Transaction>;
|
90
90
|
}
|
91
|
-
export { calculateChecksum, verifyChecksum, updateChecksum, verifyWitnessChecksum, createCKBFSCell, createPublishTransaction, createAppendTransaction, publishCKBFS, appendCKBFS, readFile, readFileAsText, readFileAsUint8Array, writeFile, getContentType, splitFileIntoChunks, combineChunksToFile, createCKBFSWitness, createTextCKBFSWitness, extractCKBFSWitnessContent, isCKBFSWitness, createChunkedCKBFSWitnesses, CKBFSData, BackLinkV1, BackLinkV2, CKBFSDataType, BackLinkType, CKBFSCellOptions, PublishOptions, AppendOptions, CKBFS_HEADER, CKBFS_HEADER_STRING, NetworkType, ProtocolVersion, 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 };
|
91
|
+
export { calculateChecksum, verifyChecksum, updateChecksum, verifyWitnessChecksum, createCKBFSCell, createPublishTransaction, createAppendTransaction, publishCKBFS, appendCKBFS, readFile, readFileAsText, readFileAsUint8Array, writeFile, getContentType, splitFileIntoChunks, combineChunksToFile, getFileContentFromChain, saveFileFromChain, createCKBFSWitness, createTextCKBFSWitness, extractCKBFSWitnessContent, isCKBFSWitness, createChunkedCKBFSWitnesses, CKBFSData, BackLinkV1, BackLinkV2, CKBFSDataType, BackLinkType, CKBFSCellOptions, PublishOptions, AppendOptions, CKBFS_HEADER, CKBFS_HEADER_STRING, NetworkType, ProtocolVersion, 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,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.getCKBFSScriptConfig = exports.DEPLOY_TX_HASH = exports.DEP_GROUP_TX_HASH = 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.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;
|
3
|
+
exports.getCKBFSScriptConfig = exports.DEPLOY_TX_HASH = exports.DEP_GROUP_TX_HASH = 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.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
4
|
const core_1 = require("@ckb-ccc/core");
|
5
5
|
const checksum_1 = require("./utils/checksum");
|
6
6
|
Object.defineProperty(exports, "calculateChecksum", { enumerable: true, get: function () { return checksum_1.calculateChecksum; } });
|
@@ -21,6 +21,8 @@ Object.defineProperty(exports, "writeFile", { enumerable: true, get: function ()
|
|
21
21
|
Object.defineProperty(exports, "getContentType", { enumerable: true, get: function () { return file_1.getContentType; } });
|
22
22
|
Object.defineProperty(exports, "splitFileIntoChunks", { enumerable: true, get: function () { return file_1.splitFileIntoChunks; } });
|
23
23
|
Object.defineProperty(exports, "combineChunksToFile", { enumerable: true, get: function () { return file_1.combineChunksToFile; } });
|
24
|
+
Object.defineProperty(exports, "getFileContentFromChain", { enumerable: true, get: function () { return file_1.getFileContentFromChain; } });
|
25
|
+
Object.defineProperty(exports, "saveFileFromChain", { enumerable: true, get: function () { return file_1.saveFileFromChain; } });
|
24
26
|
const witness_1 = require("./utils/witness");
|
25
27
|
Object.defineProperty(exports, "createCKBFSWitness", { enumerable: true, get: function () { return witness_1.createCKBFSWitness; } });
|
26
28
|
Object.defineProperty(exports, "createTextCKBFSWitness", { enumerable: true, get: function () { return witness_1.createTextCKBFSWitness; } });
|
package/dist/utils/checksum.d.ts
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
*/
|
9
9
|
export declare function calculateChecksum(data: Uint8Array): Promise<number>;
|
10
10
|
/**
|
11
|
-
* Updates an existing checksum with new data
|
11
|
+
* Updates an existing checksum with new data using proper rolling Adler-32 calculation
|
12
12
|
* @param previousChecksum The existing checksum to update
|
13
13
|
* @param newData The new data to add to the checksum
|
14
14
|
* @returns Promise resolving to the updated checksum as a number
|
package/dist/utils/checksum.js
CHANGED
@@ -19,21 +19,38 @@ async function calculateChecksum(data) {
|
|
19
19
|
return checksumBuffer.readUInt32BE();
|
20
20
|
}
|
21
21
|
/**
|
22
|
-
* Updates an existing checksum with new data
|
22
|
+
* Updates an existing checksum with new data using proper rolling Adler-32 calculation
|
23
23
|
* @param previousChecksum The existing checksum to update
|
24
24
|
* @param newData The new data to add to the checksum
|
25
25
|
* @returns Promise resolving to the updated checksum as a number
|
26
26
|
*/
|
27
27
|
async function updateChecksum(previousChecksum, newData) {
|
28
|
-
//
|
29
|
-
//
|
30
|
-
//
|
31
|
-
const
|
32
|
-
|
33
|
-
//
|
34
|
-
|
35
|
-
//
|
36
|
-
|
28
|
+
// Extract a and b values from the previous checksum
|
29
|
+
// In Adler-32, the checksum is composed of two 16-bit integers: a and b
|
30
|
+
// The final checksum is (b << 16) | a
|
31
|
+
const a = previousChecksum & 0xFFFF;
|
32
|
+
const b = (previousChecksum >>> 16) & 0xFFFF;
|
33
|
+
// Use the adler-32 package to calculate a proper rolling checksum
|
34
|
+
// The package doesn't have a "resume" function, so we need to work with the underlying algorithm
|
35
|
+
// Initialize with existing a and b values
|
36
|
+
let adlerA = a;
|
37
|
+
let adlerB = b;
|
38
|
+
const MOD_ADLER = 65521; // Adler-32 modulo value
|
39
|
+
// Process each byte of the new data
|
40
|
+
for (let i = 0; i < newData.length; i++) {
|
41
|
+
adlerA = (adlerA + newData[i]) % MOD_ADLER;
|
42
|
+
adlerB = (adlerB + adlerA) % MOD_ADLER;
|
43
|
+
}
|
44
|
+
// Combine a and b to get the final checksum
|
45
|
+
// Use a Uint32Array to ensure we get a proper unsigned 32-bit integer
|
46
|
+
const buffer = new ArrayBuffer(4);
|
47
|
+
const view = new DataView(buffer);
|
48
|
+
view.setUint16(0, adlerA, true); // Set lower 16 bits (little endian)
|
49
|
+
view.setUint16(2, adlerB, true); // Set upper 16 bits (little endian)
|
50
|
+
// Read as an unsigned 32-bit integer
|
51
|
+
const updatedChecksum = view.getUint32(0, true);
|
52
|
+
console.log(`Updated checksum from ${previousChecksum} to ${updatedChecksum} for appended content`);
|
53
|
+
return updatedChecksum;
|
37
54
|
}
|
38
55
|
/**
|
39
56
|
* Verifies if a given checksum matches the expected checksum for the data
|
package/dist/utils/file.d.ts
CHANGED
@@ -44,3 +44,22 @@ export declare function splitFileIntoChunks(filePath: string, chunkSize: number)
|
|
44
44
|
* @param outputPath The path to write the combined file to
|
45
45
|
*/
|
46
46
|
export declare function combineChunksToFile(chunks: Uint8Array[], outputPath: string): void;
|
47
|
+
/**
|
48
|
+
* Retrieves complete file content from the blockchain by following backlinks
|
49
|
+
* @param client The CKB client to use for blockchain queries
|
50
|
+
* @param outPoint The output point of the latest CKBFS cell
|
51
|
+
* @param ckbfsData The data from the latest CKBFS cell
|
52
|
+
* @returns Promise resolving to the complete file content
|
53
|
+
*/
|
54
|
+
export declare function getFileContentFromChain(client: any, outPoint: {
|
55
|
+
txHash: string;
|
56
|
+
index: number;
|
57
|
+
}, ckbfsData: any): Promise<Uint8Array>;
|
58
|
+
/**
|
59
|
+
* Saves file content retrieved from blockchain to disk
|
60
|
+
* @param content The file content to save
|
61
|
+
* @param ckbfsData The CKBFS cell data containing file metadata
|
62
|
+
* @param outputPath Optional path to save the file (defaults to filename in current directory)
|
63
|
+
* @returns The path where the file was saved
|
64
|
+
*/
|
65
|
+
export declare function saveFileFromChain(content: Uint8Array, ckbfsData: any, outputPath?: string): string;
|
package/dist/utils/file.js
CHANGED
@@ -10,6 +10,8 @@ exports.writeFile = writeFile;
|
|
10
10
|
exports.getContentType = getContentType;
|
11
11
|
exports.splitFileIntoChunks = splitFileIntoChunks;
|
12
12
|
exports.combineChunksToFile = combineChunksToFile;
|
13
|
+
exports.getFileContentFromChain = getFileContentFromChain;
|
14
|
+
exports.saveFileFromChain = saveFileFromChain;
|
13
15
|
const fs_1 = __importDefault(require("fs"));
|
14
16
|
const path_1 = __importDefault(require("path"));
|
15
17
|
/**
|
@@ -106,3 +108,126 @@ function combineChunksToFile(chunks, outputPath) {
|
|
106
108
|
const combinedBuffer = Buffer.concat(chunks.map(chunk => Buffer.from(chunk)));
|
107
109
|
writeFile(outputPath, combinedBuffer);
|
108
110
|
}
|
111
|
+
/**
|
112
|
+
* Utility function to safely decode buffer to string
|
113
|
+
* @param buffer The buffer to decode
|
114
|
+
* @returns Decoded string or placeholder on error
|
115
|
+
*/
|
116
|
+
function safelyDecode(buffer) {
|
117
|
+
if (!buffer)
|
118
|
+
return '[Unknown]';
|
119
|
+
try {
|
120
|
+
if (buffer instanceof Uint8Array) {
|
121
|
+
return new TextDecoder().decode(buffer);
|
122
|
+
}
|
123
|
+
else if (typeof buffer === 'string') {
|
124
|
+
return buffer;
|
125
|
+
}
|
126
|
+
else {
|
127
|
+
return `[Buffer: ${buffer.toString()}]`;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
catch (e) {
|
131
|
+
return '[Decode Error]';
|
132
|
+
}
|
133
|
+
}
|
134
|
+
/**
|
135
|
+
* Retrieves complete file content from the blockchain by following backlinks
|
136
|
+
* @param client The CKB client to use for blockchain queries
|
137
|
+
* @param outPoint The output point of the latest CKBFS cell
|
138
|
+
* @param ckbfsData The data from the latest CKBFS cell
|
139
|
+
* @returns Promise resolving to the complete file content
|
140
|
+
*/
|
141
|
+
async function getFileContentFromChain(client, outPoint, ckbfsData) {
|
142
|
+
console.log(`Retrieving file: ${safelyDecode(ckbfsData.filename)}`);
|
143
|
+
console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
|
144
|
+
// Prepare to collect all content pieces
|
145
|
+
const contentPieces = [];
|
146
|
+
let currentData = ckbfsData;
|
147
|
+
let currentOutPoint = outPoint;
|
148
|
+
// Process the current transaction first
|
149
|
+
const tx = await client.getTransaction(currentOutPoint.txHash);
|
150
|
+
if (!tx || !tx.transaction) {
|
151
|
+
throw new Error(`Transaction ${currentOutPoint.txHash} not found`);
|
152
|
+
}
|
153
|
+
// Get content from witnesses
|
154
|
+
const indexes = currentData.indexes || (currentData.index !== undefined ? [currentData.index] : []);
|
155
|
+
if (indexes.length > 0) {
|
156
|
+
// Get content from each witness index
|
157
|
+
for (const idx of indexes) {
|
158
|
+
if (idx >= tx.transaction.witnesses.length) {
|
159
|
+
console.warn(`Witness index ${idx} out of range`);
|
160
|
+
continue;
|
161
|
+
}
|
162
|
+
const witnessHex = tx.transaction.witnesses[idx];
|
163
|
+
const witness = Buffer.from(witnessHex.slice(2), 'hex'); // Remove 0x prefix
|
164
|
+
// Extract content (skip CKBFS header + version byte)
|
165
|
+
if (witness.length >= 6 && witness.slice(0, 5).toString() === 'CKBFS') {
|
166
|
+
const content = witness.slice(6);
|
167
|
+
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
168
|
+
}
|
169
|
+
else {
|
170
|
+
console.warn(`Witness at index ${idx} is not a valid CKBFS witness`);
|
171
|
+
}
|
172
|
+
}
|
173
|
+
}
|
174
|
+
// Follow backlinks recursively
|
175
|
+
if (currentData.backLinks && currentData.backLinks.length > 0) {
|
176
|
+
// Process each backlink, from most recent to oldest
|
177
|
+
for (let i = currentData.backLinks.length - 1; i >= 0; i--) {
|
178
|
+
const backlink = currentData.backLinks[i];
|
179
|
+
// Get the transaction for this backlink
|
180
|
+
const backTx = await client.getTransaction(backlink.txHash);
|
181
|
+
if (!backTx || !backTx.transaction) {
|
182
|
+
console.warn(`Backlink transaction ${backlink.txHash} not found`);
|
183
|
+
continue;
|
184
|
+
}
|
185
|
+
// Get content from backlink witnesses
|
186
|
+
const backIndexes = backlink.indexes || (backlink.index !== undefined ? [backlink.index] : []);
|
187
|
+
if (backIndexes.length > 0) {
|
188
|
+
// Get content from each witness index
|
189
|
+
for (const idx of backIndexes) {
|
190
|
+
if (idx >= backTx.transaction.witnesses.length) {
|
191
|
+
console.warn(`Backlink witness index ${idx} out of range`);
|
192
|
+
continue;
|
193
|
+
}
|
194
|
+
const witnessHex = backTx.transaction.witnesses[idx];
|
195
|
+
const witness = Buffer.from(witnessHex.slice(2), 'hex'); // Remove 0x prefix
|
196
|
+
// Extract content (skip CKBFS header + version byte)
|
197
|
+
if (witness.length >= 6 && witness.slice(0, 5).toString() === 'CKBFS') {
|
198
|
+
const content = witness.slice(6);
|
199
|
+
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
200
|
+
}
|
201
|
+
else {
|
202
|
+
console.warn(`Backlink witness at index ${idx} is not a valid CKBFS witness`);
|
203
|
+
}
|
204
|
+
}
|
205
|
+
}
|
206
|
+
}
|
207
|
+
}
|
208
|
+
// Combine all content pieces
|
209
|
+
return Buffer.concat(contentPieces);
|
210
|
+
}
|
211
|
+
/**
|
212
|
+
* Saves file content retrieved from blockchain to disk
|
213
|
+
* @param content The file content to save
|
214
|
+
* @param ckbfsData The CKBFS cell data containing file metadata
|
215
|
+
* @param outputPath Optional path to save the file (defaults to filename in current directory)
|
216
|
+
* @returns The path where the file was saved
|
217
|
+
*/
|
218
|
+
function saveFileFromChain(content, ckbfsData, outputPath) {
|
219
|
+
// Get filename from CKBFS data
|
220
|
+
const filename = safelyDecode(ckbfsData.filename);
|
221
|
+
// Determine output path
|
222
|
+
const filePath = outputPath || filename;
|
223
|
+
// Ensure directory exists
|
224
|
+
const directory = path_1.default.dirname(filePath);
|
225
|
+
if (!fs_1.default.existsSync(directory)) {
|
226
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
227
|
+
}
|
228
|
+
// Write file
|
229
|
+
fs_1.default.writeFileSync(filePath, content);
|
230
|
+
console.log(`File saved to: ${filePath}`);
|
231
|
+
console.log(`Size: ${content.length} bytes`);
|
232
|
+
return filePath;
|
233
|
+
}
|
package/dist/utils/molecule.d.ts
CHANGED
@@ -4,34 +4,34 @@ import { molecule, number } from "@ckb-lumos/codec";
|
|
4
4
|
*/
|
5
5
|
export declare const Indexes: molecule.ArrayLayoutCodec<import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>>;
|
6
6
|
export declare const BackLinkV1: molecule.ObjectLayoutCodec<{
|
7
|
-
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
8
7
|
index: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
9
8
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
9
|
+
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
10
10
|
}>;
|
11
11
|
export declare const BackLinkV2: molecule.ObjectLayoutCodec<{
|
12
|
-
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
13
12
|
indexes: molecule.ArrayLayoutCodec<import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>>;
|
14
13
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
14
|
+
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
15
15
|
}>;
|
16
16
|
export declare const BackLinksV1: molecule.ArrayLayoutCodec<molecule.ObjectLayoutCodec<{
|
17
|
-
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
18
17
|
index: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
19
18
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
19
|
+
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
20
20
|
}>>;
|
21
21
|
export declare const BackLinksV2: molecule.ArrayLayoutCodec<molecule.ObjectLayoutCodec<{
|
22
|
-
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
23
22
|
indexes: molecule.ArrayLayoutCodec<import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>>;
|
24
23
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
24
|
+
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
25
25
|
}>>;
|
26
26
|
export declare const CKBFSDataV1: molecule.ObjectLayoutCodec<{
|
27
|
-
index:
|
27
|
+
index: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
28
28
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
29
29
|
contentType: import("@ckb-lumos/codec/lib/base").BytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
30
30
|
filename: import("@ckb-lumos/codec/lib/base").BytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
31
31
|
backLinks: molecule.ArrayLayoutCodec<molecule.ObjectLayoutCodec<{
|
32
|
-
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
33
32
|
index: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
34
33
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
34
|
+
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
35
35
|
}>>;
|
36
36
|
}>;
|
37
37
|
export declare const CKBFSDataV2: molecule.ObjectLayoutCodec<{
|
@@ -40,33 +40,33 @@ export declare const CKBFSDataV2: molecule.ObjectLayoutCodec<{
|
|
40
40
|
contentType: import("@ckb-lumos/codec/lib/base").BytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
41
41
|
filename: import("@ckb-lumos/codec/lib/base").BytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
42
42
|
backLinks: molecule.ArrayLayoutCodec<molecule.ObjectLayoutCodec<{
|
43
|
-
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
44
43
|
indexes: molecule.ArrayLayoutCodec<import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>>;
|
45
44
|
checksum: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<number, number.BIish>;
|
45
|
+
txHash: import("@ckb-lumos/codec/lib/base").FixedBytesCodec<string, import("@ckb-lumos/codec").BytesLike>;
|
46
46
|
}>>;
|
47
47
|
}>;
|
48
48
|
export type BackLinkTypeV1 = {
|
49
|
-
txHash: string;
|
50
49
|
index: number;
|
51
50
|
checksum: number;
|
51
|
+
txHash: string;
|
52
52
|
};
|
53
53
|
export type BackLinkTypeV2 = {
|
54
|
-
txHash: string;
|
55
54
|
indexes: number[];
|
56
55
|
checksum: number;
|
56
|
+
txHash: string;
|
57
57
|
};
|
58
58
|
export type BackLinkType = {
|
59
|
-
txHash: string;
|
60
59
|
index?: number;
|
61
60
|
indexes?: number[];
|
62
61
|
checksum: number;
|
62
|
+
txHash: string;
|
63
63
|
};
|
64
64
|
export type CKBFSDataType = {
|
65
|
-
index?: number
|
65
|
+
index?: number;
|
66
66
|
indexes?: number[];
|
67
67
|
checksum: number;
|
68
|
-
contentType:
|
69
|
-
filename:
|
68
|
+
contentType: string;
|
69
|
+
filename: string;
|
70
70
|
backLinks: BackLinkType[];
|
71
71
|
};
|
72
72
|
export declare const CKBFSData: {
|
package/dist/utils/molecule.js
CHANGED
@@ -4,36 +4,36 @@ exports.CKBFS_HEADER_STRING = exports.CKBFS_HEADER = exports.CKBFSData = exports
|
|
4
4
|
const codec_1 = require("@ckb-lumos/codec");
|
5
5
|
const base_1 = require("@ckb-lumos/base");
|
6
6
|
const constants_1 = require("./constants");
|
7
|
+
const core_1 = require("@ckb-ccc/core");
|
7
8
|
/**
|
8
9
|
* Molecule definitions for CKBFS data structures.
|
9
10
|
*/
|
10
|
-
// Define the Indexes vector
|
11
|
+
// Define the Indexes vector for V2
|
11
12
|
exports.Indexes = codec_1.molecule.vector(codec_1.number.Uint32);
|
12
|
-
//
|
13
|
+
// V1: BackLink has index as Uint32, and fields are ordered differently
|
13
14
|
exports.BackLinkV1 = codec_1.molecule.table({
|
14
|
-
txHash: base_1.blockchain.Byte32,
|
15
15
|
index: codec_1.number.Uint32,
|
16
16
|
checksum: codec_1.number.Uint32,
|
17
|
-
}, ["txHash", "index", "checksum"]);
|
18
|
-
// Define the BackLink table structure for V2
|
19
|
-
exports.BackLinkV2 = codec_1.molecule.table({
|
20
17
|
txHash: base_1.blockchain.Byte32,
|
18
|
+
}, ["index", "checksum", "txHash"]);
|
19
|
+
// V2: BackLink has indexes as vector of Uint32
|
20
|
+
exports.BackLinkV2 = codec_1.molecule.table({
|
21
21
|
indexes: exports.Indexes,
|
22
22
|
checksum: codec_1.number.Uint32,
|
23
|
-
|
24
|
-
|
23
|
+
txHash: base_1.blockchain.Byte32,
|
24
|
+
}, ["indexes", "checksum", "txHash"]);
|
25
|
+
// Define the BackLinks vector for V1 and V2
|
25
26
|
exports.BackLinksV1 = codec_1.molecule.vector(exports.BackLinkV1);
|
26
|
-
// Define the BackLinks vector for V2
|
27
27
|
exports.BackLinksV2 = codec_1.molecule.vector(exports.BackLinkV2);
|
28
|
-
//
|
28
|
+
// V1: CKBFSData has index as optional Uint32
|
29
29
|
exports.CKBFSDataV1 = codec_1.molecule.table({
|
30
|
-
index:
|
30
|
+
index: codec_1.number.Uint32,
|
31
31
|
checksum: codec_1.number.Uint32,
|
32
32
|
contentType: base_1.blockchain.Bytes,
|
33
33
|
filename: base_1.blockchain.Bytes,
|
34
34
|
backLinks: exports.BackLinksV1,
|
35
35
|
}, ["index", "checksum", "contentType", "filename", "backLinks"]);
|
36
|
-
//
|
36
|
+
// V2: CKBFSData has indexes as vector of Uint32
|
37
37
|
exports.CKBFSDataV2 = codec_1.molecule.table({
|
38
38
|
indexes: exports.Indexes,
|
39
39
|
checksum: codec_1.number.Uint32,
|
@@ -41,20 +41,23 @@ exports.CKBFSDataV2 = codec_1.molecule.table({
|
|
41
41
|
filename: base_1.blockchain.Bytes,
|
42
42
|
backLinks: exports.BackLinksV2,
|
43
43
|
}, ["indexes", "checksum", "contentType", "filename", "backLinks"]);
|
44
|
-
// Helper function to
|
44
|
+
// Helper function to get indexes array from data
|
45
45
|
function getIndexes(data) {
|
46
|
-
if (
|
46
|
+
if (data.indexes)
|
47
47
|
return data.indexes;
|
48
|
-
|
49
|
-
if (Array.isArray(data.index)) {
|
50
|
-
return data.index;
|
51
|
-
}
|
52
|
-
if (typeof data.index === 'number') {
|
48
|
+
if (typeof data.index === 'number')
|
53
49
|
return [data.index];
|
54
|
-
}
|
55
50
|
return [];
|
56
51
|
}
|
57
|
-
// Helper function to
|
52
|
+
// Helper function to get single index from data
|
53
|
+
function getIndex(data) {
|
54
|
+
if (typeof data.index === 'number')
|
55
|
+
return data.index;
|
56
|
+
if (data.indexes && data.indexes.length > 0)
|
57
|
+
return data.indexes[0];
|
58
|
+
return 0;
|
59
|
+
}
|
60
|
+
// Helper function to safely get either index or indexes from BackLinkType for V1
|
58
61
|
function getBackLinkIndex(bl) {
|
59
62
|
if (typeof bl.index === 'number') {
|
60
63
|
return bl.index;
|
@@ -64,7 +67,7 @@ function getBackLinkIndex(bl) {
|
|
64
67
|
}
|
65
68
|
return 0;
|
66
69
|
}
|
67
|
-
// Helper function to safely get indexes array from BackLinkType
|
70
|
+
// Helper function to safely get indexes array from BackLinkType for V2
|
68
71
|
function getBackLinkIndexes(bl) {
|
69
72
|
if (Array.isArray(bl.indexes)) {
|
70
73
|
return bl.indexes;
|
@@ -78,78 +81,43 @@ function getBackLinkIndexes(bl) {
|
|
78
81
|
exports.CKBFSData = {
|
79
82
|
pack: (data, version = constants_1.ProtocolVersion.V2) => {
|
80
83
|
if (version === constants_1.ProtocolVersion.V1) {
|
81
|
-
//
|
82
|
-
let indexValue = [];
|
83
|
-
// Handle the various ways index might be specified
|
84
|
-
if (typeof data.index === 'number') {
|
85
|
-
// Single number
|
86
|
-
indexValue = [data.index];
|
87
|
-
}
|
88
|
-
else if (Array.isArray(data.index) && data.index.length > 0) {
|
89
|
-
// Array of numbers, use as is
|
90
|
-
indexValue = data.index;
|
91
|
-
}
|
92
|
-
else if (Array.isArray(data.indexes) && data.indexes.length > 0) {
|
93
|
-
// Try using indexes field if index is not available
|
94
|
-
indexValue = data.indexes;
|
95
|
-
}
|
96
|
-
// Map backlinks to V1 format - ensure each one has a single index
|
97
|
-
const v1Backlinks = data.backLinks.map(bl => {
|
98
|
-
let singleIndex = 0;
|
99
|
-
if (typeof bl.index === 'number') {
|
100
|
-
singleIndex = bl.index;
|
101
|
-
}
|
102
|
-
else if (Array.isArray(bl.indexes) && bl.indexes.length > 0) {
|
103
|
-
singleIndex = bl.indexes[0];
|
104
|
-
}
|
105
|
-
return {
|
106
|
-
txHash: bl.txHash,
|
107
|
-
index: singleIndex,
|
108
|
-
checksum: bl.checksum
|
109
|
-
};
|
110
|
-
});
|
84
|
+
// V1 formatting - uses single index
|
111
85
|
return exports.CKBFSDataV1.pack({
|
112
|
-
index:
|
86
|
+
index: getIndex(data),
|
113
87
|
checksum: data.checksum,
|
114
|
-
contentType: data.contentType,
|
115
|
-
filename: data.filename,
|
116
|
-
backLinks:
|
88
|
+
contentType: core_1.ccc.bytesFrom(data.contentType, 'utf8'),
|
89
|
+
filename: core_1.ccc.bytesFrom(data.filename, 'utf8'),
|
90
|
+
backLinks: data.backLinks.map(bl => {
|
91
|
+
// Ensure txHash is in proper format for molecule encoding
|
92
|
+
const txHash = typeof bl.txHash === 'string'
|
93
|
+
? core_1.ccc.bytesFrom(bl.txHash)
|
94
|
+
: bl.txHash;
|
95
|
+
return {
|
96
|
+
index: getBackLinkIndex(bl),
|
97
|
+
checksum: bl.checksum,
|
98
|
+
txHash,
|
99
|
+
};
|
100
|
+
}),
|
117
101
|
});
|
118
102
|
}
|
119
103
|
else {
|
120
|
-
// V2
|
121
|
-
let indexesArray = [];
|
122
|
-
// Handle different index specification formats
|
123
|
-
if (Array.isArray(data.indexes) && data.indexes.length > 0) {
|
124
|
-
indexesArray = data.indexes;
|
125
|
-
}
|
126
|
-
else if (Array.isArray(data.index) && data.index.length > 0) {
|
127
|
-
indexesArray = data.index;
|
128
|
-
}
|
129
|
-
else if (typeof data.index === 'number') {
|
130
|
-
indexesArray = [data.index];
|
131
|
-
}
|
132
|
-
// Map backlinks to V2 format - ensure each one has indexes array
|
133
|
-
const v2Backlinks = data.backLinks.map(bl => {
|
134
|
-
let indexesValue = [];
|
135
|
-
if (Array.isArray(bl.indexes) && bl.indexes.length > 0) {
|
136
|
-
indexesValue = bl.indexes;
|
137
|
-
}
|
138
|
-
else if (typeof bl.index === 'number') {
|
139
|
-
indexesValue = [bl.index];
|
140
|
-
}
|
141
|
-
return {
|
142
|
-
txHash: bl.txHash,
|
143
|
-
indexes: indexesValue,
|
144
|
-
checksum: bl.checksum
|
145
|
-
};
|
146
|
-
});
|
104
|
+
// V2 formatting - uses indexes array
|
147
105
|
return exports.CKBFSDataV2.pack({
|
148
|
-
indexes:
|
106
|
+
indexes: getIndexes(data),
|
149
107
|
checksum: data.checksum,
|
150
|
-
contentType: data.contentType,
|
151
|
-
filename: data.filename,
|
152
|
-
backLinks:
|
108
|
+
contentType: core_1.ccc.bytesFrom(data.contentType, 'utf8'),
|
109
|
+
filename: core_1.ccc.bytesFrom(data.filename, 'utf8'),
|
110
|
+
backLinks: data.backLinks.map(bl => {
|
111
|
+
// Ensure txHash is in proper format for molecule encoding
|
112
|
+
const txHash = typeof bl.txHash === 'string'
|
113
|
+
? bl.txHash
|
114
|
+
: bl.txHash;
|
115
|
+
return {
|
116
|
+
indexes: getBackLinkIndexes(bl),
|
117
|
+
checksum: bl.checksum,
|
118
|
+
txHash,
|
119
|
+
};
|
120
|
+
}),
|
153
121
|
});
|
154
122
|
}
|
155
123
|
},
|
@@ -160,12 +128,12 @@ exports.CKBFSData = {
|
|
160
128
|
return {
|
161
129
|
index: unpacked.index,
|
162
130
|
checksum: unpacked.checksum,
|
163
|
-
contentType:
|
164
|
-
filename:
|
131
|
+
contentType: core_1.ccc.bytesTo(unpacked.contentType, 'utf8'),
|
132
|
+
filename: core_1.ccc.bytesTo(unpacked.filename, 'utf8'),
|
165
133
|
backLinks: unpacked.backLinks.map(bl => ({
|
166
|
-
txHash: bl.txHash,
|
167
134
|
index: bl.index,
|
168
135
|
checksum: bl.checksum,
|
136
|
+
txHash: bl.txHash,
|
169
137
|
})),
|
170
138
|
};
|
171
139
|
}
|
@@ -175,12 +143,12 @@ exports.CKBFSData = {
|
|
175
143
|
return {
|
176
144
|
indexes: unpacked.indexes,
|
177
145
|
checksum: unpacked.checksum,
|
178
|
-
contentType:
|
179
|
-
filename:
|
146
|
+
contentType: core_1.ccc.bytesTo(unpacked.contentType, 'utf8'),
|
147
|
+
filename: core_1.ccc.bytesTo(unpacked.filename, 'utf8'),
|
180
148
|
backLinks: unpacked.backLinks.map(bl => ({
|
181
|
-
txHash: bl.txHash,
|
182
149
|
indexes: bl.indexes,
|
183
150
|
checksum: bl.checksum,
|
151
|
+
txHash: bl.txHash,
|
184
152
|
})),
|
185
153
|
};
|
186
154
|
}
|
@@ -51,7 +51,9 @@ async function createPublishTransaction(signer, options) {
|
|
51
51
|
const textEncoder = new TextEncoder();
|
52
52
|
const combinedContent = Buffer.concat(contentChunks);
|
53
53
|
const checksum = await (0, checksum_1.calculateChecksum)(combinedContent);
|
54
|
-
// Create CKBFS witnesses
|
54
|
+
// Create CKBFS witnesses - each chunk already includes the CKBFS header
|
55
|
+
// Pass 0 as version byte - this is the protocol version byte in the witness header
|
56
|
+
// not to be confused with the Protocol Version (V1 vs V2)
|
55
57
|
const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
|
56
58
|
// Calculate the actual witness indices where our content is placed
|
57
59
|
// Index 0 is reserved for the secp256k1 witness for signing
|
@@ -61,24 +63,24 @@ async function createPublishTransaction(signer, options) {
|
|
61
63
|
// Create CKBFS cell output data based on version
|
62
64
|
let outputData;
|
63
65
|
if (version === constants_1.ProtocolVersion.V1) {
|
64
|
-
// V1 format: Single index field
|
66
|
+
// V1 format: Single index field (a single number, not an array)
|
65
67
|
// For V1, use the first index where content is placed
|
66
68
|
outputData = molecule_1.CKBFSData.pack({
|
67
|
-
index:
|
69
|
+
index: contentStartIndex,
|
68
70
|
checksum,
|
69
|
-
contentType:
|
70
|
-
filename:
|
71
|
+
contentType: contentType,
|
72
|
+
filename: filename,
|
71
73
|
backLinks: [],
|
72
74
|
}, version);
|
73
75
|
}
|
74
76
|
else {
|
75
|
-
// V2 format: Multiple indexes
|
77
|
+
// V2 format: Multiple indexes (array of numbers)
|
76
78
|
// For V2, use all the indices where content is placed
|
77
79
|
outputData = molecule_1.CKBFSData.pack({
|
78
80
|
indexes: witnessIndices,
|
79
81
|
checksum,
|
80
|
-
contentType
|
81
|
-
filename
|
82
|
+
contentType,
|
83
|
+
filename,
|
82
84
|
backLinks: [],
|
83
85
|
}, version);
|
84
86
|
}
|
@@ -156,115 +158,84 @@ async function createAppendTransaction(signer, options) {
|
|
156
158
|
const { outPoint, data, type, lock, capacity } = ckbfsCell;
|
157
159
|
// Get CKBFS script config early to use version info
|
158
160
|
const config = (0, constants_1.getCKBFSScriptConfig)(network, version);
|
159
|
-
// Create CKBFS witnesses -
|
160
|
-
// Pass the version
|
161
|
-
|
162
|
-
|
161
|
+
// Create CKBFS witnesses - each chunk already includes the CKBFS header
|
162
|
+
// Pass 0 as version byte - this is the protocol version byte in the witness header
|
163
|
+
// not to be confused with the Protocol Version (V1 vs V2)
|
164
|
+
const ckbfsWitnesses = (0, witness_1.createChunkedCKBFSWitnesses)(contentChunks);
|
165
|
+
// Combine the new content chunks for checksum calculation
|
163
166
|
const combinedContent = Buffer.concat(contentChunks);
|
164
|
-
//
|
165
|
-
//
|
166
|
-
// cumulative nature
|
167
|
+
// Update the existing checksum with the new content - this matches Adler32's
|
168
|
+
// cumulative nature as required by Rule 11 in the RFC
|
167
169
|
const contentChecksum = await (0, checksum_1.updateChecksum)(data.checksum, combinedContent);
|
168
170
|
console.log(`Updated checksum from ${data.checksum} to ${contentChecksum} for appended content`);
|
171
|
+
// Calculate the actual witness indices where our content is placed
|
172
|
+
// Index 0 is reserved for the secp256k1 witness for signing
|
173
|
+
// So our CKBFS data starts at index 1
|
174
|
+
const contentStartIndex = 1;
|
175
|
+
const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
|
169
176
|
// Create backlink for the current state based on version
|
170
177
|
let newBackLink;
|
171
178
|
if (version === constants_1.ProtocolVersion.V1) {
|
172
179
|
// V1 format: Use index field (single number)
|
173
|
-
// In V1, BackLink.index should be a single number (not an array)
|
174
|
-
let singleIndex = 0;
|
175
|
-
if (data.index) {
|
176
|
-
if (Array.isArray(data.index) && data.index.length > 0) {
|
177
|
-
singleIndex = data.index[0];
|
178
|
-
}
|
179
|
-
else if (typeof data.index === 'number') {
|
180
|
-
singleIndex = data.index;
|
181
|
-
}
|
182
|
-
}
|
183
180
|
newBackLink = {
|
184
|
-
|
185
|
-
index
|
181
|
+
// In V1, field order is index, checksum, txHash
|
182
|
+
// and index is a single number value, not an array
|
183
|
+
index: data.index || (data.indexes && data.indexes.length > 0 ? data.indexes[0] : 0),
|
186
184
|
checksum: data.checksum,
|
185
|
+
txHash: outPoint.txHash,
|
187
186
|
};
|
188
187
|
}
|
189
188
|
else {
|
190
189
|
// V2 format: Use indexes field (array of numbers)
|
191
|
-
const indices = [];
|
192
|
-
if (data.indexes && Array.isArray(data.indexes)) {
|
193
|
-
indices.push(...data.indexes);
|
194
|
-
}
|
195
|
-
else if (data.index) {
|
196
|
-
if (Array.isArray(data.index)) {
|
197
|
-
indices.push(...data.index);
|
198
|
-
}
|
199
|
-
else if (typeof data.index === 'number') {
|
200
|
-
indices.push(data.index);
|
201
|
-
}
|
202
|
-
}
|
203
190
|
newBackLink = {
|
204
|
-
|
205
|
-
indexes
|
191
|
+
// In V2, field order is indexes, checksum, txHash
|
192
|
+
// and indexes is an array of numbers
|
193
|
+
indexes: data.indexes || (data.index ? [data.index] : []),
|
206
194
|
checksum: data.checksum,
|
195
|
+
txHash: outPoint.txHash,
|
207
196
|
};
|
208
197
|
}
|
209
|
-
// Update backlinks -
|
198
|
+
// Update backlinks - add the new one to the existing backlinks array
|
210
199
|
const backLinks = [...(data.backLinks || []), newBackLink];
|
211
|
-
//
|
212
|
-
// Index 0 is reserved for the secp256k1 witness for signing
|
213
|
-
// So our CKBFS data starts at index 1
|
214
|
-
const contentStartIndex = 1;
|
215
|
-
const witnessIndices = Array.from({ length: contentChunks.length }, (_, i) => contentStartIndex + i);
|
216
|
-
// Define indices based on version
|
200
|
+
// Define output data based on version
|
217
201
|
let outputData;
|
218
202
|
if (version === constants_1.ProtocolVersion.V1) {
|
219
|
-
// In V1, index
|
220
|
-
//
|
203
|
+
// In V1, index is a single number, not an array
|
204
|
+
// The first witness index is used (V1 can only reference one witness)
|
221
205
|
outputData = molecule_1.CKBFSData.pack({
|
222
|
-
index: witnessIndices[0], // Use the first
|
206
|
+
index: witnessIndices[0], // Use only the first index as a number
|
223
207
|
checksum: contentChecksum,
|
224
208
|
contentType: data.contentType,
|
225
209
|
filename: data.filename,
|
226
|
-
backLinks
|
227
|
-
|
228
|
-
let singleIndex = 0;
|
229
|
-
// Handle existing index field
|
230
|
-
if (typeof bl.index === 'number') {
|
231
|
-
singleIndex = bl.index;
|
232
|
-
}
|
233
|
-
// Try to extract from indexes array if available
|
234
|
-
else if (bl.indexes && Array.isArray(bl.indexes) && bl.indexes.length > 0) {
|
235
|
-
singleIndex = bl.indexes[0];
|
236
|
-
}
|
237
|
-
return {
|
238
|
-
txHash: bl.txHash,
|
239
|
-
index: singleIndex,
|
240
|
-
checksum: bl.checksum
|
241
|
-
};
|
242
|
-
}),
|
243
|
-
}, version);
|
210
|
+
backLinks,
|
211
|
+
}, constants_1.ProtocolVersion.V1); // Explicitly use V1 for packing
|
244
212
|
}
|
245
213
|
else {
|
246
|
-
// In V2,
|
247
|
-
// Rule 13 for V2: Output CKBFS Cell's indexes cannot be empty
|
214
|
+
// In V2, indexes is an array of witness indices
|
248
215
|
outputData = molecule_1.CKBFSData.pack({
|
249
216
|
indexes: witnessIndices,
|
250
217
|
checksum: contentChecksum,
|
251
218
|
contentType: data.contentType,
|
252
219
|
filename: data.filename,
|
253
220
|
backLinks,
|
254
|
-
},
|
221
|
+
}, constants_1.ProtocolVersion.V2); // Explicitly use V2 for packing
|
255
222
|
}
|
256
223
|
// Pack the original data to get its size - use the appropriate version
|
257
224
|
const originalData = molecule_1.CKBFSData.pack(data, version);
|
258
225
|
const originalDataSize = originalData.length;
|
259
|
-
// Get sizes
|
226
|
+
// Get sizes and calculate capacity requirements
|
260
227
|
const newDataSize = outputData.length;
|
261
|
-
|
262
|
-
//
|
263
|
-
//
|
264
|
-
|
265
|
-
//
|
266
|
-
|
267
|
-
const
|
228
|
+
// Calculate the required capacity for the output cell
|
229
|
+
// This accounts for:
|
230
|
+
// 1. The output data size
|
231
|
+
// 2. The type script's occupied size
|
232
|
+
// 3. The lock script's occupied size
|
233
|
+
// 4. A constant of 8 bytes (for header overhead)
|
234
|
+
const ckbfsCellSize = BigInt(outputData.length + type.occupiedSize + lock.occupiedSize + 8) * 100000000n;
|
235
|
+
console.log(`Original capacity: ${capacity}, Calculated size: ${ckbfsCellSize}, Data size: ${outputData.length}`);
|
236
|
+
// Use the maximum value between calculated size and original capacity
|
237
|
+
// to ensure we have enough capacity but don't decrease capacity unnecessarily
|
238
|
+
const outputCapacity = ckbfsCellSize > capacity ? ckbfsCellSize : capacity;
|
268
239
|
// Create initial transaction with the CKBFS cell input
|
269
240
|
const tx = core_1.Transaction.from({
|
270
241
|
inputs: [
|
@@ -302,7 +273,8 @@ async function createAppendTransaction(signer, options) {
|
|
302
273
|
// Get the recommended address to ensure lock script cell deps are included
|
303
274
|
const address = await signer.getRecommendedAddressObj();
|
304
275
|
// If we need more capacity than the original cell had, add additional inputs
|
305
|
-
if (
|
276
|
+
if (outputCapacity > capacity) {
|
277
|
+
console.log(`Need additional capacity: ${outputCapacity - capacity} shannons`);
|
306
278
|
// Add more inputs to cover the increased capacity
|
307
279
|
await tx.completeInputsByCapacity(signer);
|
308
280
|
}
|
package/dist/utils/witness.d.ts
CHANGED
@@ -7,14 +7,14 @@
|
|
7
7
|
* @param version Optional version byte (default is 0)
|
8
8
|
* @returns Uint8Array containing the witness data
|
9
9
|
*/
|
10
|
-
export declare function createCKBFSWitness(content: Uint8Array
|
10
|
+
export declare function createCKBFSWitness(content: Uint8Array): Uint8Array;
|
11
11
|
/**
|
12
12
|
* Creates a CKBFS witness with text content
|
13
13
|
* @param text The text content to include in the witness
|
14
14
|
* @param version Optional version byte (default is 0)
|
15
15
|
* @returns Uint8Array containing the witness data
|
16
16
|
*/
|
17
|
-
export declare function createTextCKBFSWitness(text: string
|
17
|
+
export declare function createTextCKBFSWitness(text: string): Uint8Array;
|
18
18
|
/**
|
19
19
|
* Extracts content from a CKBFS witness
|
20
20
|
* @param witness The CKBFS witness data
|
@@ -34,7 +34,6 @@ export declare function isCKBFSWitness(witness: Uint8Array): boolean;
|
|
34
34
|
* Creates an array of witnesses for a CKBFS transaction from content chunks
|
35
35
|
* @param contentChunks Array of content chunks
|
36
36
|
* @param version Optional version byte (default is 0)
|
37
|
-
* @param protocolVersion Optional protocol version (default is V2)
|
38
37
|
* @returns Array of Uint8Array witnesses
|
39
38
|
*/
|
40
|
-
export declare function createChunkedCKBFSWitnesses(contentChunks: Uint8Array[]
|
39
|
+
export declare function createChunkedCKBFSWitnesses(contentChunks: Uint8Array[]): Uint8Array[];
|
package/dist/utils/witness.js
CHANGED
@@ -15,9 +15,10 @@ const molecule_1 = require("./molecule");
|
|
15
15
|
* @param version Optional version byte (default is 0)
|
16
16
|
* @returns Uint8Array containing the witness data
|
17
17
|
*/
|
18
|
-
function createCKBFSWitness(content
|
18
|
+
function createCKBFSWitness(content) {
|
19
19
|
// Create witness with CKBFS header, version byte, and content
|
20
|
-
|
20
|
+
// Version byte must always be 0x00 per protocol
|
21
|
+
const versionByte = new Uint8Array([0]);
|
21
22
|
return Buffer.concat([molecule_1.CKBFS_HEADER, versionByte, content]);
|
22
23
|
}
|
23
24
|
/**
|
@@ -26,10 +27,10 @@ function createCKBFSWitness(content, version = 0) {
|
|
26
27
|
* @param version Optional version byte (default is 0)
|
27
28
|
* @returns Uint8Array containing the witness data
|
28
29
|
*/
|
29
|
-
function createTextCKBFSWitness(text
|
30
|
+
function createTextCKBFSWitness(text) {
|
30
31
|
const textEncoder = new TextEncoder();
|
31
32
|
const contentBytes = textEncoder.encode(text);
|
32
|
-
return createCKBFSWitness(contentBytes
|
33
|
+
return createCKBFSWitness(contentBytes);
|
33
34
|
}
|
34
35
|
/**
|
35
36
|
* Extracts content from a CKBFS witness
|
@@ -65,9 +66,8 @@ function isCKBFSWitness(witness) {
|
|
65
66
|
* Creates an array of witnesses for a CKBFS transaction from content chunks
|
66
67
|
* @param contentChunks Array of content chunks
|
67
68
|
* @param version Optional version byte (default is 0)
|
68
|
-
* @param protocolVersion Optional protocol version (default is V2)
|
69
69
|
* @returns Array of Uint8Array witnesses
|
70
70
|
*/
|
71
|
-
function createChunkedCKBFSWitnesses(contentChunks
|
72
|
-
return contentChunks.map(chunk => createCKBFSWitness(chunk
|
71
|
+
function createChunkedCKBFSWitnesses(contentChunks) {
|
72
|
+
return contentChunks.map(chunk => createCKBFSWitness(chunk));
|
73
73
|
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import { CKBFS, NetworkType, ProtocolVersion, getFileContentFromChain, saveFileFromChain } from '../src/index';
|
2
|
+
import { ClientPublicTestnet } from "@ckb-ccc/core";
|
3
|
+
|
4
|
+
// Replace with your actual private key (or leave this default if just reading)
|
5
|
+
const privateKey = process.env.CKB_PRIVATE_KEY || 'your-private-key-here';
|
6
|
+
|
7
|
+
// Parse command line arguments for transaction hash
|
8
|
+
const txHashArg = process.argv.find(arg => arg.startsWith('--txhash='));
|
9
|
+
const outputArg = process.argv.find(arg => arg.startsWith('--output='));
|
10
|
+
|
11
|
+
const txHash = txHashArg ? txHashArg.split('=')[1] : process.env.CKBFS_TX_HASH || '';
|
12
|
+
const outputPath = outputArg ? outputArg.split('=')[1] : undefined;
|
13
|
+
|
14
|
+
if (!txHash) {
|
15
|
+
console.error('Please provide a transaction hash using --txhash=<tx_hash> or the CKBFS_TX_HASH environment variable');
|
16
|
+
process.exit(1);
|
17
|
+
}
|
18
|
+
|
19
|
+
// Initialize the SDK (read-only is fine for retrieval)
|
20
|
+
const ckbfs = new CKBFS(
|
21
|
+
privateKey,
|
22
|
+
NetworkType.Testnet,
|
23
|
+
{
|
24
|
+
version: ProtocolVersion.V2,
|
25
|
+
useTypeID: false
|
26
|
+
}
|
27
|
+
);
|
28
|
+
|
29
|
+
// Initialize CKB client for testnet
|
30
|
+
const client = new ClientPublicTestnet();
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Example of retrieving a file from CKBFS
|
34
|
+
*/
|
35
|
+
async function retrieveExample() {
|
36
|
+
try {
|
37
|
+
console.log(`Retrieving CKBFS file from transaction: ${txHash}`);
|
38
|
+
|
39
|
+
// Get transaction details
|
40
|
+
const txWithStatus = await client.getTransaction(txHash);
|
41
|
+
if (!txWithStatus || !txWithStatus.transaction) {
|
42
|
+
throw new Error(`Transaction ${txHash} not found`);
|
43
|
+
}
|
44
|
+
|
45
|
+
// Find index of the CKBFS cell in outputs (assuming it's the first one with a type script)
|
46
|
+
const tx = txWithStatus.transaction;
|
47
|
+
let ckbfsCellIndex = 0;
|
48
|
+
|
49
|
+
// Get cell data
|
50
|
+
const outputData = tx.outputsData[ckbfsCellIndex];
|
51
|
+
if (!outputData) {
|
52
|
+
throw new Error('Output data not found');
|
53
|
+
}
|
54
|
+
|
55
|
+
// Get cell info for retrieval
|
56
|
+
const outPoint = {
|
57
|
+
txHash,
|
58
|
+
index: ckbfsCellIndex
|
59
|
+
};
|
60
|
+
|
61
|
+
// Import necessary components from index
|
62
|
+
const { CKBFSData } = require('../src/index');
|
63
|
+
|
64
|
+
// Parse the output data
|
65
|
+
const rawData = outputData.startsWith('0x')
|
66
|
+
? Buffer.from(outputData.slice(2), 'hex')
|
67
|
+
: Buffer.from(outputData, 'hex');
|
68
|
+
|
69
|
+
// Try with both V1 and V2 protocols
|
70
|
+
let ckbfsData;
|
71
|
+
try {
|
72
|
+
console.log('Trying to unpack with V2...');
|
73
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V2);
|
74
|
+
} catch (error) {
|
75
|
+
console.log('Failed with V2, trying V1...');
|
76
|
+
ckbfsData = CKBFSData.unpack(rawData, ProtocolVersion.V1);
|
77
|
+
}
|
78
|
+
|
79
|
+
// Retrieve full file content
|
80
|
+
console.log('Retrieving file content by following backlinks...');
|
81
|
+
const fileContent = await getFileContentFromChain(client, outPoint, ckbfsData);
|
82
|
+
console.log(`Retrieved file content: ${fileContent.length} bytes`);
|
83
|
+
|
84
|
+
// Save to file
|
85
|
+
const savedPath = saveFileFromChain(fileContent, ckbfsData, outputPath);
|
86
|
+
console.log(`File saved to: ${savedPath}`);
|
87
|
+
|
88
|
+
return savedPath;
|
89
|
+
} catch (error) {
|
90
|
+
console.error('Error retrieving file:', error);
|
91
|
+
throw error;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Main function to run the example
|
97
|
+
*/
|
98
|
+
async function main() {
|
99
|
+
console.log('Running CKBFS file retrieval example...');
|
100
|
+
console.log('--------------------------------------');
|
101
|
+
|
102
|
+
try {
|
103
|
+
await retrieveExample();
|
104
|
+
console.log('Example completed successfully!');
|
105
|
+
process.exit(0);
|
106
|
+
} catch (error) {
|
107
|
+
console.error('Example failed:', error);
|
108
|
+
process.exit(1);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
// Run the example if this file is executed directly
|
113
|
+
if (require.main === module) {
|
114
|
+
main().catch(console.error);
|
115
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ckbfs/api",
|
3
|
-
"version": "1.1
|
3
|
+
"version": "1.2.1",
|
4
4
|
"description": "SDK for CKBFS protocol on CKB",
|
5
5
|
"license": "MIT",
|
6
6
|
"author": "Code Monad<code@lab-11.org>",
|
@@ -14,7 +14,8 @@
|
|
14
14
|
"test": "jest",
|
15
15
|
"example": "ts-node examples/index.ts",
|
16
16
|
"example:publish": "ts-node examples/publish.ts",
|
17
|
-
"example:append": "ts-node examples/append.ts"
|
17
|
+
"example:append": "ts-node examples/append.ts",
|
18
|
+
"example:retrieve": "ts-node examples/retrieve.ts"
|
18
19
|
},
|
19
20
|
"keywords": [
|
20
21
|
"ckb",
|
package/src/index.ts
CHANGED
@@ -22,7 +22,9 @@ import {
|
|
22
22
|
writeFile,
|
23
23
|
getContentType,
|
24
24
|
splitFileIntoChunks,
|
25
|
-
combineChunksToFile
|
25
|
+
combineChunksToFile,
|
26
|
+
getFileContentFromChain,
|
27
|
+
saveFileFromChain
|
26
28
|
} from './utils/file';
|
27
29
|
import {
|
28
30
|
createCKBFSWitness,
|
@@ -323,6 +325,8 @@ export {
|
|
323
325
|
getContentType,
|
324
326
|
splitFileIntoChunks,
|
325
327
|
combineChunksToFile,
|
328
|
+
getFileContentFromChain,
|
329
|
+
saveFileFromChain,
|
326
330
|
|
327
331
|
// Witness utilities
|
328
332
|
createCKBFSWitness,
|
package/src/utils/checksum.ts
CHANGED
@@ -44,11 +44,16 @@ export async function updateChecksum(previousChecksum: number, newData: Uint8Arr
|
|
44
44
|
}
|
45
45
|
|
46
46
|
// Combine a and b to get the final checksum
|
47
|
-
|
47
|
+
// Use a Uint32Array to ensure we get a proper unsigned 32-bit integer
|
48
|
+
const buffer = new ArrayBuffer(4);
|
49
|
+
const view = new DataView(buffer);
|
50
|
+
view.setUint16(0, adlerA, true); // Set lower 16 bits (little endian)
|
51
|
+
view.setUint16(2, adlerB, true); // Set upper 16 bits (little endian)
|
48
52
|
|
49
|
-
//
|
50
|
-
|
51
|
-
|
53
|
+
// Read as an unsigned 32-bit integer
|
54
|
+
const updatedChecksum = view.getUint32(0, true);
|
55
|
+
|
56
|
+
console.log(`Updated checksum from ${previousChecksum} to ${updatedChecksum} for appended content`);
|
52
57
|
|
53
58
|
return updatedChecksum;
|
54
59
|
}
|
package/src/utils/file.ts
CHANGED
@@ -106,4 +106,147 @@ export function splitFileIntoChunks(filePath: string, chunkSize: number): Uint8A
|
|
106
106
|
export function combineChunksToFile(chunks: Uint8Array[], outputPath: string): void {
|
107
107
|
const combinedBuffer = Buffer.concat(chunks.map(chunk => Buffer.from(chunk)));
|
108
108
|
writeFile(outputPath, combinedBuffer);
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Utility function to safely decode buffer to string
|
113
|
+
* @param buffer The buffer to decode
|
114
|
+
* @returns Decoded string or placeholder on error
|
115
|
+
*/
|
116
|
+
function safelyDecode(buffer: any): string {
|
117
|
+
if (!buffer) return '[Unknown]';
|
118
|
+
try {
|
119
|
+
if (buffer instanceof Uint8Array) {
|
120
|
+
return new TextDecoder().decode(buffer);
|
121
|
+
} else if (typeof buffer === 'string') {
|
122
|
+
return buffer;
|
123
|
+
} else {
|
124
|
+
return `[Buffer: ${buffer.toString()}]`;
|
125
|
+
}
|
126
|
+
} catch (e) {
|
127
|
+
return '[Decode Error]';
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
/**
|
132
|
+
* Retrieves complete file content from the blockchain by following backlinks
|
133
|
+
* @param client The CKB client to use for blockchain queries
|
134
|
+
* @param outPoint The output point of the latest CKBFS cell
|
135
|
+
* @param ckbfsData The data from the latest CKBFS cell
|
136
|
+
* @returns Promise resolving to the complete file content
|
137
|
+
*/
|
138
|
+
export async function getFileContentFromChain(
|
139
|
+
client: any,
|
140
|
+
outPoint: { txHash: string; index: number },
|
141
|
+
ckbfsData: any
|
142
|
+
): Promise<Uint8Array> {
|
143
|
+
console.log(`Retrieving file: ${safelyDecode(ckbfsData.filename)}`);
|
144
|
+
console.log(`Content type: ${safelyDecode(ckbfsData.contentType)}`);
|
145
|
+
|
146
|
+
// Prepare to collect all content pieces
|
147
|
+
const contentPieces: Uint8Array[] = [];
|
148
|
+
let currentData = ckbfsData;
|
149
|
+
let currentOutPoint = outPoint;
|
150
|
+
|
151
|
+
// Process the current transaction first
|
152
|
+
const tx = await client.getTransaction(currentOutPoint.txHash);
|
153
|
+
if (!tx || !tx.transaction) {
|
154
|
+
throw new Error(`Transaction ${currentOutPoint.txHash} not found`);
|
155
|
+
}
|
156
|
+
|
157
|
+
// Get content from witnesses
|
158
|
+
const indexes = currentData.indexes || (currentData.index !== undefined ? [currentData.index] : []);
|
159
|
+
if (indexes.length > 0) {
|
160
|
+
// Get content from each witness index
|
161
|
+
for (const idx of indexes) {
|
162
|
+
if (idx >= tx.transaction.witnesses.length) {
|
163
|
+
console.warn(`Witness index ${idx} out of range`);
|
164
|
+
continue;
|
165
|
+
}
|
166
|
+
|
167
|
+
const witnessHex = tx.transaction.witnesses[idx];
|
168
|
+
const witness = Buffer.from(witnessHex.slice(2), 'hex'); // Remove 0x prefix
|
169
|
+
|
170
|
+
// Extract content (skip CKBFS header + version byte)
|
171
|
+
if (witness.length >= 6 && witness.slice(0, 5).toString() === 'CKBFS') {
|
172
|
+
const content = witness.slice(6);
|
173
|
+
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
174
|
+
} else {
|
175
|
+
console.warn(`Witness at index ${idx} is not a valid CKBFS witness`);
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
// Follow backlinks recursively
|
181
|
+
if (currentData.backLinks && currentData.backLinks.length > 0) {
|
182
|
+
// Process each backlink, from most recent to oldest
|
183
|
+
for (let i = currentData.backLinks.length - 1; i >= 0; i--) {
|
184
|
+
const backlink = currentData.backLinks[i];
|
185
|
+
|
186
|
+
// Get the transaction for this backlink
|
187
|
+
const backTx = await client.getTransaction(backlink.txHash);
|
188
|
+
if (!backTx || !backTx.transaction) {
|
189
|
+
console.warn(`Backlink transaction ${backlink.txHash} not found`);
|
190
|
+
continue;
|
191
|
+
}
|
192
|
+
|
193
|
+
// Get content from backlink witnesses
|
194
|
+
const backIndexes = backlink.indexes || (backlink.index !== undefined ? [backlink.index] : []);
|
195
|
+
if (backIndexes.length > 0) {
|
196
|
+
// Get content from each witness index
|
197
|
+
for (const idx of backIndexes) {
|
198
|
+
if (idx >= backTx.transaction.witnesses.length) {
|
199
|
+
console.warn(`Backlink witness index ${idx} out of range`);
|
200
|
+
continue;
|
201
|
+
}
|
202
|
+
|
203
|
+
const witnessHex = backTx.transaction.witnesses[idx];
|
204
|
+
const witness = Buffer.from(witnessHex.slice(2), 'hex'); // Remove 0x prefix
|
205
|
+
|
206
|
+
// Extract content (skip CKBFS header + version byte)
|
207
|
+
if (witness.length >= 6 && witness.slice(0, 5).toString() === 'CKBFS') {
|
208
|
+
const content = witness.slice(6);
|
209
|
+
contentPieces.unshift(content); // Add to beginning of array (we're going backwards)
|
210
|
+
} else {
|
211
|
+
console.warn(`Backlink witness at index ${idx} is not a valid CKBFS witness`);
|
212
|
+
}
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
// Combine all content pieces
|
219
|
+
return Buffer.concat(contentPieces);
|
220
|
+
}
|
221
|
+
|
222
|
+
/**
|
223
|
+
* Saves file content retrieved from blockchain to disk
|
224
|
+
* @param content The file content to save
|
225
|
+
* @param ckbfsData The CKBFS cell data containing file metadata
|
226
|
+
* @param outputPath Optional path to save the file (defaults to filename in current directory)
|
227
|
+
* @returns The path where the file was saved
|
228
|
+
*/
|
229
|
+
export function saveFileFromChain(
|
230
|
+
content: Uint8Array,
|
231
|
+
ckbfsData: any,
|
232
|
+
outputPath?: string
|
233
|
+
): string {
|
234
|
+
// Get filename from CKBFS data
|
235
|
+
const filename = safelyDecode(ckbfsData.filename);
|
236
|
+
|
237
|
+
// Determine output path
|
238
|
+
const filePath = outputPath || filename;
|
239
|
+
|
240
|
+
// Ensure directory exists
|
241
|
+
const directory = path.dirname(filePath);
|
242
|
+
if (!fs.existsSync(directory)) {
|
243
|
+
fs.mkdirSync(directory, { recursive: true });
|
244
|
+
}
|
245
|
+
|
246
|
+
// Write file
|
247
|
+
fs.writeFileSync(filePath, content);
|
248
|
+
console.log(`File saved to: ${filePath}`);
|
249
|
+
console.log(`Size: ${content.length} bytes`);
|
250
|
+
|
251
|
+
return filePath;
|
109
252
|
}
|