@ckbfs/api 1.2.4 → 1.3.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/dist/index.d.ts +46 -4
- package/dist/index.js +117 -9
- package/dist/utils/transaction.d.ts +6 -0
- package/dist/utils/transaction.js +1 -0
- package/example.txt +1 -1
- package/examples/append.ts +39 -2
- package/examples/publish.ts +35 -1
- package/package.json +1 -1
- package/src/index.ts +173 -22
- package/src/utils/transaction.ts +1 -1
- package/.cursor/rules/typescript.mdc +0 -49
- package/ABC.LOGS +0 -1
- package/README.md +0 -138
- package/RFC.v2.md +0 -341
- package/append.txt +0 -1
- package/examples/retrieve.ts +0 -115
- package/publish-tx-hash-v1.txt +0 -1
- package/test-download.txt +0 -2
package/src/index.ts
CHANGED
@@ -7,10 +7,10 @@ import {
|
|
7
7
|
} from './utils/checksum';
|
8
8
|
import {
|
9
9
|
createCKBFSCell,
|
10
|
-
createPublishTransaction,
|
11
|
-
createAppendTransaction,
|
12
|
-
publishCKBFS,
|
13
|
-
appendCKBFS,
|
10
|
+
createPublishTransaction as utilCreatePublishTransaction,
|
11
|
+
createAppendTransaction as utilCreateAppendTransaction,
|
12
|
+
publishCKBFS as utilPublishCKBFS,
|
13
|
+
appendCKBFS as utilAppendCKBFS,
|
14
14
|
CKBFSCellOptions,
|
15
15
|
PublishOptions,
|
16
16
|
AppendOptions
|
@@ -56,6 +56,10 @@ import {
|
|
56
56
|
getCKBFSScriptConfig,
|
57
57
|
CKBFSScriptConfig
|
58
58
|
} from './utils/constants';
|
59
|
+
import { ensureHexPrefix } from './utils/transaction';
|
60
|
+
|
61
|
+
// Helper to encode string to Uint8Array
|
62
|
+
const textEncoder = new TextEncoder();
|
59
63
|
|
60
64
|
/**
|
61
65
|
* Custom options for file publishing and appending
|
@@ -70,6 +74,19 @@ export interface FileOptions {
|
|
70
74
|
useTypeID?: boolean;
|
71
75
|
}
|
72
76
|
|
77
|
+
/**
|
78
|
+
* Options required when publishing content directly (string or Uint8Array)
|
79
|
+
*/
|
80
|
+
export type PublishContentOptions = Omit<FileOptions, 'capacity' | 'contentType' | 'filename'> &
|
81
|
+
Required<Pick<FileOptions, 'contentType' | 'filename'>> &
|
82
|
+
{ capacity?: bigint };
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Options required when appending content directly (string or Uint8Array)
|
86
|
+
*/
|
87
|
+
export type AppendContentOptions = Omit<FileOptions, 'contentType' | 'filename' | 'capacity'> &
|
88
|
+
{ capacity?: bigint };
|
89
|
+
|
73
90
|
/**
|
74
91
|
* Configuration options for the CKBFS SDK
|
75
92
|
*/
|
@@ -176,8 +193,8 @@ export class CKBFS {
|
|
176
193
|
const pathParts = filePath.split(/[\\\/]/);
|
177
194
|
const filename = options.filename || pathParts[pathParts.length - 1];
|
178
195
|
|
179
|
-
// Create and sign the transaction
|
180
|
-
const tx = await
|
196
|
+
// Create and sign the transaction using the utility function
|
197
|
+
const tx = await utilPublishCKBFS(this.signer, {
|
181
198
|
contentChunks,
|
182
199
|
contentType,
|
183
200
|
filename,
|
@@ -189,16 +206,53 @@ export class CKBFS {
|
|
189
206
|
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
|
190
207
|
});
|
191
208
|
|
192
|
-
console.log('tx', tx.stringify());
|
209
|
+
console.log('Publish file tx:', tx.stringify());
|
193
210
|
|
194
211
|
// Send the transaction
|
195
212
|
const txHash = await this.signer.sendTransaction(tx);
|
196
213
|
|
197
|
-
return txHash;
|
214
|
+
return ensureHexPrefix(txHash);
|
215
|
+
}
|
216
|
+
|
217
|
+
/**
|
218
|
+
* Publishes content (string or Uint8Array) directly to CKBFS
|
219
|
+
* @param content The content string or byte array to publish
|
220
|
+
* @param options Options for publishing the content (contentType and filename are required)
|
221
|
+
* @returns Promise resolving to the transaction hash
|
222
|
+
*/
|
223
|
+
async publishContent(content: string | Uint8Array, options: PublishContentOptions): Promise<string> {
|
224
|
+
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
|
225
|
+
const contentChunks = [];
|
226
|
+
|
227
|
+
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
|
228
|
+
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
|
229
|
+
}
|
230
|
+
|
231
|
+
const lock = await this.getLock();
|
232
|
+
|
233
|
+
// Use provided contentType and filename (required)
|
234
|
+
const { contentType, filename } = options;
|
235
|
+
|
236
|
+
const tx = await utilPublishCKBFS(this.signer, {
|
237
|
+
contentChunks,
|
238
|
+
contentType,
|
239
|
+
filename,
|
240
|
+
lock,
|
241
|
+
capacity: options.capacity,
|
242
|
+
feeRate: options.feeRate,
|
243
|
+
network: options.network || this.network,
|
244
|
+
version: options.version || this.version,
|
245
|
+
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
|
246
|
+
});
|
247
|
+
|
248
|
+
console.log('Publish content tx:', tx.stringify());
|
249
|
+
|
250
|
+
const txHash = await this.signer.sendTransaction(tx);
|
251
|
+
return ensureHexPrefix(txHash);
|
198
252
|
}
|
199
253
|
|
200
254
|
/**
|
201
|
-
* Appends content to an existing CKBFS file
|
255
|
+
* Appends content from a file to an existing CKBFS file
|
202
256
|
* @param filePath The path to the file containing the content to append
|
203
257
|
* @param ckbfsCell The CKBFS cell to append to
|
204
258
|
* @param options Additional options for the append operation
|
@@ -217,19 +271,55 @@ export class CKBFS {
|
|
217
271
|
contentChunks.push(fileContent.slice(i, i + this.chunkSize));
|
218
272
|
}
|
219
273
|
|
220
|
-
// Create and sign the transaction
|
221
|
-
const tx = await
|
274
|
+
// Create and sign the transaction using the utility function
|
275
|
+
const tx = await utilAppendCKBFS(this.signer, {
|
222
276
|
ckbfsCell,
|
223
277
|
contentChunks,
|
224
278
|
feeRate: options.feeRate,
|
225
279
|
network: options.network || this.network,
|
226
280
|
version: options.version || this.version
|
227
281
|
});
|
282
|
+
|
283
|
+
console.log('Append file tx:', tx.stringify());
|
228
284
|
|
229
285
|
// Send the transaction
|
230
286
|
const txHash = await this.signer.sendTransaction(tx);
|
231
287
|
|
232
|
-
return txHash;
|
288
|
+
return ensureHexPrefix(txHash);
|
289
|
+
}
|
290
|
+
|
291
|
+
/**
|
292
|
+
* Appends content (string or Uint8Array) directly to an existing CKBFS file
|
293
|
+
* @param content The content string or byte array to append
|
294
|
+
* @param ckbfsCell The CKBFS cell to append to
|
295
|
+
* @param options Additional options for the append operation
|
296
|
+
* @returns Promise resolving to the transaction hash
|
297
|
+
*/
|
298
|
+
async appendContent(
|
299
|
+
content: string | Uint8Array,
|
300
|
+
ckbfsCell: AppendOptions['ckbfsCell'],
|
301
|
+
options: AppendContentOptions = {}
|
302
|
+
): Promise<string> {
|
303
|
+
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
|
304
|
+
const contentChunks = [];
|
305
|
+
|
306
|
+
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
|
307
|
+
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
|
308
|
+
}
|
309
|
+
|
310
|
+
const tx = await utilAppendCKBFS(this.signer, {
|
311
|
+
ckbfsCell,
|
312
|
+
contentChunks,
|
313
|
+
feeRate: options.feeRate,
|
314
|
+
network: options.network || this.network,
|
315
|
+
version: options.version || this.version
|
316
|
+
// No useTypeID option for append
|
317
|
+
});
|
318
|
+
|
319
|
+
console.log('Append content tx:', tx.stringify());
|
320
|
+
|
321
|
+
const txHash = await this.signer.sendTransaction(tx);
|
322
|
+
return ensureHexPrefix(txHash);
|
233
323
|
}
|
234
324
|
|
235
325
|
/**
|
@@ -257,8 +347,40 @@ export class CKBFS {
|
|
257
347
|
const pathParts = filePath.split(/[\\\/]/);
|
258
348
|
const filename = options.filename || pathParts[pathParts.length - 1];
|
259
349
|
|
260
|
-
// Create the transaction
|
261
|
-
return
|
350
|
+
// Create the transaction using the utility function
|
351
|
+
return utilCreatePublishTransaction(this.signer, {
|
352
|
+
contentChunks,
|
353
|
+
contentType,
|
354
|
+
filename,
|
355
|
+
lock,
|
356
|
+
capacity: options.capacity,
|
357
|
+
feeRate: options.feeRate,
|
358
|
+
network: options.network || this.network,
|
359
|
+
version: options.version || this.version,
|
360
|
+
useTypeID: options.useTypeID !== undefined ? options.useTypeID : this.useTypeID
|
361
|
+
});
|
362
|
+
}
|
363
|
+
|
364
|
+
/**
|
365
|
+
* Creates a new transaction for publishing content (string or Uint8Array) directly, but doesn't sign or send it
|
366
|
+
* @param content The content string or byte array to publish
|
367
|
+
* @param options Options for publishing the content (contentType and filename are required)
|
368
|
+
* @returns Promise resolving to the unsigned transaction
|
369
|
+
*/
|
370
|
+
async createPublishContentTransaction(content: string | Uint8Array, options: PublishContentOptions): Promise<Transaction> {
|
371
|
+
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
|
372
|
+
const contentChunks = [];
|
373
|
+
|
374
|
+
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
|
375
|
+
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
|
376
|
+
}
|
377
|
+
|
378
|
+
const lock = await this.getLock();
|
379
|
+
|
380
|
+
// Use provided contentType and filename (required)
|
381
|
+
const { contentType, filename } = options;
|
382
|
+
|
383
|
+
return utilCreatePublishTransaction(this.signer, {
|
262
384
|
contentChunks,
|
263
385
|
contentType,
|
264
386
|
filename,
|
@@ -272,7 +394,7 @@ export class CKBFS {
|
|
272
394
|
}
|
273
395
|
|
274
396
|
/**
|
275
|
-
* Creates a new transaction for appending content but doesn't sign or send it
|
397
|
+
* Creates a new transaction for appending content from a file but doesn't sign or send it
|
276
398
|
* @param filePath The path to the file containing the content to append
|
277
399
|
* @param ckbfsCell The CKBFS cell to append to
|
278
400
|
* @param options Additional options for the append operation
|
@@ -291,13 +413,42 @@ export class CKBFS {
|
|
291
413
|
contentChunks.push(fileContent.slice(i, i + this.chunkSize));
|
292
414
|
}
|
293
415
|
|
294
|
-
// Create the transaction
|
295
|
-
return
|
416
|
+
// Create the transaction using the utility function
|
417
|
+
return utilCreateAppendTransaction(this.signer, {
|
418
|
+
ckbfsCell,
|
419
|
+
contentChunks,
|
420
|
+
feeRate: options.feeRate,
|
421
|
+
network: options.network || this.network,
|
422
|
+
version: options.version || this.version
|
423
|
+
});
|
424
|
+
}
|
425
|
+
|
426
|
+
/**
|
427
|
+
* Creates a new transaction for appending content (string or Uint8Array) directly, but doesn't sign or send it
|
428
|
+
* @param content The content string or byte array to append
|
429
|
+
* @param ckbfsCell The CKBFS cell to append to
|
430
|
+
* @param options Additional options for the append operation
|
431
|
+
* @returns Promise resolving to the unsigned transaction
|
432
|
+
*/
|
433
|
+
async createAppendContentTransaction(
|
434
|
+
content: string | Uint8Array,
|
435
|
+
ckbfsCell: AppendOptions['ckbfsCell'],
|
436
|
+
options: AppendContentOptions = {}
|
437
|
+
): Promise<Transaction> {
|
438
|
+
const contentBytes = typeof content === 'string' ? textEncoder.encode(content) : content;
|
439
|
+
const contentChunks = [];
|
440
|
+
|
441
|
+
for (let i = 0; i < contentBytes.length; i += this.chunkSize) {
|
442
|
+
contentChunks.push(contentBytes.slice(i, i + this.chunkSize));
|
443
|
+
}
|
444
|
+
|
445
|
+
return utilCreateAppendTransaction(this.signer, {
|
296
446
|
ckbfsCell,
|
297
447
|
contentChunks,
|
298
448
|
feeRate: options.feeRate,
|
299
449
|
network: options.network || this.network,
|
300
450
|
version: options.version || this.version
|
451
|
+
// No useTypeID option for append
|
301
452
|
});
|
302
453
|
}
|
303
454
|
}
|
@@ -310,12 +461,12 @@ export {
|
|
310
461
|
updateChecksum,
|
311
462
|
verifyWitnessChecksum,
|
312
463
|
|
313
|
-
// Transaction utilities
|
464
|
+
// Transaction utilities (Exporting original names from transaction.ts)
|
314
465
|
createCKBFSCell,
|
315
|
-
createPublishTransaction,
|
316
|
-
createAppendTransaction,
|
317
|
-
publishCKBFS,
|
318
|
-
appendCKBFS,
|
466
|
+
utilCreatePublishTransaction as createPublishTransaction,
|
467
|
+
utilCreateAppendTransaction as createAppendTransaction,
|
468
|
+
utilPublishCKBFS as publishCKBFS,
|
469
|
+
utilAppendCKBFS as appendCKBFS,
|
319
470
|
|
320
471
|
// File utilities
|
321
472
|
readFile,
|
package/src/utils/transaction.ts
CHANGED
@@ -57,7 +57,7 @@ export interface AppendOptions {
|
|
57
57
|
* @param value The string to ensure is hex prefixed
|
58
58
|
* @returns A hex prefixed string
|
59
59
|
*/
|
60
|
-
function ensureHexPrefix(value: string): `0x${string}` {
|
60
|
+
export function ensureHexPrefix(value: string): `0x${string}` {
|
61
61
|
if (value.startsWith('0x')) {
|
62
62
|
return value as `0x${string}`;
|
63
63
|
}
|
@@ -1,49 +0,0 @@
|
|
1
|
-
---
|
2
|
-
description:
|
3
|
-
globs:
|
4
|
-
alwaysApply: true
|
5
|
-
---
|
6
|
-
|
7
|
-
You are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI and Tailwind.
|
8
|
-
|
9
|
-
Code Style and Structure
|
10
|
-
- Write concise, technical TypeScript code with accurate examples.
|
11
|
-
- Use functional and declarative programming patterns; avoid classes.
|
12
|
-
- Prefer iteration and modularization over code duplication.
|
13
|
-
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
|
14
|
-
- Structure files: exported component, subcomponents, helpers, static content, types.
|
15
|
-
|
16
|
-
Naming Conventions
|
17
|
-
- Use lowercase with dashes for directories (e.g., components/auth-wizard).
|
18
|
-
- Favor named exports for components.
|
19
|
-
|
20
|
-
TypeScript Usage
|
21
|
-
- Use TypeScript for all code; prefer interfaces over types.
|
22
|
-
- Avoid enums; use maps instead.
|
23
|
-
- Use functional components with TypeScript interfaces.
|
24
|
-
|
25
|
-
Syntax and Formatting
|
26
|
-
- Use the "function" keyword for pure functions.
|
27
|
-
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
|
28
|
-
- Use declarative JSX.
|
29
|
-
|
30
|
-
UI and Styling
|
31
|
-
- Use Shadcn UI, Radix, and Tailwind for components and styling.
|
32
|
-
- Implement responsive design with Tailwind CSS; use a mobile-first approach.
|
33
|
-
|
34
|
-
Performance Optimization
|
35
|
-
- Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC).
|
36
|
-
- Wrap client components in Suspense with fallback.
|
37
|
-
- Use dynamic loading for non-critical components.
|
38
|
-
- Optimize images: use WebP format, include size data, implement lazy loading.
|
39
|
-
|
40
|
-
Key Conventions
|
41
|
-
- Use 'nuqs' for URL search parameter state management.
|
42
|
-
- Optimize Web Vitals (LCP, CLS, FID).
|
43
|
-
- Limit 'use client':
|
44
|
-
- Favor server components and Next.js SSR.
|
45
|
-
- Use only for Web API access in small components.
|
46
|
-
- Avoid for data fetching or state management.
|
47
|
-
|
48
|
-
Follow Next.js docs for Data Fetching, Rendering, and Routing.
|
49
|
-
|
package/ABC.LOGS
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
MINT,TO:ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdpallzwnhe64rqnqmev7hf98yrmh4yzucgdw7qwGIVE_NAME,NEW_NAME:NERVAPE_COOKIE
|
package/README.md
DELETED
@@ -1,138 +0,0 @@
|
|
1
|
-
# CKBFS API SDK
|
2
|
-
|
3
|
-
A JavaScript/TypeScript SDK for the CKBFS (Cell Knowledge Base File System) protocol on Nervos CKB.
|
4
|
-
|
5
|
-
## Overview
|
6
|
-
|
7
|
-
CKBFS is a protocol designed to describe a witnesses-based file storage system on Nervos CKB Network. With CKBFS, you can:
|
8
|
-
|
9
|
-
- Store files on Nervos CKB Network
|
10
|
-
- Publish large files that may exceed block size limitation (~500kb), across multiple blocks
|
11
|
-
- Maintain simple indexing and retrieval
|
12
|
-
|
13
|
-
This SDK provides a simple way to interact with the CKBFS protocol, allowing you to:
|
14
|
-
|
15
|
-
- Read and write files to CKB
|
16
|
-
- Calculate and verify Adler32 checksums
|
17
|
-
- Create, manage, and update CKBFS cells
|
18
|
-
- Handle witnesses creation and verification
|
19
|
-
- Create and send CKB transactions for file operations
|
20
|
-
|
21
|
-
## Installation
|
22
|
-
|
23
|
-
```bash
|
24
|
-
npm install ckbfs-api
|
25
|
-
```
|
26
|
-
|
27
|
-
## Basic Usage
|
28
|
-
|
29
|
-
```typescript
|
30
|
-
import { CKBFS } from 'ckbfs-api';
|
31
|
-
|
32
|
-
// Initialize the SDK with your private key
|
33
|
-
const privateKey = process.env.CKB_PRIVATE_KEY;
|
34
|
-
const ckbfs = new CKBFS(privateKey);
|
35
|
-
|
36
|
-
// Publish a file to CKBFS
|
37
|
-
async function publishExample() {
|
38
|
-
const txHash = await ckbfs.publishFile('path/to/your/file.txt');
|
39
|
-
console.log(`File published with transaction: ${txHash}`);
|
40
|
-
}
|
41
|
-
|
42
|
-
// Append content to an existing CKBFS file
|
43
|
-
async function appendExample(ckbfsCell) {
|
44
|
-
const txHash = await ckbfs.appendFile('path/to/more/content.txt', ckbfsCell);
|
45
|
-
console.log(`Content appended with transaction: ${txHash}`);
|
46
|
-
}
|
47
|
-
```
|
48
|
-
|
49
|
-
## Advanced Usage
|
50
|
-
|
51
|
-
The SDK provides utility functions for more granular control:
|
52
|
-
|
53
|
-
### Working with Files
|
54
|
-
|
55
|
-
```typescript
|
56
|
-
import { readFileAsUint8Array, splitFileIntoChunks } from 'ckbfs-api';
|
57
|
-
|
58
|
-
// Read a file
|
59
|
-
const fileContent = readFileAsUint8Array('path/to/file.txt');
|
60
|
-
|
61
|
-
// Split a file into chunks (e.g., for large files)
|
62
|
-
const chunks = splitFileIntoChunks('path/to/large/file.bin', 30 * 1024); // 30KB chunks
|
63
|
-
```
|
64
|
-
|
65
|
-
### Checksum Operations
|
66
|
-
|
67
|
-
```typescript
|
68
|
-
import { calculateChecksum, verifyChecksum } from 'ckbfs-api';
|
69
|
-
|
70
|
-
// Calculate Adler32 checksum
|
71
|
-
const content = new TextEncoder().encode('Hello CKBFS');
|
72
|
-
const checksum = await calculateChecksum(content);
|
73
|
-
|
74
|
-
// Verify checksum
|
75
|
-
const isValid = await verifyChecksum(content, checksum);
|
76
|
-
```
|
77
|
-
|
78
|
-
### Transaction Utilities
|
79
|
-
|
80
|
-
```typescript
|
81
|
-
import { createPublishTransaction, createAppendTransaction } from 'ckbfs-api';
|
82
|
-
|
83
|
-
// Create a transaction without signing or sending
|
84
|
-
const tx = await ckbfs.createPublishTransaction('path/to/file.txt');
|
85
|
-
|
86
|
-
// Customize the transaction before signing
|
87
|
-
// ... modify tx ...
|
88
|
-
|
89
|
-
// Sign and send manually
|
90
|
-
const signedTx = await signer.signTransaction(tx);
|
91
|
-
const txHash = await client.sendTransaction(signedTx);
|
92
|
-
```
|
93
|
-
|
94
|
-
## Examples
|
95
|
-
|
96
|
-
The SDK comes with several examples demonstrating how to use it:
|
97
|
-
|
98
|
-
### Running Examples
|
99
|
-
|
100
|
-
```bash
|
101
|
-
# Show available examples
|
102
|
-
npm run example
|
103
|
-
|
104
|
-
# Run the publish file example
|
105
|
-
npm run example:publish
|
106
|
-
|
107
|
-
# Run the append file example (requires a transaction hash)
|
108
|
-
npm run example:append -- --txhash=0x123456...
|
109
|
-
# OR
|
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
|
116
|
-
```
|
117
|
-
|
118
|
-
### Example Files
|
119
|
-
|
120
|
-
- `examples/publish.ts` - Shows how to publish a file to CKBFS
|
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
|
123
|
-
|
124
|
-
To run the examples, first set your CKB private key:
|
125
|
-
|
126
|
-
```bash
|
127
|
-
export CKB_PRIVATE_KEY=your_private_key_here
|
128
|
-
```
|
129
|
-
|
130
|
-
Note: The append example requires a valid transaction hash from a previously published file. You can either provide it via command line argument or environment variable.
|
131
|
-
|
132
|
-
## Contributing
|
133
|
-
|
134
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
135
|
-
|
136
|
-
## License
|
137
|
-
|
138
|
-
This project is licensed under the MIT License - see the LICENSE file for details.
|