@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 +6 -0
- package/examples/retrieve.ts +115 -0
- package/package.json +3 -2
- package/src/index.ts +5 -1
- 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
|
|
@@ -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.
|
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
|
}
|