@btc-vision/cli 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.
Files changed (110) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/dependabot.yml +9 -0
  3. package/.github/workflows/ci.yml +48 -0
  4. package/.prettierrc.json +12 -0
  5. package/CONTRIBUTING.md +56 -0
  6. package/LICENSE +190 -0
  7. package/NOTICE +17 -0
  8. package/README.md +509 -0
  9. package/SECURITY.md +35 -0
  10. package/build/commands/AcceptCommand.d.ts +7 -0
  11. package/build/commands/AcceptCommand.js +110 -0
  12. package/build/commands/BaseCommand.d.ts +12 -0
  13. package/build/commands/BaseCommand.js +27 -0
  14. package/build/commands/CompileCommand.d.ts +7 -0
  15. package/build/commands/CompileCommand.js +138 -0
  16. package/build/commands/ConfigCommand.d.ts +17 -0
  17. package/build/commands/ConfigCommand.js +124 -0
  18. package/build/commands/DeprecateCommand.d.ts +7 -0
  19. package/build/commands/DeprecateCommand.js +112 -0
  20. package/build/commands/InfoCommand.d.ts +10 -0
  21. package/build/commands/InfoCommand.js +223 -0
  22. package/build/commands/InitCommand.d.ts +16 -0
  23. package/build/commands/InitCommand.js +336 -0
  24. package/build/commands/InstallCommand.d.ts +7 -0
  25. package/build/commands/InstallCommand.js +130 -0
  26. package/build/commands/KeygenCommand.d.ts +13 -0
  27. package/build/commands/KeygenCommand.js +133 -0
  28. package/build/commands/ListCommand.d.ts +7 -0
  29. package/build/commands/ListCommand.js +117 -0
  30. package/build/commands/LoginCommand.d.ts +9 -0
  31. package/build/commands/LoginCommand.js +139 -0
  32. package/build/commands/LogoutCommand.d.ts +7 -0
  33. package/build/commands/LogoutCommand.js +57 -0
  34. package/build/commands/PublishCommand.d.ts +7 -0
  35. package/build/commands/PublishCommand.js +163 -0
  36. package/build/commands/SearchCommand.d.ts +7 -0
  37. package/build/commands/SearchCommand.js +97 -0
  38. package/build/commands/SignCommand.d.ts +7 -0
  39. package/build/commands/SignCommand.js +80 -0
  40. package/build/commands/TransferCommand.d.ts +8 -0
  41. package/build/commands/TransferCommand.js +179 -0
  42. package/build/commands/UndeprecateCommand.d.ts +7 -0
  43. package/build/commands/UndeprecateCommand.js +95 -0
  44. package/build/commands/UpdateCommand.d.ts +7 -0
  45. package/build/commands/UpdateCommand.js +130 -0
  46. package/build/commands/VerifyCommand.d.ts +7 -0
  47. package/build/commands/VerifyCommand.js +167 -0
  48. package/build/commands/WhoamiCommand.d.ts +7 -0
  49. package/build/commands/WhoamiCommand.js +84 -0
  50. package/build/index.d.ts +2 -0
  51. package/build/index.js +64 -0
  52. package/build/lib/PackageRegistry.abi.d.ts +2 -0
  53. package/build/lib/PackageRegistry.abi.js +356 -0
  54. package/build/lib/binary.d.ts +16 -0
  55. package/build/lib/binary.js +165 -0
  56. package/build/lib/config.d.ts +11 -0
  57. package/build/lib/config.js +160 -0
  58. package/build/lib/credentials.d.ts +10 -0
  59. package/build/lib/credentials.js +89 -0
  60. package/build/lib/ipfs.d.ts +16 -0
  61. package/build/lib/ipfs.js +209 -0
  62. package/build/lib/manifest.d.ts +14 -0
  63. package/build/lib/manifest.js +88 -0
  64. package/build/lib/provider.d.ts +9 -0
  65. package/build/lib/provider.js +48 -0
  66. package/build/lib/registry.d.ts +58 -0
  67. package/build/lib/registry.js +197 -0
  68. package/build/lib/wallet.d.ts +32 -0
  69. package/build/lib/wallet.js +114 -0
  70. package/build/types/PackageRegistry.d.ts +177 -0
  71. package/build/types/PackageRegistry.js +1 -0
  72. package/build/types/index.d.ts +30 -0
  73. package/build/types/index.js +52 -0
  74. package/eslint.config.js +41 -0
  75. package/gulpfile.js +41 -0
  76. package/package.json +83 -0
  77. package/src/commands/AcceptCommand.ts +151 -0
  78. package/src/commands/BaseCommand.ts +59 -0
  79. package/src/commands/CompileCommand.ts +196 -0
  80. package/src/commands/ConfigCommand.ts +144 -0
  81. package/src/commands/DeprecateCommand.ts +156 -0
  82. package/src/commands/InfoCommand.ts +293 -0
  83. package/src/commands/InitCommand.ts +465 -0
  84. package/src/commands/InstallCommand.ts +179 -0
  85. package/src/commands/KeygenCommand.ts +157 -0
  86. package/src/commands/ListCommand.ts +169 -0
  87. package/src/commands/LoginCommand.ts +197 -0
  88. package/src/commands/LogoutCommand.ts +76 -0
  89. package/src/commands/PublishCommand.ts +230 -0
  90. package/src/commands/SearchCommand.ts +141 -0
  91. package/src/commands/SignCommand.ts +122 -0
  92. package/src/commands/TransferCommand.ts +235 -0
  93. package/src/commands/UndeprecateCommand.ts +134 -0
  94. package/src/commands/UpdateCommand.ts +200 -0
  95. package/src/commands/VerifyCommand.ts +228 -0
  96. package/src/commands/WhoamiCommand.ts +113 -0
  97. package/src/index.ts +86 -0
  98. package/src/lib/PackageRegistry.abi.json +765 -0
  99. package/src/lib/PackageRegistry.abi.ts +365 -0
  100. package/src/lib/binary.ts +336 -0
  101. package/src/lib/config.ts +265 -0
  102. package/src/lib/credentials.ts +176 -0
  103. package/src/lib/ipfs.ts +369 -0
  104. package/src/lib/manifest.ts +172 -0
  105. package/src/lib/provider.ts +121 -0
  106. package/src/lib/registry.ts +464 -0
  107. package/src/lib/wallet.ts +271 -0
  108. package/src/types/PackageRegistry.ts +344 -0
  109. package/src/types/index.ts +145 -0
  110. package/tsconfig.json +25 -0
@@ -0,0 +1,369 @@
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
+ const config = loadConfig();
107
+ const endpoint = config.ipfsPinningEndpoint;
108
+
109
+ if (!endpoint) {
110
+ throw new Error(
111
+ 'IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`',
112
+ );
113
+ }
114
+
115
+ // Build multipart form data
116
+ const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
117
+ const fileName = name || 'plugin.opnet';
118
+
119
+ const formParts: Buffer[] = [];
120
+
121
+ // File part
122
+ formParts.push(
123
+ Buffer.from(
124
+ `--${boundary}\r\n` +
125
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
126
+ `Content-Type: application/octet-stream\r\n\r\n`,
127
+ ),
128
+ );
129
+ formParts.push(data);
130
+ formParts.push(Buffer.from('\r\n'));
131
+
132
+ // End boundary
133
+ formParts.push(Buffer.from(`--${boundary}--\r\n`));
134
+
135
+ const body = Buffer.concat(formParts);
136
+
137
+ // Build headers
138
+ const headers: Record<string, string> = {
139
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
140
+ 'Content-Length': body.length.toString(),
141
+ };
142
+
143
+ // Add authorization if configured
144
+ if (config.ipfsPinningAuthHeader) {
145
+ const [headerName, headerValue] = config.ipfsPinningAuthHeader
146
+ .split(':')
147
+ .map((s) => s.trim());
148
+ if (headerName && headerValue) {
149
+ headers[headerName] = headerValue;
150
+ }
151
+ } else if (config.ipfsPinningApiKey) {
152
+ headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
153
+ }
154
+
155
+ // Detect pinning service type from URL and adjust request
156
+ const url = new URL(endpoint);
157
+
158
+ let requestUrl: string;
159
+ if (url.hostname.includes('ipfs.opnet.org')) {
160
+ // OPNet IPFS gateway - uses standard IPFS API
161
+ requestUrl = endpoint;
162
+ } else if (url.hostname.includes('pinata')) {
163
+ // Pinata-specific endpoint
164
+ requestUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
165
+ if (config.ipfsPinningApiKey) {
166
+ headers['pinata_api_key'] = config.ipfsPinningApiKey;
167
+ }
168
+ } else if (url.hostname.includes('web3.storage') || url.hostname.includes('w3s.link')) {
169
+ // web3.storage endpoint
170
+ requestUrl = endpoint.endsWith('/') ? endpoint + 'upload' : endpoint + '/upload';
171
+ } else if (url.hostname.includes('nft.storage')) {
172
+ // nft.storage endpoint
173
+ requestUrl = 'https://api.nft.storage/upload';
174
+ } else if (url.pathname.includes('/api/v0/')) {
175
+ // Standard IPFS API endpoint
176
+ requestUrl = endpoint;
177
+ } else {
178
+ // Generic IPFS pinning service (assumed to follow IPFS Pinning Services API)
179
+ requestUrl = endpoint.endsWith('/') ? endpoint + 'pins' : endpoint + '/pins';
180
+ }
181
+
182
+ const response = await httpRequest(requestUrl, {
183
+ method: 'POST',
184
+ headers,
185
+ body,
186
+ timeout: 120000, // 2 minutes for upload
187
+ });
188
+
189
+ // Parse response to extract CID
190
+ const result = JSON.parse(response.toString()) as Record<string, unknown>;
191
+
192
+ // Handle different response formats
193
+ let cid: string | undefined;
194
+
195
+ if (typeof result.IpfsHash === 'string') {
196
+ // Pinata format
197
+ cid = result.IpfsHash;
198
+ } else if (typeof result.cid === 'string') {
199
+ // web3.storage / nft.storage format
200
+ cid = result.cid;
201
+ } else if (typeof result.Hash === 'string') {
202
+ // IPFS API format
203
+ cid = result.Hash;
204
+ } else if (result.value && typeof (result.value as Record<string, unknown>).cid === 'string') {
205
+ // NFT.storage wrapped format
206
+ cid = (result.value as Record<string, unknown>).cid as string;
207
+ }
208
+
209
+ if (!cid) {
210
+ throw new Error(`Failed to extract CID from pinning response: ${JSON.stringify(result)}`);
211
+ }
212
+
213
+ return {
214
+ cid,
215
+ size: data.length,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Fetch a file from IPFS using configured gateways
221
+ *
222
+ * @param cid - The IPFS CID
223
+ * @returns The file data
224
+ */
225
+ export async function fetchFromIPFS(cid: string): Promise<FetchResult> {
226
+ const config = loadConfig();
227
+ const gateways = config.ipfsGateways.length > 0 ? config.ipfsGateways : [config.ipfsGateway];
228
+
229
+ let lastError: Error | undefined;
230
+
231
+ for (const gateway of gateways) {
232
+ try {
233
+ const url = buildGatewayUrl(gateway, cid);
234
+ const data = await httpRequest(url, {
235
+ method: 'GET',
236
+ timeout: 60000, // 1 minute
237
+ });
238
+
239
+ return {
240
+ data,
241
+ size: data.length,
242
+ };
243
+ } catch (error) {
244
+ lastError = error instanceof Error ? error : new Error(String(error));
245
+ // Try next gateway
246
+ continue;
247
+ }
248
+ }
249
+
250
+ throw new Error(`Failed to fetch from all gateways: ${lastError?.message}`);
251
+ }
252
+
253
+ /**
254
+ * Build a gateway URL for a CID
255
+ *
256
+ * @param gateway - The gateway base URL
257
+ * @param cid - The IPFS CID
258
+ * @returns Full URL
259
+ */
260
+ export function buildGatewayUrl(gateway: string, cid: string): string {
261
+ // Remove trailing slash
262
+ const base = gateway.replace(/\/$/, '');
263
+
264
+ // Handle different gateway URL patterns
265
+ if (base.includes('{cid}')) {
266
+ // Template URL
267
+ return base.replace('{cid}', cid);
268
+ } else if (base.includes('/ipfs/')) {
269
+ // Path-style gateway
270
+ return `${base}${cid}`;
271
+ } else {
272
+ // Standard gateway
273
+ return `${base}/ipfs/${cid}`;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Validate an IPFS CID
279
+ *
280
+ * @param cid - The CID to validate
281
+ * @returns True if valid CID format
282
+ */
283
+ export function isValidCid(cid: string): boolean {
284
+ // CIDv0 (Qm...) or CIDv1 (ba...)
285
+ if (cid.startsWith('Qm') && cid.length === 46) {
286
+ // CIDv0 base58btc
287
+ return /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/.test(cid);
288
+ } else if (cid.startsWith('ba')) {
289
+ // CIDv1 base32
290
+ return /^ba[a-z2-7]{57,}$/.test(cid);
291
+ } else if (cid.startsWith('b')) {
292
+ // Other CIDv1 bases
293
+ return cid.length >= 50;
294
+ }
295
+
296
+ return false;
297
+ }
298
+
299
+ /**
300
+ * Download a plugin binary from IPFS and save to file
301
+ *
302
+ * @param cid - The IPFS CID
303
+ * @param outputPath - Path to save the file
304
+ * @returns The downloaded file size
305
+ */
306
+ export async function downloadPlugin(cid: string, outputPath: string): Promise<number> {
307
+ const result = await fetchFromIPFS(cid);
308
+ fs.writeFileSync(outputPath, result.data);
309
+ return result.size;
310
+ }
311
+
312
+ /**
313
+ * Upload a plugin binary file to IPFS
314
+ *
315
+ * @param filePath - Path to the plugin file
316
+ * @returns The IPFS CID
317
+ */
318
+ export async function uploadPlugin(filePath: string): Promise<PinResult> {
319
+ const data = fs.readFileSync(filePath);
320
+ const fileName = filePath.split('/').pop() || 'plugin.opnet';
321
+ return pinToIPFS(data, fileName);
322
+ }
323
+
324
+ /**
325
+ * Check if a CID is available on any gateway
326
+ *
327
+ * @param cid - The IPFS CID
328
+ * @returns True if the file is accessible
329
+ */
330
+ export async function checkAvailability(cid: string): Promise<boolean> {
331
+ try {
332
+ await fetchFromIPFS(cid);
333
+ return true;
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Get IPFS gateway status
341
+ *
342
+ * @returns Object with gateway availability status
343
+ */
344
+ export async function getGatewayStatus(): Promise<Record<string, boolean>> {
345
+ const config = loadConfig();
346
+ const gateways = config.ipfsGateways.length > 0 ? config.ipfsGateways : [config.ipfsGateway];
347
+
348
+ // Use a well-known CID for testing (empty directory)
349
+ const testCid = 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn';
350
+
351
+ const status: Record<string, boolean> = {};
352
+
353
+ await Promise.all(
354
+ gateways.map(async (gateway) => {
355
+ try {
356
+ const url = buildGatewayUrl(gateway, testCid);
357
+ await httpRequest(url, {
358
+ method: 'HEAD',
359
+ timeout: 10000,
360
+ });
361
+ status[gateway] = true;
362
+ } catch {
363
+ status[gateway] = false;
364
+ }
365
+ }),
366
+ );
367
+
368
+ return status;
369
+ }
@@ -0,0 +1,172 @@
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
+ * @param manifestPath - Path to plugin.json
93
+ * @returns The loaded manifest or throws with validation errors
94
+ */
95
+ export function loadManifest(manifestPath: string): IPluginMetadata {
96
+ const resolvedPath = path.resolve(manifestPath);
97
+
98
+ if (!fs.existsSync(resolvedPath)) {
99
+ throw new Error(`Manifest not found: ${resolvedPath}`);
100
+ }
101
+
102
+ let manifest: Partial<IPluginMetadata>;
103
+ try {
104
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
105
+ manifest = JSON.parse(content) as Partial<IPluginMetadata>;
106
+ } catch (e) {
107
+ throw new Error(`Failed to parse manifest: ${e instanceof Error ? e.message : String(e)}`);
108
+ }
109
+
110
+ const result = validateManifest(manifest);
111
+ if (!result.valid) {
112
+ const errorList = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join('\n');
113
+ throw new Error(`Invalid manifest:\n${errorList}`);
114
+ }
115
+
116
+ return manifest as IPluginMetadata;
117
+ }
118
+
119
+ /**
120
+ * Save a plugin manifest to file
121
+ *
122
+ * @param manifestPath - Path to save to
123
+ * @param manifest - The manifest to save
124
+ */
125
+ export function saveManifest(manifestPath: string, manifest: IPluginMetadata): void {
126
+ const content = JSON.stringify(manifest, null, 4);
127
+ fs.writeFileSync(manifestPath, content, 'utf-8');
128
+ }
129
+
130
+ /**
131
+ * Create a default plugin manifest
132
+ *
133
+ * @param options - Manifest options
134
+ * @returns A complete manifest with defaults
135
+ */
136
+ export function createManifest(options: {
137
+ name: string;
138
+ author: string;
139
+ email?: string;
140
+ description?: string;
141
+ pluginType: 'standalone' | 'library';
142
+ }): IPluginMetadata {
143
+ return {
144
+ name: options.name,
145
+ version: '1.0.0',
146
+ opnetVersion: '^1.0.0',
147
+ main: 'dist/index.jsc',
148
+ target: 'bytenode',
149
+ type: 'plugin',
150
+ checksum: '',
151
+ author: {
152
+ name: options.author,
153
+ email: options.email,
154
+ },
155
+ description: options.description,
156
+ pluginType: options.pluginType,
157
+ permissions: DEFAULT_PERMISSIONS,
158
+ resources: DEFAULT_RESOURCES,
159
+ lifecycle: DEFAULT_LIFECYCLE,
160
+ dependencies: {},
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Get the manifest path for a directory
166
+ *
167
+ * @param dir - Directory to search
168
+ * @returns Path to plugin.json
169
+ */
170
+ export function getManifestPath(dir: string = process.cwd()): string {
171
+ return path.join(dir, PLUGIN_MANIFEST_FILENAME);
172
+ }
@@ -0,0 +1,121 @@
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
+ }