@ckbfs/api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/RFC.v2.md +341 -0
- package/append.txt +1 -0
- package/example.txt +1 -0
- package/examples/append.ts +222 -0
- package/examples/index.ts +43 -0
- package/examples/publish.ts +76 -0
- package/package.json +39 -0
- package/src/index.ts +363 -0
- package/src/utils/checksum.ts +74 -0
- package/src/utils/constants.ts +123 -0
- package/src/utils/file.ts +109 -0
- package/src/utils/molecule.ts +190 -0
- package/src/utils/transaction.ts +420 -0
- package/src/utils/witness.ts +76 -0
- package/tsconfig.json +15 -0
@@ -0,0 +1,420 @@
|
|
1
|
+
import { ccc, Transaction, Script, Signer } from "@ckb-ccc/core";
|
2
|
+
import { calculateChecksum, updateChecksum } from './checksum';
|
3
|
+
import { CKBFSData, BackLinkType, CKBFSDataType } from './molecule';
|
4
|
+
import { createChunkedCKBFSWitnesses } from './witness';
|
5
|
+
import {
|
6
|
+
getCKBFSScriptConfig,
|
7
|
+
NetworkType,
|
8
|
+
ProtocolVersion,
|
9
|
+
DEFAULT_NETWORK,
|
10
|
+
DEFAULT_VERSION
|
11
|
+
} from './constants';
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Utility functions for CKB transaction creation and handling
|
15
|
+
*/
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Options for creating a CKBFS cell
|
19
|
+
*/
|
20
|
+
export interface CKBFSCellOptions {
|
21
|
+
contentType: string;
|
22
|
+
filename: string;
|
23
|
+
capacity?: bigint;
|
24
|
+
lock: Script;
|
25
|
+
network?: NetworkType;
|
26
|
+
version?: string;
|
27
|
+
useTypeID?: boolean;
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Options for publishing a file to CKBFS
|
32
|
+
*/
|
33
|
+
export interface PublishOptions extends CKBFSCellOptions {
|
34
|
+
contentChunks: Uint8Array[];
|
35
|
+
feeRate?: number;
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Options for appending content to a CKBFS file
|
40
|
+
*/
|
41
|
+
export interface AppendOptions {
|
42
|
+
ckbfsCell: {
|
43
|
+
outPoint: { txHash: string; index: number };
|
44
|
+
data: CKBFSDataType;
|
45
|
+
type: Script;
|
46
|
+
lock: Script;
|
47
|
+
capacity: bigint;
|
48
|
+
};
|
49
|
+
contentChunks: Uint8Array[];
|
50
|
+
feeRate?: number;
|
51
|
+
network?: NetworkType;
|
52
|
+
version?: string;
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Ensures a string is prefixed with '0x'
|
57
|
+
* @param value The string to ensure is hex prefixed
|
58
|
+
* @returns A hex prefixed string
|
59
|
+
*/
|
60
|
+
function ensureHexPrefix(value: string): `0x${string}` {
|
61
|
+
if (value.startsWith('0x')) {
|
62
|
+
return value as `0x${string}`;
|
63
|
+
}
|
64
|
+
return `0x${value}` as `0x${string}`;
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Creates a CKBFS cell
|
69
|
+
* @param options Options for creating the CKBFS cell
|
70
|
+
* @returns The created cell output
|
71
|
+
*/
|
72
|
+
export function createCKBFSCell(options: CKBFSCellOptions) {
|
73
|
+
const {
|
74
|
+
contentType,
|
75
|
+
filename,
|
76
|
+
capacity,
|
77
|
+
lock,
|
78
|
+
network = DEFAULT_NETWORK,
|
79
|
+
version = DEFAULT_VERSION,
|
80
|
+
useTypeID = false
|
81
|
+
} = options;
|
82
|
+
|
83
|
+
// Get CKBFS script config
|
84
|
+
const config = getCKBFSScriptConfig(network, version, useTypeID);
|
85
|
+
|
86
|
+
// Create pre CKBFS type script
|
87
|
+
const preCkbfsTypeScript = new Script(
|
88
|
+
ensureHexPrefix(config.codeHash),
|
89
|
+
config.hashType as any,
|
90
|
+
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
91
|
+
);
|
92
|
+
|
93
|
+
// Return the cell output
|
94
|
+
return {
|
95
|
+
lock,
|
96
|
+
type: preCkbfsTypeScript,
|
97
|
+
capacity: capacity || 200n * 100000000n, // Default 200 CKB
|
98
|
+
};
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Creates a transaction for publishing a file to CKBFS
|
103
|
+
* @param signer The signer to use for the transaction
|
104
|
+
* @param options Options for publishing the file
|
105
|
+
* @returns Promise resolving to the created transaction
|
106
|
+
*/
|
107
|
+
export async function createPublishTransaction(
|
108
|
+
signer: Signer,
|
109
|
+
options: PublishOptions
|
110
|
+
): Promise<Transaction> {
|
111
|
+
const {
|
112
|
+
contentChunks,
|
113
|
+
contentType,
|
114
|
+
filename,
|
115
|
+
lock,
|
116
|
+
feeRate,
|
117
|
+
network = DEFAULT_NETWORK,
|
118
|
+
version = DEFAULT_VERSION,
|
119
|
+
useTypeID = false
|
120
|
+
} = options;
|
121
|
+
|
122
|
+
// Calculate checksum for the combined content
|
123
|
+
const textEncoder = new TextEncoder();
|
124
|
+
const combinedContent = Buffer.concat(contentChunks);
|
125
|
+
const checksum = await calculateChecksum(combinedContent);
|
126
|
+
|
127
|
+
// Create CKBFS witnesses
|
128
|
+
const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
|
129
|
+
|
130
|
+
// Calculate the actual witness indices where our content is placed
|
131
|
+
// Index 0 is reserved for the secp256k1 witness for signing
|
132
|
+
// So our CKBFS data starts at index 1
|
133
|
+
const contentStartIndex = 1;
|
134
|
+
const witnessIndices = Array.from(
|
135
|
+
{ length: contentChunks.length },
|
136
|
+
(_, i) => contentStartIndex + i
|
137
|
+
);
|
138
|
+
|
139
|
+
// Create CKBFS cell output data based on version
|
140
|
+
let outputData: Uint8Array;
|
141
|
+
|
142
|
+
if (version === ProtocolVersion.V1) {
|
143
|
+
// V1 format: Single index field
|
144
|
+
// For V1, use the first index where content is placed
|
145
|
+
outputData = CKBFSData.pack({
|
146
|
+
index: [contentStartIndex],
|
147
|
+
checksum,
|
148
|
+
contentType: textEncoder.encode(contentType),
|
149
|
+
filename: textEncoder.encode(filename),
|
150
|
+
backLinks: [],
|
151
|
+
}, version);
|
152
|
+
} else {
|
153
|
+
// V2 format: Multiple indexes
|
154
|
+
// For V2, use all the indices where content is placed
|
155
|
+
outputData = CKBFSData.pack({
|
156
|
+
indexes: witnessIndices,
|
157
|
+
checksum,
|
158
|
+
contentType: textEncoder.encode(contentType),
|
159
|
+
filename: textEncoder.encode(filename),
|
160
|
+
backLinks: [],
|
161
|
+
}, version);
|
162
|
+
}
|
163
|
+
|
164
|
+
// Get CKBFS script config
|
165
|
+
const config = getCKBFSScriptConfig(network, version, useTypeID);
|
166
|
+
|
167
|
+
// Create pre transaction without cell deps initially
|
168
|
+
const preTx = Transaction.from({
|
169
|
+
outputs: [
|
170
|
+
createCKBFSCell({
|
171
|
+
contentType,
|
172
|
+
filename,
|
173
|
+
lock,
|
174
|
+
network,
|
175
|
+
version,
|
176
|
+
useTypeID
|
177
|
+
})
|
178
|
+
],
|
179
|
+
witnesses: [
|
180
|
+
[], // Empty secp witness for signing
|
181
|
+
...ckbfsWitnesses.map(w => `0x${Buffer.from(w).toString('hex')}`),
|
182
|
+
],
|
183
|
+
outputsData: [
|
184
|
+
outputData,
|
185
|
+
]
|
186
|
+
});
|
187
|
+
|
188
|
+
// Add the CKBFS dep group cell dependency
|
189
|
+
preTx.addCellDeps({
|
190
|
+
outPoint: {
|
191
|
+
txHash: ensureHexPrefix(config.depTxHash),
|
192
|
+
index: config.depIndex || 0,
|
193
|
+
},
|
194
|
+
depType: "depGroup"
|
195
|
+
});
|
196
|
+
|
197
|
+
// Get the recommended address to ensure lock script cell deps are included
|
198
|
+
const address = await signer.getRecommendedAddressObj();
|
199
|
+
|
200
|
+
// Complete inputs by capacity
|
201
|
+
await preTx.completeInputsByCapacity(signer);
|
202
|
+
|
203
|
+
// Complete fee change to lock
|
204
|
+
await preTx.completeFeeChangeToLock(signer, lock, feeRate || 2000);
|
205
|
+
|
206
|
+
// Create type ID args
|
207
|
+
const args = ccc.hashTypeId(preTx.inputs[0], 0x0);
|
208
|
+
|
209
|
+
// Create CKBFS type script with type ID
|
210
|
+
const ckbfsTypeScript = new Script(
|
211
|
+
ensureHexPrefix(config.codeHash),
|
212
|
+
config.hashType as any,
|
213
|
+
args
|
214
|
+
);
|
215
|
+
|
216
|
+
// Create final transaction with same cell deps as preTx
|
217
|
+
const tx = Transaction.from({
|
218
|
+
cellDeps: preTx.cellDeps,
|
219
|
+
witnesses: [
|
220
|
+
[], // Reset first witness for signing
|
221
|
+
...preTx.witnesses.slice(1)
|
222
|
+
],
|
223
|
+
outputsData: preTx.outputsData,
|
224
|
+
inputs: preTx.inputs,
|
225
|
+
outputs: [
|
226
|
+
{
|
227
|
+
lock,
|
228
|
+
type: ckbfsTypeScript,
|
229
|
+
capacity: preTx.outputs[0].capacity,
|
230
|
+
},
|
231
|
+
...preTx.outputs.slice(1) // Include rest of outputs (e.g., change)
|
232
|
+
]
|
233
|
+
});
|
234
|
+
|
235
|
+
return tx;
|
236
|
+
}
|
237
|
+
|
238
|
+
/**
|
239
|
+
* Creates a transaction for appending content to a CKBFS file
|
240
|
+
* @param signer The signer to use for the transaction
|
241
|
+
* @param options Options for appending content
|
242
|
+
* @returns Promise resolving to the created transaction
|
243
|
+
*/
|
244
|
+
export async function createAppendTransaction(
|
245
|
+
signer: Signer,
|
246
|
+
options: AppendOptions
|
247
|
+
): Promise<Transaction> {
|
248
|
+
const {
|
249
|
+
ckbfsCell,
|
250
|
+
contentChunks,
|
251
|
+
feeRate,
|
252
|
+
network = DEFAULT_NETWORK,
|
253
|
+
version = DEFAULT_VERSION
|
254
|
+
} = options;
|
255
|
+
const { outPoint, data, type, lock, capacity } = ckbfsCell;
|
256
|
+
|
257
|
+
// Get CKBFS script config early to use version info
|
258
|
+
const config = getCKBFSScriptConfig(network, version);
|
259
|
+
|
260
|
+
// Create CKBFS witnesses - this may vary between V1 and V2
|
261
|
+
const ckbfsWitnesses = createChunkedCKBFSWitnesses(contentChunks);
|
262
|
+
|
263
|
+
// Combine the new content chunks
|
264
|
+
const combinedContent = Buffer.concat(contentChunks);
|
265
|
+
|
266
|
+
// Instead of calculating a new checksum from scratch, update the existing checksum
|
267
|
+
// with the new content - this is more efficient and matches the Adler32 algorithm's
|
268
|
+
// cumulative nature
|
269
|
+
const contentChecksum = await updateChecksum(data.checksum, combinedContent);
|
270
|
+
console.log(`Updated checksum from ${data.checksum} to ${contentChecksum} for appended content`);
|
271
|
+
|
272
|
+
// Create backlink for the current state based on version
|
273
|
+
let newBackLink: any;
|
274
|
+
|
275
|
+
if (version === ProtocolVersion.V1) {
|
276
|
+
// V1 format: Use index field (single number)
|
277
|
+
newBackLink = {
|
278
|
+
txHash: outPoint.txHash,
|
279
|
+
index: data.index && data.index.length > 0 ? data.index[0] : 0,
|
280
|
+
checksum: data.checksum,
|
281
|
+
};
|
282
|
+
} else {
|
283
|
+
// V2 format: Use indexes field (array of numbers)
|
284
|
+
newBackLink = {
|
285
|
+
txHash: outPoint.txHash,
|
286
|
+
indexes: data.indexes || data.index || [],
|
287
|
+
checksum: data.checksum,
|
288
|
+
};
|
289
|
+
}
|
290
|
+
|
291
|
+
// Update backlinks
|
292
|
+
const backLinks = [newBackLink];
|
293
|
+
|
294
|
+
// Define indices based on version
|
295
|
+
let outputData: Uint8Array;
|
296
|
+
|
297
|
+
// Calculate the actual witness indices where our content is placed
|
298
|
+
// Index 0 is reserved for the secp256k1 witness for signing
|
299
|
+
// So our CKBFS data starts at index 1
|
300
|
+
const contentStartIndex = 1;
|
301
|
+
const witnessIndices = Array.from(
|
302
|
+
{ length: contentChunks.length },
|
303
|
+
(_, i) => contentStartIndex + i
|
304
|
+
);
|
305
|
+
|
306
|
+
if (version === ProtocolVersion.V1) {
|
307
|
+
// In V1, use the first index where content is placed
|
308
|
+
// (even if we have multiple witnesses, V1 only supports a single index)
|
309
|
+
outputData = CKBFSData.pack({
|
310
|
+
index: [contentStartIndex],
|
311
|
+
checksum: contentChecksum,
|
312
|
+
contentType: data.contentType,
|
313
|
+
filename: data.filename,
|
314
|
+
backLinks,
|
315
|
+
}, version);
|
316
|
+
} else {
|
317
|
+
// In V2, use all the indices where content is placed
|
318
|
+
outputData = CKBFSData.pack({
|
319
|
+
indexes: witnessIndices,
|
320
|
+
checksum: contentChecksum,
|
321
|
+
contentType: data.contentType,
|
322
|
+
filename: data.filename,
|
323
|
+
backLinks,
|
324
|
+
}, version);
|
325
|
+
}
|
326
|
+
|
327
|
+
// Pack the original data to get its size - use the appropriate version
|
328
|
+
const originalData = CKBFSData.pack(data, version);
|
329
|
+
const originalDataSize = originalData.length;
|
330
|
+
|
331
|
+
// Get sizes
|
332
|
+
const newDataSize = outputData.length;
|
333
|
+
const dataSizeDiff = newDataSize - originalDataSize;
|
334
|
+
|
335
|
+
// Calculate the additional capacity needed (in shannons)
|
336
|
+
// CKB requires 1 shannon per byte of data
|
337
|
+
const additionalCapacity = BigInt(Math.max(0, dataSizeDiff)) * 100000000n;
|
338
|
+
|
339
|
+
// Add the additional capacity to the original cell capacity
|
340
|
+
console.log(`Original capacity: ${capacity}, Additional needed: ${additionalCapacity}, Data size diff: ${dataSizeDiff}, Version: ${version}`);
|
341
|
+
const outputCapacity = capacity + additionalCapacity;
|
342
|
+
|
343
|
+
// Create initial transaction with the CKBFS cell input
|
344
|
+
const tx = Transaction.from({
|
345
|
+
inputs: [
|
346
|
+
{
|
347
|
+
previousOutput: {
|
348
|
+
txHash: outPoint.txHash,
|
349
|
+
index: outPoint.index,
|
350
|
+
},
|
351
|
+
since: "0x0",
|
352
|
+
}
|
353
|
+
],
|
354
|
+
outputs: [
|
355
|
+
{
|
356
|
+
lock,
|
357
|
+
type,
|
358
|
+
capacity: outputCapacity,
|
359
|
+
}
|
360
|
+
],
|
361
|
+
witnesses: [
|
362
|
+
[], // Empty secp witness for signing
|
363
|
+
...ckbfsWitnesses.map(w => `0x${Buffer.from(w).toString('hex')}`),
|
364
|
+
],
|
365
|
+
outputsData: [
|
366
|
+
outputData,
|
367
|
+
]
|
368
|
+
});
|
369
|
+
|
370
|
+
// Add the CKBFS dep group cell dependency
|
371
|
+
tx.addCellDeps({
|
372
|
+
outPoint: {
|
373
|
+
txHash: ensureHexPrefix(config.depTxHash),
|
374
|
+
index: config.depIndex || 0,
|
375
|
+
},
|
376
|
+
depType: "depGroup"
|
377
|
+
});
|
378
|
+
|
379
|
+
// Get the recommended address to ensure lock script cell deps are included
|
380
|
+
const address = await signer.getRecommendedAddressObj();
|
381
|
+
|
382
|
+
// If we need more capacity than the original cell had, add additional inputs
|
383
|
+
if (additionalCapacity > 0n) {
|
384
|
+
// Add more inputs to cover the increased capacity
|
385
|
+
await tx.completeInputsByCapacity(signer);
|
386
|
+
}
|
387
|
+
|
388
|
+
// Complete fee
|
389
|
+
await tx.completeFeeChangeToLock(signer, lock || address.script, feeRate || 2000);
|
390
|
+
|
391
|
+
return tx;
|
392
|
+
}
|
393
|
+
|
394
|
+
/**
|
395
|
+
* Creates a complete transaction for publishing a file to CKBFS
|
396
|
+
* @param signer The signer to use for the transaction
|
397
|
+
* @param options Options for publishing the file
|
398
|
+
* @returns Promise resolving to the signed transaction
|
399
|
+
*/
|
400
|
+
export async function publishCKBFS(
|
401
|
+
signer: Signer,
|
402
|
+
options: PublishOptions
|
403
|
+
): Promise<Transaction> {
|
404
|
+
const tx = await createPublishTransaction(signer, options);
|
405
|
+
return signer.signTransaction(tx);
|
406
|
+
}
|
407
|
+
|
408
|
+
/**
|
409
|
+
* Creates a complete transaction for appending content to a CKBFS file
|
410
|
+
* @param signer The signer to use for the transaction
|
411
|
+
* @param options Options for appending content
|
412
|
+
* @returns Promise resolving to the signed transaction
|
413
|
+
*/
|
414
|
+
export async function appendCKBFS(
|
415
|
+
signer: Signer,
|
416
|
+
options: AppendOptions
|
417
|
+
): Promise<Transaction> {
|
418
|
+
const tx = await createAppendTransaction(signer, options);
|
419
|
+
return signer.signTransaction(tx);
|
420
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import { CKBFS_HEADER } from './molecule';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Utility functions for creating and handling CKBFS witnesses
|
5
|
+
*/
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Creates a CKBFS witness with content
|
9
|
+
* @param content The content to include in the witness
|
10
|
+
* @param version Optional version byte (default is 0)
|
11
|
+
* @returns Uint8Array containing the witness data
|
12
|
+
*/
|
13
|
+
export function createCKBFSWitness(content: Uint8Array, version: number = 0): Uint8Array {
|
14
|
+
// Create witness with CKBFS header, version byte, and content
|
15
|
+
const versionByte = new Uint8Array([version]);
|
16
|
+
return Buffer.concat([CKBFS_HEADER, versionByte, content]);
|
17
|
+
}
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Creates a CKBFS witness with text content
|
21
|
+
* @param text The text content to include in the witness
|
22
|
+
* @param version Optional version byte (default is 0)
|
23
|
+
* @returns Uint8Array containing the witness data
|
24
|
+
*/
|
25
|
+
export function createTextCKBFSWitness(text: string, version: number = 0): Uint8Array {
|
26
|
+
const textEncoder = new TextEncoder();
|
27
|
+
const contentBytes = textEncoder.encode(text);
|
28
|
+
return createCKBFSWitness(contentBytes, version);
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* Extracts content from a CKBFS witness
|
33
|
+
* @param witness The CKBFS witness data
|
34
|
+
* @returns Object containing the extracted version and content bytes
|
35
|
+
*/
|
36
|
+
export function extractCKBFSWitnessContent(witness: Uint8Array): { version: number; content: Uint8Array } {
|
37
|
+
// Ensure the witness has the CKBFS header
|
38
|
+
const header = witness.slice(0, 5);
|
39
|
+
const headerString = new TextDecoder().decode(header);
|
40
|
+
|
41
|
+
if (headerString !== 'CKBFS') {
|
42
|
+
throw new Error('Invalid CKBFS witness: missing CKBFS header');
|
43
|
+
}
|
44
|
+
|
45
|
+
// Extract version byte and content
|
46
|
+
const version = witness[5];
|
47
|
+
const content = witness.slice(6);
|
48
|
+
|
49
|
+
return { version, content };
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Checks if a witness is a valid CKBFS witness
|
54
|
+
* @param witness The witness data to check
|
55
|
+
* @returns Boolean indicating whether the witness is a valid CKBFS witness
|
56
|
+
*/
|
57
|
+
export function isCKBFSWitness(witness: Uint8Array): boolean {
|
58
|
+
if (witness.length < 6) {
|
59
|
+
return false;
|
60
|
+
}
|
61
|
+
|
62
|
+
const header = witness.slice(0, 5);
|
63
|
+
const headerString = new TextDecoder().decode(header);
|
64
|
+
|
65
|
+
return headerString === 'CKBFS';
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Creates an array of witnesses for a CKBFS transaction from content chunks
|
70
|
+
* @param contentChunks Array of content chunks
|
71
|
+
* @param version Optional version byte (default is 0)
|
72
|
+
* @returns Array of Uint8Array witnesses
|
73
|
+
*/
|
74
|
+
export function createChunkedCKBFSWitnesses(contentChunks: Uint8Array[], version: number = 0): Uint8Array[] {
|
75
|
+
return contentChunks.map(chunk => createCKBFSWitness(chunk, version));
|
76
|
+
}
|
package/tsconfig.json
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "es2020",
|
4
|
+
"module": "commonjs",
|
5
|
+
"declaration": true,
|
6
|
+
"outDir": "./dist",
|
7
|
+
"strict": true,
|
8
|
+
"esModuleInterop": true,
|
9
|
+
"skipLibCheck": true,
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
11
|
+
"resolveJsonModule": true
|
12
|
+
},
|
13
|
+
"include": ["src/**/*"],
|
14
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
15
|
+
}
|