@btc-vision/cli 1.0.4 → 1.0.6
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/package.json +7 -4
- package/.gitattributes +0 -2
- package/.github/dependabot.yml +0 -9
- package/.github/workflows/ci.yml +0 -48
- package/.prettierrc.json +0 -12
- package/CONTRIBUTING.md +0 -56
- package/NOTICE +0 -17
- package/SECURITY.md +0 -35
- package/eslint.config.js +0 -41
- package/gulpfile.js +0 -41
- package/src/commands/AcceptCommand.ts +0 -224
- package/src/commands/BaseCommand.ts +0 -59
- package/src/commands/CompileCommand.ts +0 -195
- package/src/commands/ConfigCommand.ts +0 -117
- package/src/commands/DeprecateCommand.ts +0 -193
- package/src/commands/InfoCommand.ts +0 -293
- package/src/commands/InitCommand.ts +0 -541
- package/src/commands/InstallCommand.ts +0 -179
- package/src/commands/KeygenCommand.ts +0 -157
- package/src/commands/ListCommand.ts +0 -169
- package/src/commands/LoginCommand.ts +0 -197
- package/src/commands/LogoutCommand.ts +0 -76
- package/src/commands/PublishCommand.ts +0 -340
- package/src/commands/ScopeRegisterCommand.ts +0 -164
- package/src/commands/SearchCommand.ts +0 -140
- package/src/commands/SignCommand.ts +0 -110
- package/src/commands/TransferCommand.ts +0 -363
- package/src/commands/UndeprecateCommand.ts +0 -167
- package/src/commands/UpdateCommand.ts +0 -200
- package/src/commands/VerifyCommand.ts +0 -228
- package/src/commands/WhoamiCommand.ts +0 -113
- package/src/index.ts +0 -88
- package/src/lib/PackageRegistry.abi.json +0 -765
- package/src/lib/PackageRegistry.abi.ts +0 -365
- package/src/lib/binary.ts +0 -338
- package/src/lib/config.ts +0 -265
- package/src/lib/credentials.ts +0 -176
- package/src/lib/ipfs.ts +0 -382
- package/src/lib/manifest.ts +0 -195
- package/src/lib/provider.ts +0 -121
- package/src/lib/registry.ts +0 -467
- package/src/lib/transaction.ts +0 -205
- package/src/lib/wallet.ts +0 -262
- package/src/types/PackageRegistry.ts +0 -344
- package/src/types/index.ts +0 -147
- package/tsconfig.json +0 -25
package/src/lib/ipfs.ts
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IPFS operations for OPNet CLI
|
|
3
|
-
*
|
|
4
|
-
* Handles pinning and fetching plugin binaries from IPFS.
|
|
5
|
-
*
|
|
6
|
-
* @module lib/ipfs
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as https from 'https';
|
|
10
|
-
import * as http from 'http';
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
import { loadConfig } from './config.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* IPFS pinning result
|
|
16
|
-
*/
|
|
17
|
-
export interface PinResult {
|
|
18
|
-
cid: string;
|
|
19
|
-
size: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* IPFS gateway fetch result
|
|
24
|
-
*/
|
|
25
|
-
export interface FetchResult {
|
|
26
|
-
data: Buffer;
|
|
27
|
-
size: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* HTTP request options
|
|
32
|
-
*/
|
|
33
|
-
interface RequestOptions {
|
|
34
|
-
method: string;
|
|
35
|
-
headers?: Record<string, string>;
|
|
36
|
-
body?: Buffer | string;
|
|
37
|
-
timeout?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Make an HTTP/HTTPS request
|
|
42
|
-
*
|
|
43
|
-
* @param url - The URL to request
|
|
44
|
-
* @param options - Request options
|
|
45
|
-
* @returns Response body buffer
|
|
46
|
-
*/
|
|
47
|
-
async function httpRequest(url: string, options: RequestOptions): Promise<Buffer> {
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
const parsedUrl = new URL(url);
|
|
50
|
-
const isHttps = parsedUrl.protocol === 'https:';
|
|
51
|
-
const lib = isHttps ? https : http;
|
|
52
|
-
|
|
53
|
-
const reqOptions: https.RequestOptions = {
|
|
54
|
-
hostname: parsedUrl.hostname,
|
|
55
|
-
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
56
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
57
|
-
method: options.method,
|
|
58
|
-
headers: options.headers || {},
|
|
59
|
-
timeout: options.timeout || 30000,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const req = lib.request(reqOptions, (res) => {
|
|
63
|
-
const chunks: Buffer[] = [];
|
|
64
|
-
|
|
65
|
-
res.on('data', (chunk: Buffer) => {
|
|
66
|
-
chunks.push(chunk);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
res.on('end', () => {
|
|
70
|
-
const body = Buffer.concat(chunks);
|
|
71
|
-
|
|
72
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
73
|
-
resolve(body);
|
|
74
|
-
} else {
|
|
75
|
-
reject(
|
|
76
|
-
new Error(
|
|
77
|
-
`HTTP ${res.statusCode}: ${res.statusMessage} - ${body.toString()}`,
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
req.on('error', reject);
|
|
85
|
-
req.on('timeout', () => {
|
|
86
|
-
req.destroy();
|
|
87
|
-
reject(new Error('Request timeout'));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (options.body) {
|
|
91
|
-
req.write(options.body);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
req.end();
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Pin a file to IPFS using the configured pinning service
|
|
100
|
-
*
|
|
101
|
-
* @param data - The file data to pin
|
|
102
|
-
* @param name - Optional file name for metadata
|
|
103
|
-
* @returns The IPFS CID
|
|
104
|
-
*/
|
|
105
|
-
export async function pinToIPFS(data: Buffer, name?: string): Promise<PinResult> {
|
|
106
|
-
try {
|
|
107
|
-
const config = loadConfig();
|
|
108
|
-
const endpoint = config.ipfsPinningEndpoint;
|
|
109
|
-
|
|
110
|
-
if (!endpoint) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
'IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Build multipart form data
|
|
117
|
-
const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
|
|
118
|
-
const fileName = name || 'plugin.opnet';
|
|
119
|
-
|
|
120
|
-
const formParts: Buffer[] = [];
|
|
121
|
-
|
|
122
|
-
// File part
|
|
123
|
-
formParts.push(
|
|
124
|
-
Buffer.from(
|
|
125
|
-
`--${boundary}\r\n` +
|
|
126
|
-
`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
|
|
127
|
-
`Content-Type: application/octet-stream\r\n\r\n`,
|
|
128
|
-
),
|
|
129
|
-
);
|
|
130
|
-
formParts.push(data);
|
|
131
|
-
formParts.push(Buffer.from('\r\n'));
|
|
132
|
-
|
|
133
|
-
// End boundary
|
|
134
|
-
formParts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
135
|
-
|
|
136
|
-
const body = Buffer.concat(formParts);
|
|
137
|
-
|
|
138
|
-
// Build headers
|
|
139
|
-
const headers: Record<string, string> = {
|
|
140
|
-
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
141
|
-
'Content-Length': body.length.toString(),
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
// Add authorization if configured
|
|
145
|
-
if (config.ipfsPinningAuthHeader) {
|
|
146
|
-
const [headerName, headerValue] = config.ipfsPinningAuthHeader
|
|
147
|
-
.split(':')
|
|
148
|
-
.map((s) => s.trim());
|
|
149
|
-
if (headerName && headerValue) {
|
|
150
|
-
headers[headerName] = headerValue;
|
|
151
|
-
}
|
|
152
|
-
} else if (config.ipfsPinningApiKey) {
|
|
153
|
-
headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Detect pinning service type from URL and adjust request
|
|
157
|
-
const url = new URL(endpoint);
|
|
158
|
-
|
|
159
|
-
let requestUrl: string;
|
|
160
|
-
if (url.hostname.includes('ipfs.opnet.org')) {
|
|
161
|
-
// OPNet IPFS gateway - uses standard IPFS API
|
|
162
|
-
requestUrl = endpoint;
|
|
163
|
-
} else if (url.hostname.includes('pinata')) {
|
|
164
|
-
// Pinata-specific endpoint
|
|
165
|
-
requestUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
|
|
166
|
-
// Only set pinata_api_key header if using API key (not JWT)
|
|
167
|
-
// JWT tokens start with "eyJ", API keys don't
|
|
168
|
-
if (config.ipfsPinningApiKey && !config.ipfsPinningApiKey.startsWith('eyJ')) {
|
|
169
|
-
headers['pinata_api_key'] = config.ipfsPinningApiKey;
|
|
170
|
-
if (config.ipfsPinningSecret) {
|
|
171
|
-
headers['pinata_secret_api_key'] = config.ipfsPinningSecret;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
} else if (url.hostname.includes('web3.storage') || url.hostname.includes('w3s.link')) {
|
|
175
|
-
// web3.storage endpoint
|
|
176
|
-
requestUrl = endpoint.endsWith('/') ? endpoint + 'upload' : endpoint + '/upload';
|
|
177
|
-
} else if (url.hostname.includes('nft.storage')) {
|
|
178
|
-
// nft.storage endpoint
|
|
179
|
-
requestUrl = 'https://api.nft.storage/upload';
|
|
180
|
-
} else if (url.pathname.includes('/api/v0/')) {
|
|
181
|
-
// Standard IPFS API endpoint
|
|
182
|
-
requestUrl = endpoint;
|
|
183
|
-
} else {
|
|
184
|
-
// Generic IPFS pinning service (assumed to follow IPFS Pinning Services API)
|
|
185
|
-
requestUrl = endpoint.endsWith('/') ? endpoint + 'pins' : endpoint + '/pins';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const response = await httpRequest(requestUrl, {
|
|
189
|
-
method: 'POST',
|
|
190
|
-
headers,
|
|
191
|
-
body,
|
|
192
|
-
timeout: 120000, // 2 minutes for upload
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// Parse response to extract CID
|
|
196
|
-
const result = JSON.parse(response.toString()) as Record<string, unknown>;
|
|
197
|
-
|
|
198
|
-
// Handle different response formats
|
|
199
|
-
let cid: string | undefined;
|
|
200
|
-
|
|
201
|
-
if (typeof result.IpfsHash === 'string') {
|
|
202
|
-
// Pinata format
|
|
203
|
-
cid = result.IpfsHash;
|
|
204
|
-
} else if (typeof result.cid === 'string') {
|
|
205
|
-
// web3.storage / nft.storage format
|
|
206
|
-
cid = result.cid;
|
|
207
|
-
} else if (typeof result.Hash === 'string') {
|
|
208
|
-
// IPFS API format
|
|
209
|
-
cid = result.Hash;
|
|
210
|
-
} else if (
|
|
211
|
-
result.value &&
|
|
212
|
-
typeof (result.value as Record<string, unknown>).cid === 'string'
|
|
213
|
-
) {
|
|
214
|
-
// NFT.storage wrapped format
|
|
215
|
-
cid = (result.value as Record<string, unknown>).cid as string;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!cid) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
`Failed to extract CID from pinning response: ${JSON.stringify(result)}`,
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
cid,
|
|
226
|
-
size: data.length,
|
|
227
|
-
};
|
|
228
|
-
} catch (e) {
|
|
229
|
-
throw new Error(`IPFS pinning failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Fetch a file from IPFS using configured gateways
|
|
235
|
-
*
|
|
236
|
-
* @param cid - The IPFS CID
|
|
237
|
-
* @returns The file data
|
|
238
|
-
*/
|
|
239
|
-
export async function fetchFromIPFS(cid: string): Promise<FetchResult> {
|
|
240
|
-
const config = loadConfig();
|
|
241
|
-
const gateways = config.ipfsGateways.length > 0 ? config.ipfsGateways : [config.ipfsGateway];
|
|
242
|
-
|
|
243
|
-
let lastError: Error | undefined;
|
|
244
|
-
|
|
245
|
-
for (const gateway of gateways) {
|
|
246
|
-
try {
|
|
247
|
-
const url = buildGatewayUrl(gateway, cid);
|
|
248
|
-
const data = await httpRequest(url, {
|
|
249
|
-
method: 'GET',
|
|
250
|
-
timeout: 60000, // 1 minute
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
data,
|
|
255
|
-
size: data.length,
|
|
256
|
-
};
|
|
257
|
-
} catch (error) {
|
|
258
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
259
|
-
// Try next gateway
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
throw new Error(`Failed to fetch from all gateways: ${lastError?.message}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Build a gateway URL for a CID
|
|
268
|
-
*
|
|
269
|
-
* @param gateway - The gateway base URL
|
|
270
|
-
* @param cid - The IPFS CID
|
|
271
|
-
* @returns Full URL
|
|
272
|
-
*/
|
|
273
|
-
export function buildGatewayUrl(gateway: string, cid: string): string {
|
|
274
|
-
// Remove trailing slash
|
|
275
|
-
const base = gateway.replace(/\/$/, '');
|
|
276
|
-
|
|
277
|
-
// Handle different gateway URL patterns
|
|
278
|
-
if (base.includes('{cid}')) {
|
|
279
|
-
// Template URL
|
|
280
|
-
return base.replace('{cid}', cid);
|
|
281
|
-
} else if (base.includes('/ipfs/')) {
|
|
282
|
-
// Path-style gateway
|
|
283
|
-
return `${base}${cid}`;
|
|
284
|
-
} else {
|
|
285
|
-
// Standard gateway
|
|
286
|
-
return `${base}/ipfs/${cid}`;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Validate an IPFS CID
|
|
292
|
-
*
|
|
293
|
-
* @param cid - The CID to validate
|
|
294
|
-
* @returns True if valid CID format
|
|
295
|
-
*/
|
|
296
|
-
export function isValidCid(cid: string): boolean {
|
|
297
|
-
// CIDv0 (Qm...) or CIDv1 (ba...)
|
|
298
|
-
if (cid.startsWith('Qm') && cid.length === 46) {
|
|
299
|
-
// CIDv0 base58btc
|
|
300
|
-
return /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/.test(cid);
|
|
301
|
-
} else if (cid.startsWith('ba')) {
|
|
302
|
-
// CIDv1 base32
|
|
303
|
-
return /^ba[a-z2-7]{57,}$/.test(cid);
|
|
304
|
-
} else if (cid.startsWith('b')) {
|
|
305
|
-
// Other CIDv1 bases
|
|
306
|
-
return cid.length >= 50;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Download a plugin binary from IPFS and save to file
|
|
314
|
-
*
|
|
315
|
-
* @param cid - The IPFS CID
|
|
316
|
-
* @param outputPath - Path to save the file
|
|
317
|
-
* @returns The downloaded file size
|
|
318
|
-
*/
|
|
319
|
-
export async function downloadPlugin(cid: string, outputPath: string): Promise<number> {
|
|
320
|
-
const result = await fetchFromIPFS(cid);
|
|
321
|
-
fs.writeFileSync(outputPath, result.data);
|
|
322
|
-
return result.size;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Upload a plugin binary file to IPFS
|
|
327
|
-
*
|
|
328
|
-
* @param filePath - Path to the plugin file
|
|
329
|
-
* @returns The IPFS CID
|
|
330
|
-
*/
|
|
331
|
-
export async function uploadPlugin(filePath: string): Promise<PinResult> {
|
|
332
|
-
const data = fs.readFileSync(filePath);
|
|
333
|
-
const fileName = filePath.split('/').pop() || 'plugin.opnet';
|
|
334
|
-
return pinToIPFS(data, fileName);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Check if a CID is available on any gateway
|
|
339
|
-
*
|
|
340
|
-
* @param cid - The IPFS CID
|
|
341
|
-
* @returns True if the file is accessible
|
|
342
|
-
*/
|
|
343
|
-
export async function checkAvailability(cid: string): Promise<boolean> {
|
|
344
|
-
try {
|
|
345
|
-
await fetchFromIPFS(cid);
|
|
346
|
-
return true;
|
|
347
|
-
} catch {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Get IPFS gateway status
|
|
354
|
-
*
|
|
355
|
-
* @returns Object with gateway availability status
|
|
356
|
-
*/
|
|
357
|
-
export async function getGatewayStatus(): Promise<Record<string, boolean>> {
|
|
358
|
-
const config = loadConfig();
|
|
359
|
-
const gateways = config.ipfsGateways.length > 0 ? config.ipfsGateways : [config.ipfsGateway];
|
|
360
|
-
|
|
361
|
-
// Use a well-known CID for testing (empty directory)
|
|
362
|
-
const testCid = 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn';
|
|
363
|
-
|
|
364
|
-
const status: Record<string, boolean> = {};
|
|
365
|
-
|
|
366
|
-
await Promise.all(
|
|
367
|
-
gateways.map(async (gateway) => {
|
|
368
|
-
try {
|
|
369
|
-
const url = buildGatewayUrl(gateway, testCid);
|
|
370
|
-
await httpRequest(url, {
|
|
371
|
-
method: 'HEAD',
|
|
372
|
-
timeout: 10000,
|
|
373
|
-
});
|
|
374
|
-
status[gateway] = true;
|
|
375
|
-
} catch {
|
|
376
|
-
status[gateway] = false;
|
|
377
|
-
}
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
return status;
|
|
382
|
-
}
|
package/src/lib/manifest.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin manifest (plugin.json) utilities
|
|
3
|
-
*
|
|
4
|
-
* Uses @btc-vision/plugin-sdk for validation.
|
|
5
|
-
*
|
|
6
|
-
* @module lib/manifest
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as fs from 'fs';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_LIFECYCLE,
|
|
13
|
-
DEFAULT_PERMISSIONS,
|
|
14
|
-
DEFAULT_RESOURCES,
|
|
15
|
-
IPluginMetadata,
|
|
16
|
-
IPluginPermissions,
|
|
17
|
-
IValidationResult,
|
|
18
|
-
PLUGIN_MANIFEST_FILENAME,
|
|
19
|
-
PLUGIN_NAME_REGEX,
|
|
20
|
-
validateManifest as sdkValidateManifest,
|
|
21
|
-
} from '@btc-vision/plugin-sdk';
|
|
22
|
-
|
|
23
|
-
/** Scoped package name pattern */
|
|
24
|
-
const SCOPED_NAME_PATTERN = /^@[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Validate a plugin name (scoped or unscoped)
|
|
28
|
-
*
|
|
29
|
-
* @param name - The name to validate
|
|
30
|
-
* @returns Array of validation errors (empty if valid)
|
|
31
|
-
*/
|
|
32
|
-
export function validatePluginName(name: string): string[] {
|
|
33
|
-
const errors: string[] = [];
|
|
34
|
-
|
|
35
|
-
if (!name) {
|
|
36
|
-
errors.push('Name is required');
|
|
37
|
-
return errors;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if scoped
|
|
41
|
-
if (name.startsWith('@')) {
|
|
42
|
-
if (!SCOPED_NAME_PATTERN.test(name)) {
|
|
43
|
-
errors.push(
|
|
44
|
-
'Scoped name must be @scope/name where scope and name are lowercase alphanumeric with hyphens, starting with a letter',
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
if (!PLUGIN_NAME_REGEX.test(name)) {
|
|
49
|
-
errors.push('Name must be lowercase alphanumeric with hyphens, starting with a letter');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check length
|
|
54
|
-
if (name.length > 100) {
|
|
55
|
-
errors.push('Name must be 100 characters or less');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return errors;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validate a complete plugin manifest using the SDK validator
|
|
63
|
-
*
|
|
64
|
-
* @param manifest - The manifest to validate
|
|
65
|
-
* @returns Validation result with field-specific errors
|
|
66
|
-
*/
|
|
67
|
-
export function validateManifest(manifest: Partial<IPluginMetadata>): IValidationResult {
|
|
68
|
-
return sdkValidateManifest(manifest);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Validate plugin permissions
|
|
73
|
-
*
|
|
74
|
-
* @param permissions - The permissions object
|
|
75
|
-
* @returns Array of validation errors (empty if valid)
|
|
76
|
-
*/
|
|
77
|
-
export function validatePermissions(permissions: IPluginPermissions): string[] {
|
|
78
|
-
const errors: string[] = [];
|
|
79
|
-
|
|
80
|
-
if (permissions.database?.enabled) {
|
|
81
|
-
if (!permissions.database.collections || !Array.isArray(permissions.database.collections)) {
|
|
82
|
-
errors.push('database.collections must be an array when database is enabled');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return errors;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Load and validate a plugin manifest from file
|
|
91
|
-
*
|
|
92
|
-
* Note: This performs basic validation suitable for source manifests.
|
|
93
|
-
* The SDK's strict validation requires fields like 'checksum' which
|
|
94
|
-
* are only computed during compilation, not in source plugin.json.
|
|
95
|
-
*
|
|
96
|
-
* @param manifestPath - Path to plugin.json
|
|
97
|
-
* @returns The loaded manifest or throws with validation errors
|
|
98
|
-
*/
|
|
99
|
-
export function loadManifest(manifestPath: string): IPluginMetadata {
|
|
100
|
-
const resolvedPath = path.resolve(manifestPath);
|
|
101
|
-
|
|
102
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
103
|
-
throw new Error(`Manifest not found: ${resolvedPath}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let manifest: Partial<IPluginMetadata>;
|
|
107
|
-
try {
|
|
108
|
-
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
109
|
-
manifest = JSON.parse(content) as Partial<IPluginMetadata>;
|
|
110
|
-
} catch (e) {
|
|
111
|
-
throw new Error(`Failed to parse manifest: ${e instanceof Error ? e.message : String(e)}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Basic validation for source manifests (checksum is computed during compilation)
|
|
115
|
-
const errors: string[] = [];
|
|
116
|
-
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
117
|
-
errors.push('name is required');
|
|
118
|
-
}
|
|
119
|
-
if (!manifest.version || typeof manifest.version !== 'string') {
|
|
120
|
-
errors.push('version is required');
|
|
121
|
-
}
|
|
122
|
-
if (!manifest.author || typeof manifest.author !== 'object') {
|
|
123
|
-
errors.push('author is required');
|
|
124
|
-
}
|
|
125
|
-
if (!manifest.pluginType || !['standalone', 'library'].includes(manifest.pluginType)) {
|
|
126
|
-
errors.push('pluginType must be "standalone" or "library"');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (errors.length > 0) {
|
|
130
|
-
throw new Error(`Invalid manifest:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Set default checksum for compilation (will be computed)
|
|
134
|
-
// Cast to mutable to allow setting checksum
|
|
135
|
-
const result = manifest as { -readonly [K in keyof IPluginMetadata]: IPluginMetadata[K] };
|
|
136
|
-
if (!result.checksum) {
|
|
137
|
-
result.checksum = '';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return result as IPluginMetadata;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Save a plugin manifest to file
|
|
145
|
-
*
|
|
146
|
-
* @param manifestPath - Path to save to
|
|
147
|
-
* @param manifest - The manifest to save
|
|
148
|
-
*/
|
|
149
|
-
export function saveManifest(manifestPath: string, manifest: IPluginMetadata): void {
|
|
150
|
-
const content = JSON.stringify(manifest, null, 4);
|
|
151
|
-
fs.writeFileSync(manifestPath, content, 'utf-8');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Create a default plugin manifest
|
|
156
|
-
*
|
|
157
|
-
* @param options - Manifest options
|
|
158
|
-
* @returns A complete manifest with defaults
|
|
159
|
-
*/
|
|
160
|
-
export function createManifest(options: {
|
|
161
|
-
name: string;
|
|
162
|
-
author: string;
|
|
163
|
-
email?: string;
|
|
164
|
-
description?: string;
|
|
165
|
-
pluginType: 'standalone' | 'library';
|
|
166
|
-
}): Omit<IPluginMetadata, 'checksum'> {
|
|
167
|
-
return {
|
|
168
|
-
name: options.name,
|
|
169
|
-
version: '1.0.0',
|
|
170
|
-
opnetVersion: '>=0.0.1',
|
|
171
|
-
main: 'dist/index.jsc',
|
|
172
|
-
target: 'bytenode',
|
|
173
|
-
type: 'plugin',
|
|
174
|
-
author: {
|
|
175
|
-
name: options.author,
|
|
176
|
-
email: options.email,
|
|
177
|
-
},
|
|
178
|
-
description: options.description,
|
|
179
|
-
pluginType: options.pluginType,
|
|
180
|
-
permissions: DEFAULT_PERMISSIONS,
|
|
181
|
-
resources: DEFAULT_RESOURCES,
|
|
182
|
-
lifecycle: DEFAULT_LIFECYCLE,
|
|
183
|
-
dependencies: {},
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Get the manifest path for a directory
|
|
189
|
-
*
|
|
190
|
-
* @param dir - Directory to search
|
|
191
|
-
* @returns Path to plugin.json
|
|
192
|
-
*/
|
|
193
|
-
export function getManifestPath(dir: string = process.cwd()): string {
|
|
194
|
-
return path.join(dir, PLUGIN_MANIFEST_FILENAME);
|
|
195
|
-
}
|
package/src/lib/provider.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSONRpcProvider wrapper for OPNet CLI
|
|
3
|
-
*
|
|
4
|
-
* Provides a unified interface for blockchain interaction using the opnet package.
|
|
5
|
-
*
|
|
6
|
-
* @module lib/provider
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { JSONRpcProvider } from 'opnet';
|
|
10
|
-
|
|
11
|
-
import { NetworkName } from '../types/index.js';
|
|
12
|
-
import { getRegistryAddress, getRpcUrl, loadConfig } from './config.js';
|
|
13
|
-
import { getNetwork } from './wallet.js';
|
|
14
|
-
|
|
15
|
-
/** Provider timeout in milliseconds */
|
|
16
|
-
const DEFAULT_TIMEOUT = 30000;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Provider instance cache
|
|
20
|
-
*/
|
|
21
|
-
const providerCache = new Map<string, JSONRpcProvider>();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get a JSONRpcProvider for a network
|
|
25
|
-
*
|
|
26
|
-
* @param network - Network name (defaults to configured default)
|
|
27
|
-
* @returns JSONRpcProvider instance
|
|
28
|
-
*/
|
|
29
|
-
export function getProvider(network?: NetworkName): JSONRpcProvider {
|
|
30
|
-
const config = loadConfig();
|
|
31
|
-
const targetNetwork = network || config.defaultNetwork;
|
|
32
|
-
const rpcUrl = getRpcUrl(targetNetwork);
|
|
33
|
-
const cacheKey = `${targetNetwork}:${rpcUrl}`;
|
|
34
|
-
|
|
35
|
-
// Return cached provider if available
|
|
36
|
-
const cached = providerCache.get(cacheKey);
|
|
37
|
-
if (cached) {
|
|
38
|
-
return cached;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Create new provider
|
|
42
|
-
const bitcoinNetwork = getNetwork(targetNetwork);
|
|
43
|
-
const provider = new JSONRpcProvider(rpcUrl, bitcoinNetwork, DEFAULT_TIMEOUT);
|
|
44
|
-
|
|
45
|
-
providerCache.set(cacheKey, provider);
|
|
46
|
-
return provider;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Clear provider cache (useful for testing or network changes)
|
|
51
|
-
*/
|
|
52
|
-
export function clearProviderCache(): void {
|
|
53
|
-
providerCache.clear();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get the registry contract address for a network
|
|
58
|
-
*
|
|
59
|
-
* @param network - Network name (defaults to configured default)
|
|
60
|
-
* @returns The registry contract address
|
|
61
|
-
*/
|
|
62
|
-
export function getRegistryContractAddress(network?: NetworkName): string {
|
|
63
|
-
return getRegistryAddress(network);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Check if the provider can connect to the RPC endpoint
|
|
68
|
-
*
|
|
69
|
-
* @param network - Network name (defaults to configured default)
|
|
70
|
-
* @returns True if connection successful
|
|
71
|
-
*/
|
|
72
|
-
export async function checkConnection(network?: NetworkName): Promise<boolean> {
|
|
73
|
-
try {
|
|
74
|
-
const provider = getProvider(network);
|
|
75
|
-
// Try to get the current block to verify connection
|
|
76
|
-
await provider.getBlockNumber();
|
|
77
|
-
return true;
|
|
78
|
-
} catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get the current block number
|
|
85
|
-
*
|
|
86
|
-
* @param network - Network name (defaults to configured default)
|
|
87
|
-
* @returns Current block number
|
|
88
|
-
*/
|
|
89
|
-
export async function getBlockNumber(network?: NetworkName): Promise<bigint> {
|
|
90
|
-
const provider = getProvider(network);
|
|
91
|
-
return provider.getBlockNumber();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get the balance for an address
|
|
96
|
-
*
|
|
97
|
-
* @param address - The address to check
|
|
98
|
-
* @param network - Network name (defaults to configured default)
|
|
99
|
-
* @returns Balance in satoshis
|
|
100
|
-
*/
|
|
101
|
-
export async function getBalance(address: string, network?: NetworkName): Promise<bigint> {
|
|
102
|
-
const provider = getProvider(network);
|
|
103
|
-
return provider.getBalance(address);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Broadcast a signed transaction
|
|
108
|
-
*
|
|
109
|
-
* @param rawTx - The raw transaction hex
|
|
110
|
-
* @param network - Network name (defaults to configured default)
|
|
111
|
-
* @returns Transaction hash
|
|
112
|
-
*/
|
|
113
|
-
export async function broadcastTransaction(
|
|
114
|
-
rawTx: string,
|
|
115
|
-
network?: NetworkName,
|
|
116
|
-
isPsbt: boolean = false,
|
|
117
|
-
): Promise<string> {
|
|
118
|
-
const provider = getProvider(network);
|
|
119
|
-
const result = await provider.sendRawTransaction(rawTx, isPsbt);
|
|
120
|
-
return result.result ?? '';
|
|
121
|
-
}
|