@ckbfs/api 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
 
@@ -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.0",
3
+ "version": "1.2.0",
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/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
  }
@@ -0,0 +1,2 @@
1
+ Hello, CKBFS! This is a sample file published using 20240906.ce6724722cf6 protocol.
2
+ This is content to append to the previously published file.