@btc-vision/cli 1.0.8 → 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/commands/DeprecateCommand.js +1 -1
- package/build/commands/DomainCommand.js +3 -3
- package/build/commands/InstallCommand.d.ts +2 -0
- package/build/commands/InstallCommand.js +29 -1
- package/build/commands/LoginCommand.js +4 -23
- package/build/commands/UndeprecateCommand.js +1 -1
- package/build/commands/WebsiteDeployCommand.js +3 -3
- package/build/commands/WebsitePublishCommand.js +2 -3
- package/build/lib/ipfs.d.ts +4 -1
- package/build/lib/ipfs.js +279 -63
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confirm, input } from '@inquirer/prompts';
|
|
2
2
|
import { BaseCommand } from './BaseCommand.js';
|
|
3
|
-
import { getPackage, getRegistryContract, getVersion, isVersionImmutable
|
|
3
|
+
import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
|
|
4
4
|
import { canSign, loadCredentials } from '../lib/credentials.js';
|
|
5
5
|
import { CLIWallet } from '../lib/wallet.js';
|
|
6
6
|
import { buildTransactionParams, checkBalance, formatSats, getWalletAddress, } from '../lib/transaction.js';
|
|
@@ -3,7 +3,7 @@ import { confirm } from '@inquirer/prompts';
|
|
|
3
3
|
import { Logger } from '@btc-vision/logger';
|
|
4
4
|
import { CLIWallet } from '../lib/wallet.js';
|
|
5
5
|
import { canSign, loadCredentials } from '../lib/credentials.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getResolverContract, getDomain, getContenthash, getContenthashTypeName, validateDomainName, getTreasuryAddress, getDomainPrice, parseDomainName, } from '../lib/resolver.js';
|
|
7
7
|
import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
|
|
8
8
|
import { TransactionOutputFlags } from 'opnet';
|
|
9
9
|
const logger = new Logger();
|
|
@@ -67,7 +67,6 @@ async function registerDomain(domain, options) {
|
|
|
67
67
|
logger.log(`Treasury: ${treasuryAddr}`);
|
|
68
68
|
logger.log(`Network: ${network}`);
|
|
69
69
|
logger.log(`Your wallet: ${wallet.p2trAddress}`);
|
|
70
|
-
logger.log(`MLDSA Public Key Hash: ${wallet.address.toHex()}`);
|
|
71
70
|
logger.log('');
|
|
72
71
|
if (options.dryRun) {
|
|
73
72
|
logger.warn('Dry run - no changes made.');
|
|
@@ -223,7 +222,8 @@ async function domainInfo(domain, options) {
|
|
|
223
222
|
process.exit(1);
|
|
224
223
|
}
|
|
225
224
|
}
|
|
226
|
-
const domainCommand = new Command('domain')
|
|
225
|
+
const domainCommand = new Command('domain')
|
|
226
|
+
.description('Manage .btc domains');
|
|
227
227
|
domainCommand
|
|
228
228
|
.command('register')
|
|
229
229
|
.description('Register a new .btc domain')
|
|
@@ -106,8 +106,10 @@ export class InstallCommand extends BaseCommand {
|
|
|
106
106
|
}
|
|
107
107
|
const outputDir = options?.output || path.join(process.cwd(), 'plugins');
|
|
108
108
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
109
|
-
const
|
|
109
|
+
const packageBaseName = packageName.replace(/^@/, '').replace(/\//g, '-');
|
|
110
|
+
const fileName = `${packageBaseName}-${version}.opnet`;
|
|
110
111
|
const outputPath = path.join(outputDir, fileName);
|
|
112
|
+
this.removeOldVersions(outputDir, packageBaseName, version);
|
|
111
113
|
this.logger.info('Saving plugin...');
|
|
112
114
|
fs.writeFileSync(outputPath, result.data);
|
|
113
115
|
this.logger.success('Plugin installed');
|
|
@@ -126,5 +128,31 @@ export class InstallCommand extends BaseCommand {
|
|
|
126
128
|
this.exitWithError(this.formatError(error));
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
removeOldVersions(outputDir, packageBaseName, newVersion) {
|
|
132
|
+
try {
|
|
133
|
+
const entries = fs.readdirSync(outputDir, { withFileTypes: true });
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (!entry.isFile() || !entry.name.endsWith('.opnet')) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const pattern = new RegExp(`^${this.escapeRegex(packageBaseName)}-(.+)\\.opnet$`);
|
|
139
|
+
const match = entry.name.match(pattern);
|
|
140
|
+
if (match) {
|
|
141
|
+
const oldVersion = match[1];
|
|
142
|
+
if (oldVersion !== newVersion) {
|
|
143
|
+
const oldFilePath = path.join(outputDir, entry.name);
|
|
144
|
+
this.logger.info(`Removing old version: ${entry.name}`);
|
|
145
|
+
fs.unlinkSync(oldFilePath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.logger.warn(`Could not clean up old versions: ${this.formatError(error)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
escapeRegex(str) {
|
|
155
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
156
|
+
}
|
|
129
157
|
}
|
|
130
158
|
export const installCommand = new InstallCommand().getCommand();
|
|
@@ -76,9 +76,9 @@ export class LoginCommand extends BaseCommand {
|
|
|
76
76
|
network: options.network,
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
return this.interactiveLogin(options.network
|
|
79
|
+
return this.interactiveLogin(options.network);
|
|
80
80
|
}
|
|
81
|
-
async interactiveLogin(defaultNetwork
|
|
81
|
+
async interactiveLogin(defaultNetwork) {
|
|
82
82
|
this.logger.info('OPNet Wallet Configuration\n');
|
|
83
83
|
const loginMethod = await select({
|
|
84
84
|
message: 'How would you like to authenticate?',
|
|
@@ -104,27 +104,8 @@ export class LoginCommand extends BaseCommand {
|
|
|
104
104
|
],
|
|
105
105
|
default: defaultNetwork,
|
|
106
106
|
}));
|
|
107
|
-
const selectedLevel =
|
|
108
|
-
|
|
109
|
-
choices: [
|
|
110
|
-
{
|
|
111
|
-
name: 'MLDSA-44 (Level 2, fastest)',
|
|
112
|
-
value: 44,
|
|
113
|
-
description: '1312 byte public key',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: 'MLDSA-65 (Level 3, balanced)',
|
|
117
|
-
value: 65,
|
|
118
|
-
description: '1952 byte public key',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'MLDSA-87 (Level 5, most secure)',
|
|
122
|
-
value: 87,
|
|
123
|
-
description: '2592 byte public key',
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
default: defaultLevel,
|
|
127
|
-
}));
|
|
107
|
+
const selectedLevel = 44;
|
|
108
|
+
this.logger.info('Using MLDSA-44 (only supported level on OPNet)');
|
|
128
109
|
if (loginMethod === 'mnemonic') {
|
|
129
110
|
const mnemonic = await password({
|
|
130
111
|
message: 'Enter your mnemonic phrase (12 or 24 words):',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts';
|
|
2
2
|
import { BaseCommand } from './BaseCommand.js';
|
|
3
|
-
import { getPackage, getRegistryContract, getVersion, isVersionImmutable
|
|
3
|
+
import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
|
|
4
4
|
import { canSign, loadCredentials } from '../lib/credentials.js';
|
|
5
5
|
import { CLIWallet } from '../lib/wallet.js';
|
|
6
6
|
import { buildTransactionParams, checkBalance, formatSats, getWalletAddress, } from '../lib/transaction.js';
|
|
@@ -6,7 +6,7 @@ import { CLIWallet } from '../lib/wallet.js';
|
|
|
6
6
|
import { canSign, loadCredentials } from '../lib/credentials.js';
|
|
7
7
|
import { uploadDirectory, uploadFile } from '../lib/ipfs.js';
|
|
8
8
|
import { formatFileSize } from '../lib/binary.js';
|
|
9
|
-
import {
|
|
9
|
+
import { getResolverContract, getDomain, getSubdomain, getContenthash, getContenthashTypeName, isSubdomain, parseDomainName, } from '../lib/resolver.js';
|
|
10
10
|
import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
|
|
11
11
|
export class WebsiteDeployCommand extends BaseCommand {
|
|
12
12
|
constructor() {
|
|
@@ -17,7 +17,7 @@ export class WebsiteDeployCommand extends BaseCommand {
|
|
|
17
17
|
.argument('<domain>', 'Domain name (e.g., mysite or mysite.btc)')
|
|
18
18
|
.argument('<path>', 'Path to website directory or HTML file')
|
|
19
19
|
.option('-n, --network <network>', 'Network to use', 'mainnet')
|
|
20
|
-
.option('--dry-run',
|
|
20
|
+
.option('--dry-run', 'Upload to IPFS but don\'t update on-chain')
|
|
21
21
|
.option('-y, --yes', 'Skip confirmation prompts')
|
|
22
22
|
.action((domain, websitePath, options) => this.execute(domain, websitePath, options || { network: 'mainnet' }));
|
|
23
23
|
}
|
|
@@ -84,7 +84,7 @@ export class WebsiteDeployCommand extends BaseCommand {
|
|
|
84
84
|
if (wallet.address.toHex() !== ownerAddress) {
|
|
85
85
|
this.logger.fail('You are not the owner of this domain');
|
|
86
86
|
this.logger.log(`Domain owner: ${ownerAddress}`);
|
|
87
|
-
this.logger.log(`Your address: ${wallet.
|
|
87
|
+
this.logger.log(`Your address: ${wallet.address.toHex()}`);
|
|
88
88
|
process.exit(1);
|
|
89
89
|
}
|
|
90
90
|
this.logger.success('Ownership verified');
|
|
@@ -2,7 +2,7 @@ import { confirm } from '@inquirer/prompts';
|
|
|
2
2
|
import { BaseCommand } from './BaseCommand.js';
|
|
3
3
|
import { CLIWallet } from '../lib/wallet.js';
|
|
4
4
|
import { canSign, loadCredentials } from '../lib/credentials.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getResolverContract, getDomain, getSubdomain, getContenthash, detectContenthashType, validateCIDv0, validateCIDv1, validateIPNS, getContenthashTypeName, isSubdomain, parseDomainName, } from '../lib/resolver.js';
|
|
6
6
|
import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
|
|
7
7
|
import { CONTENTHASH_TYPE_CIDv0, CONTENTHASH_TYPE_CIDv1, CONTENTHASH_TYPE_IPNS, CONTENTHASH_TYPE_SHA256, } from '../types/BtcResolver.js';
|
|
8
8
|
export class WebsitePublishCommand extends BaseCommand {
|
|
@@ -110,8 +110,7 @@ export class WebsitePublishCommand extends BaseCommand {
|
|
|
110
110
|
if (wallet.address.toHex() !== ownerAddress) {
|
|
111
111
|
this.logger.fail('You are not the owner of this domain');
|
|
112
112
|
this.logger.log(`Domain owner: ${ownerAddress}`);
|
|
113
|
-
this.logger.log(`Your address: ${wallet.
|
|
114
|
-
this.logger.log(`MLDSA Public Key Hash: ${wallet.address.toHex()}`);
|
|
113
|
+
this.logger.log(`Your address: ${wallet.address.toHex()}`);
|
|
115
114
|
process.exit(1);
|
|
116
115
|
}
|
|
117
116
|
this.logger.success('Ownership verified');
|
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) {
|
|
@@ -23,13 +24,17 @@ async function httpRequest(url, options, redirectCount = 0) {
|
|
|
23
24
|
};
|
|
24
25
|
const req = lib.request(reqOptions, (res) => {
|
|
25
26
|
const statusCode = res.statusCode ?? 0;
|
|
26
|
-
if (followRedirect && statusCode >= 300 && statusCode < 400
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
27
|
+
if (followRedirect && statusCode >= 300 && statusCode < 400) {
|
|
28
|
+
const locationHeader = res.headers.location || res.headers['Location'];
|
|
29
|
+
const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
|
|
30
|
+
if (location) {
|
|
31
|
+
const redirectUrl = new URL(location, url).href;
|
|
32
|
+
res.resume();
|
|
33
|
+
httpRequest(redirectUrl, options, redirectCount + 1)
|
|
34
|
+
.then(resolve)
|
|
35
|
+
.catch(reject);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
const chunks = [];
|
|
35
40
|
res.on('data', (chunk) => {
|
|
@@ -37,6 +42,17 @@ async function httpRequest(url, options, redirectCount = 0) {
|
|
|
37
42
|
});
|
|
38
43
|
res.on('end', () => {
|
|
39
44
|
const body = Buffer.concat(chunks);
|
|
45
|
+
if (followRedirect && statusCode >= 300 && statusCode < 400) {
|
|
46
|
+
const bodyStr = body.toString();
|
|
47
|
+
const hrefMatch = bodyStr.match(/href="([^"]+)"/);
|
|
48
|
+
if (hrefMatch && hrefMatch[1]) {
|
|
49
|
+
const redirectUrl = new URL(hrefMatch[1], url).href;
|
|
50
|
+
httpRequest(redirectUrl, options, redirectCount + 1)
|
|
51
|
+
.then(resolve)
|
|
52
|
+
.catch(reject);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
40
56
|
if (statusCode >= 200 && statusCode < 300) {
|
|
41
57
|
resolve(body);
|
|
42
58
|
}
|
|
@@ -53,9 +69,37 @@ async function httpRequest(url, options, redirectCount = 0) {
|
|
|
53
69
|
reject(new Error('Request timeout'));
|
|
54
70
|
});
|
|
55
71
|
if (options.body) {
|
|
56
|
-
|
|
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();
|
|
57
102
|
}
|
|
58
|
-
req.end();
|
|
59
103
|
});
|
|
60
104
|
}
|
|
61
105
|
export async function pinToIPFS(data, name) {
|
|
@@ -141,6 +185,9 @@ export async function pinToIPFS(data, name) {
|
|
|
141
185
|
if (!cid) {
|
|
142
186
|
throw new Error(`Failed to extract CID from pinning response: ${JSON.stringify(result)}`);
|
|
143
187
|
}
|
|
188
|
+
if (isCIDv0(cid)) {
|
|
189
|
+
cid = cidV0toV1(cid);
|
|
190
|
+
}
|
|
144
191
|
return {
|
|
145
192
|
cid,
|
|
146
193
|
size: data.length,
|
|
@@ -204,6 +251,73 @@ export function isValidCid(cid) {
|
|
|
204
251
|
}
|
|
205
252
|
return false;
|
|
206
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
|
+
}
|
|
207
321
|
export async function downloadPlugin(cid, outputPath) {
|
|
208
322
|
const result = await fetchFromIPFS(cid);
|
|
209
323
|
fs.writeFileSync(outputPath, result.data);
|
|
@@ -257,74 +371,176 @@ function getMimeType(filePath) {
|
|
|
257
371
|
};
|
|
258
372
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
259
373
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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) {
|
|
271
395
|
const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
|
|
272
396
|
const formParts = [];
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
const url = new URL(endpoint);
|
|
294
|
-
if (wrapWithDirectory) {
|
|
295
|
-
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
|
+
});
|
|
296
416
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
timeout: 300000,
|
|
303
|
-
followRedirect: true,
|
|
304
|
-
});
|
|
305
|
-
const lines = response.toString().trim().split('\n');
|
|
306
|
-
let rootCid;
|
|
307
|
-
for (const line of lines) {
|
|
308
|
-
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);
|
|
309
422
|
continue;
|
|
310
|
-
const result = JSON.parse(line);
|
|
311
|
-
if (result.Hash && (result.Name === '' || !result.Name)) {
|
|
312
|
-
rootCid = result.Hash;
|
|
313
423
|
}
|
|
314
|
-
|
|
315
|
-
|
|
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);
|
|
316
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)})`);
|
|
317
489
|
}
|
|
318
|
-
|
|
319
|
-
|
|
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 {
|
|
320
525
|
}
|
|
321
526
|
return {
|
|
322
|
-
cid
|
|
527
|
+
cid,
|
|
323
528
|
files: files.length,
|
|
324
529
|
totalSize,
|
|
325
530
|
};
|
|
326
531
|
}
|
|
327
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
|
+
}
|
|
328
544
|
throw new Error(`IPFS directory upload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
329
545
|
}
|
|
330
546
|
}
|