@btc-vision/cli 1.0.9 → 1.0.10
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/build/lib/ipfs.d.ts +4 -1
- package/build/lib/ipfs.js +257 -56
- package/package.json +1 -1
package/build/lib/ipfs.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export declare function pinToIPFS(data: Buffer, name?: string): Promise<PinResul
|
|
|
10
10
|
export declare function fetchFromIPFS(cid: string): Promise<FetchResult>;
|
|
11
11
|
export declare function buildGatewayUrl(gateway: string, cid: string): string;
|
|
12
12
|
export declare function isValidCid(cid: string): boolean;
|
|
13
|
+
export declare function isCIDv0(cid: string): boolean;
|
|
14
|
+
export declare function isCIDv1(cid: string): boolean;
|
|
15
|
+
export declare function cidV0toV1(cidv0: string): string;
|
|
13
16
|
export declare function downloadPlugin(cid: string, outputPath: string): Promise<number>;
|
|
14
17
|
export declare function uploadPlugin(filePath: string): Promise<PinResult>;
|
|
15
18
|
export interface DirectoryPinResult {
|
|
@@ -17,5 +20,5 @@ export interface DirectoryPinResult {
|
|
|
17
20
|
files: number;
|
|
18
21
|
totalSize: number;
|
|
19
22
|
}
|
|
20
|
-
export declare function uploadDirectory(dirPath: string,
|
|
23
|
+
export declare function uploadDirectory(dirPath: string, _wrapWithDirectory?: boolean): Promise<DirectoryPinResult>;
|
|
21
24
|
export declare function uploadFile(filePath: string): Promise<PinResult>;
|
package/build/lib/ipfs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as https from 'https';
|
|
2
2
|
import * as http from 'http';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
+
import ora from 'ora';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
const DEFAULT_MAX_REDIRECTS = 10;
|
|
6
7
|
async function httpRequest(url, options, redirectCount = 0) {
|
|
@@ -68,9 +69,37 @@ async function httpRequest(url, options, redirectCount = 0) {
|
|
|
68
69
|
reject(new Error('Request timeout'));
|
|
69
70
|
});
|
|
70
71
|
if (options.body) {
|
|
71
|
-
|
|
72
|
+
const bodyBuffer = Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body);
|
|
73
|
+
const totalBytes = bodyBuffer.length;
|
|
74
|
+
const chunkSize = 64 * 1024;
|
|
75
|
+
let bytesSent = 0;
|
|
76
|
+
const writeChunk = () => {
|
|
77
|
+
while (bytesSent < totalBytes) {
|
|
78
|
+
const end = Math.min(bytesSent + chunkSize, totalBytes);
|
|
79
|
+
const chunk = bodyBuffer.subarray(bytesSent, end);
|
|
80
|
+
const canContinue = req.write(chunk);
|
|
81
|
+
bytesSent += chunk.length;
|
|
82
|
+
if (options.onProgress) {
|
|
83
|
+
options.onProgress(bytesSent, totalBytes);
|
|
84
|
+
}
|
|
85
|
+
if (!canContinue) {
|
|
86
|
+
req.once('drain', writeChunk);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (options.onUploadComplete) {
|
|
91
|
+
options.onUploadComplete();
|
|
92
|
+
}
|
|
93
|
+
req.end();
|
|
94
|
+
};
|
|
95
|
+
writeChunk();
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (options.onUploadComplete) {
|
|
99
|
+
options.onUploadComplete();
|
|
100
|
+
}
|
|
101
|
+
req.end();
|
|
72
102
|
}
|
|
73
|
-
req.end();
|
|
74
103
|
});
|
|
75
104
|
}
|
|
76
105
|
export async function pinToIPFS(data, name) {
|
|
@@ -156,6 +185,9 @@ export async function pinToIPFS(data, name) {
|
|
|
156
185
|
if (!cid) {
|
|
157
186
|
throw new Error(`Failed to extract CID from pinning response: ${JSON.stringify(result)}`);
|
|
158
187
|
}
|
|
188
|
+
if (isCIDv0(cid)) {
|
|
189
|
+
cid = cidV0toV1(cid);
|
|
190
|
+
}
|
|
159
191
|
return {
|
|
160
192
|
cid,
|
|
161
193
|
size: data.length,
|
|
@@ -219,6 +251,73 @@ export function isValidCid(cid) {
|
|
|
219
251
|
}
|
|
220
252
|
return false;
|
|
221
253
|
}
|
|
254
|
+
export function isCIDv0(cid) {
|
|
255
|
+
return cid.startsWith('Qm') && cid.length === 46;
|
|
256
|
+
}
|
|
257
|
+
export function isCIDv1(cid) {
|
|
258
|
+
return cid.startsWith('baf') && cid.length >= 50;
|
|
259
|
+
}
|
|
260
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
261
|
+
const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
262
|
+
function decodeBase58(str) {
|
|
263
|
+
const bytes = [];
|
|
264
|
+
for (const char of str) {
|
|
265
|
+
const value = BASE58_ALPHABET.indexOf(char);
|
|
266
|
+
if (value === -1) {
|
|
267
|
+
throw new Error(`Invalid base58 character: ${char}`);
|
|
268
|
+
}
|
|
269
|
+
let carry = value;
|
|
270
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
271
|
+
carry += bytes[i] * 58;
|
|
272
|
+
bytes[i] = carry & 0xff;
|
|
273
|
+
carry >>= 8;
|
|
274
|
+
}
|
|
275
|
+
while (carry > 0) {
|
|
276
|
+
bytes.push(carry & 0xff);
|
|
277
|
+
carry >>= 8;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const char of str) {
|
|
281
|
+
if (char === '1') {
|
|
282
|
+
bytes.push(0);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return new Uint8Array(bytes.reverse());
|
|
289
|
+
}
|
|
290
|
+
function encodeBase32(bytes) {
|
|
291
|
+
let result = '';
|
|
292
|
+
let bits = 0;
|
|
293
|
+
let value = 0;
|
|
294
|
+
for (const byte of bytes) {
|
|
295
|
+
value = (value << 8) | byte;
|
|
296
|
+
bits += 8;
|
|
297
|
+
while (bits >= 5) {
|
|
298
|
+
bits -= 5;
|
|
299
|
+
result += BASE32_ALPHABET[(value >> bits) & 0x1f];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (bits > 0) {
|
|
303
|
+
result += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
export function cidV0toV1(cidv0) {
|
|
308
|
+
if (!isCIDv0(cidv0)) {
|
|
309
|
+
return cidv0;
|
|
310
|
+
}
|
|
311
|
+
const multihash = decodeBase58(cidv0);
|
|
312
|
+
if (multihash[0] !== 0x12 || multihash[1] !== 0x20 || multihash.length !== 34) {
|
|
313
|
+
throw new Error('Invalid CIDv0: not a valid sha2-256 multihash');
|
|
314
|
+
}
|
|
315
|
+
const cidv1Bytes = new Uint8Array(2 + multihash.length);
|
|
316
|
+
cidv1Bytes[0] = 0x01;
|
|
317
|
+
cidv1Bytes[1] = 0x70;
|
|
318
|
+
cidv1Bytes.set(multihash, 2);
|
|
319
|
+
return 'b' + encodeBase32(cidv1Bytes);
|
|
320
|
+
}
|
|
222
321
|
export async function downloadPlugin(cid, outputPath) {
|
|
223
322
|
const result = await fetchFromIPFS(cid);
|
|
224
323
|
fs.writeFileSync(outputPath, result.data);
|
|
@@ -272,74 +371,176 @@ function getMimeType(filePath) {
|
|
|
272
371
|
};
|
|
273
372
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
274
373
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
374
|
+
function generateSessionId() {
|
|
375
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
376
|
+
const r = (Math.random() * 16) | 0;
|
|
377
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
378
|
+
return v.toString(16);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function sleep(ms) {
|
|
382
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
383
|
+
}
|
|
384
|
+
async function mfsCall(endpoint, apiPath, params, body, headers, maxRetries = 3) {
|
|
385
|
+
const url = new URL(endpoint);
|
|
386
|
+
url.pathname = apiPath;
|
|
387
|
+
for (const [key, value] of Object.entries(params)) {
|
|
388
|
+
url.searchParams.set(key, value);
|
|
389
|
+
}
|
|
390
|
+
const reqHeaders = {
|
|
391
|
+
...headers,
|
|
392
|
+
};
|
|
393
|
+
let formBody;
|
|
394
|
+
if (body) {
|
|
286
395
|
const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
|
|
287
396
|
const formParts = [];
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
const url = new URL(endpoint);
|
|
309
|
-
if (wrapWithDirectory) {
|
|
310
|
-
url.searchParams.set('wrap-with-directory', 'true');
|
|
397
|
+
formParts.push(Buffer.from(`--${boundary}\r\n` +
|
|
398
|
+
`Content-Disposition: form-data; name="file"\r\n` +
|
|
399
|
+
`Content-Type: application/octet-stream\r\n\r\n`));
|
|
400
|
+
formParts.push(body);
|
|
401
|
+
formParts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
402
|
+
formBody = Buffer.concat(formParts);
|
|
403
|
+
reqHeaders['Content-Type'] = `multipart/form-data; boundary=${boundary}`;
|
|
404
|
+
reqHeaders['Content-Length'] = formBody.length.toString();
|
|
405
|
+
}
|
|
406
|
+
let lastError;
|
|
407
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
408
|
+
try {
|
|
409
|
+
return await httpRequest(url.toString(), {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: reqHeaders,
|
|
412
|
+
body: formBody,
|
|
413
|
+
timeout: body ? 120000 : 60000,
|
|
414
|
+
followRedirect: true,
|
|
415
|
+
});
|
|
311
416
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
timeout: 300000,
|
|
318
|
-
followRedirect: true,
|
|
319
|
-
});
|
|
320
|
-
const lines = response.toString().trim().split('\n');
|
|
321
|
-
let rootCid;
|
|
322
|
-
for (const line of lines) {
|
|
323
|
-
if (!line.trim())
|
|
417
|
+
catch (e) {
|
|
418
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
419
|
+
if (lastError.message.includes('429')) {
|
|
420
|
+
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
421
|
+
await sleep(backoffMs);
|
|
324
422
|
continue;
|
|
325
|
-
const result = JSON.parse(line);
|
|
326
|
-
if (result.Hash && (result.Name === '' || !result.Name)) {
|
|
327
|
-
rootCid = result.Hash;
|
|
328
423
|
}
|
|
329
|
-
|
|
330
|
-
|
|
424
|
+
throw lastError;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
throw lastError || new Error('Max retries exceeded');
|
|
428
|
+
}
|
|
429
|
+
export async function uploadDirectory(dirPath, _wrapWithDirectory = true) {
|
|
430
|
+
const config = loadConfig();
|
|
431
|
+
const endpoint = config.ipfsPinningEndpoint;
|
|
432
|
+
if (!endpoint) {
|
|
433
|
+
throw new Error('IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`');
|
|
434
|
+
}
|
|
435
|
+
const baseUrl = endpoint.replace(/\/api\/v0\/add\/?$/, '');
|
|
436
|
+
const files = getAllFiles(dirPath);
|
|
437
|
+
if (files.length === 0) {
|
|
438
|
+
throw new Error('Directory is empty');
|
|
439
|
+
}
|
|
440
|
+
let totalSize = 0;
|
|
441
|
+
for (const file of files) {
|
|
442
|
+
const stat = fs.statSync(file.fullPath);
|
|
443
|
+
totalSize += stat.size;
|
|
444
|
+
}
|
|
445
|
+
const sessionId = generateSessionId();
|
|
446
|
+
const mfsPath = `/uploads/${sessionId}`;
|
|
447
|
+
const headers = {};
|
|
448
|
+
if (config.ipfsPinningApiKey) {
|
|
449
|
+
headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
|
|
450
|
+
}
|
|
451
|
+
const formatBytes = (bytes) => {
|
|
452
|
+
if (bytes < 1024)
|
|
453
|
+
return `${bytes} B`;
|
|
454
|
+
if (bytes < 1024 * 1024)
|
|
455
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
456
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
457
|
+
};
|
|
458
|
+
let uploadedBytes = 0;
|
|
459
|
+
let spinner = null;
|
|
460
|
+
try {
|
|
461
|
+
await mfsCall(baseUrl, '/api/v0/files/mkdir', {
|
|
462
|
+
arg: mfsPath,
|
|
463
|
+
parents: 'true',
|
|
464
|
+
}, undefined, headers);
|
|
465
|
+
for (let i = 0; i < files.length; i++) {
|
|
466
|
+
const file = files[i];
|
|
467
|
+
const data = fs.readFileSync(file.fullPath);
|
|
468
|
+
const fileMfsPath = `${mfsPath}/${file.path}`;
|
|
469
|
+
const parentDir = fileMfsPath.substring(0, fileMfsPath.lastIndexOf('/'));
|
|
470
|
+
if (parentDir !== mfsPath) {
|
|
471
|
+
await mfsCall(baseUrl, '/api/v0/files/mkdir', {
|
|
472
|
+
arg: parentDir,
|
|
473
|
+
parents: 'true',
|
|
474
|
+
}, undefined, headers);
|
|
331
475
|
}
|
|
476
|
+
await mfsCall(baseUrl, '/api/v0/files/write', {
|
|
477
|
+
arg: fileMfsPath,
|
|
478
|
+
create: 'true',
|
|
479
|
+
parents: 'true',
|
|
480
|
+
truncate: 'true',
|
|
481
|
+
}, data, headers);
|
|
482
|
+
uploadedBytes += data.length;
|
|
483
|
+
const percent = Math.round((uploadedBytes / totalSize) * 100);
|
|
484
|
+
const barWidth = 30;
|
|
485
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
486
|
+
const empty = barWidth - filled;
|
|
487
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
488
|
+
process.stdout.write(`\r Uploading: [${bar}] ${percent}% - ${i + 1}/${files.length} files (${formatBytes(uploadedBytes)}/${formatBytes(totalSize)})`);
|
|
332
489
|
}
|
|
333
|
-
|
|
334
|
-
|
|
490
|
+
process.stdout.write('\n');
|
|
491
|
+
spinner = ora({
|
|
492
|
+
text: 'Getting directory CID...',
|
|
493
|
+
spinner: 'dots',
|
|
494
|
+
}).start();
|
|
495
|
+
const statResponse = await mfsCall(baseUrl, '/api/v0/files/stat', {
|
|
496
|
+
arg: mfsPath,
|
|
497
|
+
hash: 'true',
|
|
498
|
+
'cid-base': 'base32',
|
|
499
|
+
}, undefined, headers);
|
|
500
|
+
const statResult = JSON.parse(statResponse.toString());
|
|
501
|
+
let cid = statResult.Hash;
|
|
502
|
+
if (!cid) {
|
|
503
|
+
spinner.fail('Failed to get directory CID');
|
|
504
|
+
throw new Error('MFS stat did not return a Hash');
|
|
505
|
+
}
|
|
506
|
+
if (isCIDv0(cid)) {
|
|
507
|
+
cid = cidV0toV1(cid);
|
|
508
|
+
}
|
|
509
|
+
spinner.text = 'Pinning content...';
|
|
510
|
+
try {
|
|
511
|
+
await mfsCall(baseUrl, '/api/v0/pin/add', {
|
|
512
|
+
arg: cid,
|
|
513
|
+
}, undefined, headers);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
}
|
|
517
|
+
spinner.succeed(`Upload complete: ${cid}`);
|
|
518
|
+
try {
|
|
519
|
+
await mfsCall(baseUrl, '/api/v0/files/rm', {
|
|
520
|
+
arg: mfsPath,
|
|
521
|
+
recursive: 'true',
|
|
522
|
+
}, undefined, headers);
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
335
525
|
}
|
|
336
526
|
return {
|
|
337
|
-
cid
|
|
527
|
+
cid,
|
|
338
528
|
files: files.length,
|
|
339
529
|
totalSize,
|
|
340
530
|
};
|
|
341
531
|
}
|
|
342
532
|
catch (e) {
|
|
533
|
+
if (spinner) {
|
|
534
|
+
spinner.fail('Upload failed');
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
await mfsCall(baseUrl, '/api/v0/files/rm', {
|
|
538
|
+
arg: mfsPath,
|
|
539
|
+
recursive: 'true',
|
|
540
|
+
}, undefined, headers);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
}
|
|
343
544
|
throw new Error(`IPFS directory upload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
344
545
|
}
|
|
345
546
|
}
|