@0xobelisk/sui-cli 1.2.0-pre.11 → 1.2.0-pre.110

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 (38) hide show
  1. package/README.md +3 -3
  2. package/dist/dubhe.js +126 -49
  3. package/dist/dubhe.js.map +1 -1
  4. package/package.json +31 -19
  5. package/src/commands/build.ts +47 -16
  6. package/src/commands/call.ts +83 -83
  7. package/src/commands/checkBalance.ts +12 -5
  8. package/src/commands/configStore.ts +12 -4
  9. package/src/commands/convertJson.ts +70 -0
  10. package/src/commands/doctor.ts +1515 -0
  11. package/src/commands/faucet.ts +11 -7
  12. package/src/commands/generateKey.ts +3 -2
  13. package/src/commands/index.ts +16 -7
  14. package/src/commands/info.ts +55 -0
  15. package/src/commands/loadMetadata.ts +57 -0
  16. package/src/commands/localnode.ts +22 -6
  17. package/src/commands/publish.ts +21 -7
  18. package/src/commands/query.ts +101 -101
  19. package/src/commands/schemagen.ts +15 -4
  20. package/src/commands/shell.ts +198 -0
  21. package/src/commands/switchEnv.ts +26 -0
  22. package/src/commands/test.ts +54 -11
  23. package/src/commands/upgrade.ts +11 -4
  24. package/src/commands/wait.ts +333 -22
  25. package/src/commands/watch.ts +2 -1
  26. package/src/dubhe.ts +12 -4
  27. package/src/utils/axios-downloader.ts +116 -0
  28. package/src/utils/callHandler.ts +118 -118
  29. package/src/utils/constants.ts +5 -0
  30. package/src/utils/generateAccount.ts +1 -1
  31. package/src/utils/index.ts +4 -3
  32. package/src/utils/metadataHandler.ts +16 -0
  33. package/src/utils/publishHandler.ts +330 -293
  34. package/src/utils/queryStorage.ts +141 -141
  35. package/src/utils/startNode.ts +115 -16
  36. package/src/utils/storeConfig.ts +6 -12
  37. package/src/utils/upgradeHandler.ts +147 -86
  38. package/src/utils/utils.ts +771 -55
@@ -1,23 +1,27 @@
1
1
  import * as fsAsync from 'fs/promises';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
- import { dirname } from 'path';
3
+ import { dirname, join as pathJoin } from 'path';
4
4
  import { SUI_PRIVATE_KEY_PREFIX } from '@mysten/sui/cryptography';
5
5
  import { FsIibError } from './errors';
6
6
  import * as fs from 'fs';
7
7
  import chalk from 'chalk';
8
8
  import { spawn } from 'child_process';
9
- import { Dubhe, NetworkType, SuiMoveNormalizedModules } from '@0xobelisk/sui-client';
9
+ import { Dubhe, NetworkType, SuiMoveNormalizedModules, loadMetadata } from '@0xobelisk/sui-client';
10
10
  import { DubheCliError } from './errors';
11
- import packageJson from '../../package.json';
11
+ import { Component, MoveType, EmptyComponent, DubheConfig } from '@0xobelisk/sui-common';
12
+ import { TESTNET_DUBHE_HUB_OBJECT_ID, TESTNET_ORIGINAL_DUBHE_PACKAGE_ID } from './constants';
12
13
 
13
14
  export type DeploymentJsonType = {
14
15
  projectName: string;
15
16
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
17
+ startCheckpoint: string;
16
18
  packageId: string;
17
- schemaId: string;
19
+ dappHub: string;
18
20
  upgradeCap: string;
19
21
  version: number;
20
- schemas: Record<string, string>;
22
+ components: Record<string, Component | MoveType | EmptyComponent>;
23
+ resources: Record<string, Component | MoveType>;
24
+ enums?: Record<string, string[]>;
21
25
  };
22
26
 
23
27
  export function validatePrivateKey(privateKey: string): false | string {
@@ -76,43 +80,68 @@ export async function getDeploymentJson(
76
80
  }
77
81
  }
78
82
 
79
- export async function getDeploymentSchemaId(projectPath: string, network: string): Promise<string> {
83
+ export async function getDeploymentDappHub(projectPath: string, network: string): Promise<string> {
80
84
  try {
81
85
  const data = await fsAsync.readFile(
82
86
  `${projectPath}/.history/sui_${network}/latest.json`,
83
87
  'utf8'
84
88
  );
85
89
  const deployment = JSON.parse(data) as DeploymentJsonType;
86
- return deployment.schemaId;
87
- } catch (error) {
90
+ return deployment.dappHub;
91
+ } catch (_error) {
88
92
  return '';
89
93
  }
90
94
  }
91
95
 
92
- export async function getDubheSchemaId(network: string) {
96
+ export async function getDubheDappHub(network: string) {
97
+ const path = process.cwd();
98
+ const contractPath = `${path}/src/dubhe`;
99
+
100
+ switch (network) {
101
+ case 'mainnet':
102
+ return TESTNET_DUBHE_HUB_OBJECT_ID;
103
+ case 'testnet':
104
+ return TESTNET_DUBHE_HUB_OBJECT_ID;
105
+ case 'devnet':
106
+ return TESTNET_DUBHE_HUB_OBJECT_ID;
107
+ case 'localnet':
108
+ return await getDeploymentDappHub(contractPath, 'localnet');
109
+ default:
110
+ throw new Error(`Invalid network: ${network}`);
111
+ }
112
+ }
113
+
114
+ export async function getOriginalDubhePackageId(network: string) {
93
115
  const path = process.cwd();
94
- const contractPath = `${path}/contracts/dubhe-framework`;
116
+ const contractPath = `${path}/src/dubhe`;
95
117
 
96
118
  switch (network) {
97
119
  case 'mainnet':
98
- return await getDeploymentSchemaId(contractPath, 'mainnet');
120
+ return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
99
121
  case 'testnet':
100
- return '0xa565cbb3641fff8f7e8ef384b215808db5f1837aa72c1cca1803b5d973699aac';
122
+ return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
101
123
  case 'devnet':
102
- return await getDeploymentSchemaId(contractPath, 'devnet');
124
+ return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
103
125
  case 'localnet':
104
- return await getDeploymentSchemaId(contractPath, 'localnet');
126
+ return await getOldPackageId(contractPath, network);
105
127
  default:
106
128
  throw new Error(`Invalid network: ${network}`);
107
129
  }
108
130
  }
131
+ export async function getOnchainComponents(
132
+ projectPath: string,
133
+ network: string
134
+ ): Promise<Record<string, Component | MoveType | EmptyComponent>> {
135
+ const deployment = await getDeploymentJson(projectPath, network);
136
+ return deployment.components;
137
+ }
109
138
 
110
- export async function getOnchainSchemas(
139
+ export async function getOnchainResources(
111
140
  projectPath: string,
112
141
  network: string
113
- ): Promise<Record<string, string>> {
142
+ ): Promise<Record<string, Component | MoveType>> {
114
143
  const deployment = await getDeploymentJson(projectPath, network);
115
- return deployment.schemas;
144
+ return deployment.resources;
116
145
  }
117
146
 
118
147
  export async function getVersion(projectPath: string, network: string): Promise<number> {
@@ -133,9 +162,9 @@ export async function getOldPackageId(projectPath: string, network: string): Pro
133
162
  return deployment.packageId;
134
163
  }
135
164
 
136
- export async function getSchemaId(projectPath: string, network: string): Promise<string> {
165
+ export async function getDappHub(projectPath: string, network: string): Promise<string> {
137
166
  const deployment = await getDeploymentJson(projectPath, network);
138
- return deployment.schemaId;
167
+ return deployment.dappHub;
139
168
  }
140
169
 
141
170
  export async function getUpgradeCap(projectPath: string, network: string): Promise<string> {
@@ -143,34 +172,73 @@ export async function getUpgradeCap(projectPath: string, network: string): Promi
143
172
  return deployment.upgradeCap;
144
173
  }
145
174
 
146
- export function saveContractData(
175
+ export async function getStartCheckpoint(projectPath: string, network: string): Promise<string> {
176
+ const deployment = await getDeploymentJson(projectPath, network);
177
+ return deployment.startCheckpoint;
178
+ }
179
+
180
+ export async function saveContractData(
147
181
  projectName: string,
148
182
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
183
+ startCheckpoint: string,
149
184
  packageId: string,
150
- schemaId: string,
185
+ dappHub: string,
151
186
  upgradeCap: string,
152
187
  version: number,
153
- schemas: Record<string, string>
188
+ components: Record<string, Component | MoveType | EmptyComponent>,
189
+ resources: Record<string, Component | MoveType>,
190
+ enums?: Record<string, string[]>
154
191
  ) {
155
192
  const DeploymentData: DeploymentJsonType = {
156
193
  projectName,
157
194
  network,
195
+ startCheckpoint,
158
196
  packageId,
159
- schemaId,
160
- schemas,
197
+ dappHub,
161
198
  upgradeCap,
162
- version
199
+ version,
200
+ components,
201
+ resources,
202
+ enums
163
203
  };
164
204
 
165
205
  const path = process.cwd();
166
206
  const storeDeploymentData = JSON.stringify(DeploymentData, null, 2);
167
- writeOutput(
207
+ await writeOutput(
168
208
  storeDeploymentData,
169
- `${path}/contracts/${projectName}/.history/sui_${network}/latest.json`,
209
+ `${path}/src/${projectName}/.history/sui_${network}/latest.json`,
170
210
  'Update deploy log'
171
211
  );
172
212
  }
173
213
 
214
+ export async function saveMetadata(
215
+ projectName: string,
216
+ network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
217
+ packageId: string
218
+ ) {
219
+ const path = process.cwd();
220
+
221
+ // Save metadata files
222
+ try {
223
+ const metadata = await loadMetadata(network, packageId);
224
+ if (metadata) {
225
+ const metadataJson = JSON.stringify(metadata, null, 2);
226
+
227
+ // Save packageId-specific metadata file
228
+ await writeOutput(
229
+ metadataJson,
230
+ `${path}/src/${projectName}/.history/sui_${network}/${packageId}.json`,
231
+ 'Save package metadata'
232
+ );
233
+
234
+ // Save latest metadata.json
235
+ await writeOutput(metadataJson, `${path}/metadata.json`, 'Save latest metadata');
236
+ }
237
+ } catch (error) {
238
+ console.warn(chalk.yellow(`Warning: Failed to save metadata: ${error}`));
239
+ }
240
+ }
241
+
174
242
  export async function writeOutput(
175
243
  output: string,
176
244
  fullOutputPath: string,
@@ -184,55 +252,486 @@ export async function writeOutput(
184
252
  }
185
253
  }
186
254
 
187
- function getDubheDependency(network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'): string {
188
- switch (network) {
189
- case 'localnet':
190
- return 'Dubhe = { local = "../dubhe" }';
191
- case 'testnet':
192
- return `Dubhe = { git = "https://github.com/0xobelisk/dubhe-wip.git", subdir = "packages/sui-framework/contracts/dubhe", rev = "${packageJson.version}" }`;
193
- case 'mainnet':
194
- return `Dubhe = { git = "https://github.com/0xobelisk/dubhe-wip.git", subdir = "packages/sui-framework/contracts/dubhe", rev = "${packageJson.version}" }`;
195
- default:
196
- throw new Error(`Unsupported network: ${network}`);
197
- }
198
- }
199
-
200
255
  export async function updateDubheDependency(
201
256
  filePath: string,
202
257
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
203
258
  ) {
204
- const fileContent = fs.readFileSync(filePath, 'utf-8');
205
- const newDependency = getDubheDependency(network);
206
- const updatedContent = fileContent.replace(/Dubhe = \{.*\}/, newDependency);
207
- fs.writeFileSync(filePath, updatedContent, 'utf-8');
208
- console.log(`Updated Dubhe dependency in ${filePath} for ${network}.`);
259
+ // With the new --build-env mechanism, we keep Dubhe as local dependency for all networks.
260
+ // The Published.toml in ../dubhe resolves the correct on-chain address per environment.
261
+ // This function is kept for backward compatibility but is a no-op for non-localnet.
262
+ if (network === 'localnet') {
263
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
264
+ const localDependency = 'Dubhe = { local = "../dubhe" }';
265
+ if (!fileContent.includes(localDependency)) {
266
+ const updatedContent = fileContent.replace(/Dubhe = \{[^}]*\}/, localDependency);
267
+ fs.writeFileSync(filePath, updatedContent, 'utf-8');
268
+ console.log(`Ensured local Dubhe dependency in ${filePath} for localnet.`);
269
+ }
270
+ }
271
+ }
272
+
273
+ // Published.toml management for the new Sui CLI (v1.44+) publishing mechanism.
274
+ // Published.toml tracks on-chain package addresses per environment.
275
+ // It SHOULD be committed to source control.
276
+
277
+ interface PublishedEntry {
278
+ chainId: string;
279
+ publishedAt: string;
280
+ originalId: string;
281
+ version: number;
282
+ }
283
+
284
+ export function readPublishedToml(packagePath: string): Record<string, PublishedEntry> {
285
+ const filePath = pathJoin(packagePath, 'Published.toml');
286
+ if (!fs.existsSync(filePath)) {
287
+ return {};
288
+ }
289
+ const content = fs.readFileSync(filePath, 'utf-8');
290
+ const result: Record<string, PublishedEntry> = {};
291
+
292
+ const sectionRegex = /\[published\.(\w+)\]([\s\S]*?)(?=\[published\.|$)/g;
293
+ let match;
294
+ while ((match = sectionRegex.exec(content)) !== null) {
295
+ const env = match[1];
296
+ const body = match[2];
297
+ const getValue = (key: string) => {
298
+ const m = body.match(new RegExp(`${key}\\s*=\\s*"([^"]*)"`));
299
+ return m ? m[1] : '';
300
+ };
301
+ const versionMatch = body.match(/version\s*=\s*(\d+)/);
302
+ result[env] = {
303
+ chainId: getValue('chain-id'),
304
+ publishedAt: getValue('published-at'),
305
+ originalId: getValue('original-id'),
306
+ version: versionMatch ? parseInt(versionMatch[1], 10) : 1
307
+ };
308
+ }
309
+ return result;
310
+ }
311
+
312
+ export function writePublishedToml(
313
+ packagePath: string,
314
+ entries: Record<string, PublishedEntry>
315
+ ): void {
316
+ const filePath = pathJoin(packagePath, 'Published.toml');
317
+ let content =
318
+ '# Generated by Move\n' +
319
+ '# This file contains metadata about published versions of this package in different environments\n' +
320
+ '# This file SHOULD be committed to source control\n';
321
+
322
+ for (const [env, entry] of Object.entries(entries)) {
323
+ content += `\n[published.${env}]\n`;
324
+ content += `chain-id = "${entry.chainId}"\n`;
325
+ content += `published-at = "${entry.publishedAt}"\n`;
326
+ content += `original-id = "${entry.originalId}"\n`;
327
+ content += `version = ${entry.version}\n`;
328
+ }
329
+
330
+ fs.writeFileSync(filePath, content, 'utf-8');
331
+ }
332
+
333
+ export function updatePublishedToml(
334
+ packagePath: string,
335
+ network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
336
+ chainId: string,
337
+ packageId: string,
338
+ originalId?: string,
339
+ version?: number
340
+ ): void {
341
+ const entries = readPublishedToml(packagePath);
342
+ const existing = entries[network];
343
+
344
+ entries[network] = {
345
+ chainId,
346
+ publishedAt: packageId,
347
+ originalId: originalId ?? existing?.originalId ?? packageId,
348
+ version: version ?? (existing ? existing.version + 1 : 1)
349
+ };
350
+
351
+ writePublishedToml(packagePath, entries);
352
+ console.log(`Updated Published.toml in ${packagePath} for ${network}.`);
353
+ }
354
+
355
+ export function getPublishedTomlEntry(
356
+ packagePath: string,
357
+ network: string
358
+ ): PublishedEntry | undefined {
359
+ const entries = readPublishedToml(packagePath);
360
+ return entries[network];
361
+ }
362
+
363
+ export function clearPublishedTomlEntry(
364
+ packagePath: string,
365
+ network: string
366
+ ): PublishedEntry | undefined {
367
+ const entries = readPublishedToml(packagePath);
368
+ const existing = entries[network];
369
+ if (!existing) return undefined;
370
+
371
+ entries[network] = {
372
+ ...existing,
373
+ publishedAt: '0x0000000000000000000000000000000000000000000000000000000000000000',
374
+ originalId: '0x0000000000000000000000000000000000000000000000000000000000000000'
375
+ };
376
+ writePublishedToml(packagePath, entries);
377
+ return existing;
378
+ }
379
+
380
+ export function restorePublishedTomlEntry(
381
+ packagePath: string,
382
+ network: string,
383
+ entry: PublishedEntry
384
+ ): void {
385
+ const entries = readPublishedToml(packagePath);
386
+ entries[network] = entry;
387
+ writePublishedToml(packagePath, entries);
388
+ }
389
+
390
+ // ─────────────────────────────────────────────────────────────────────────────
391
+ // Ephemeral publication file (Pub.<env>.toml)
392
+ //
393
+ // Per the Sui package management docs (v1.63+), localnet / devnet deployments
394
+ // should use ephemeral publication files rather than the shared Published.toml.
395
+ // The ephemeral file holds the localnet addresses so that subsequent builds
396
+ // (e.g. for upgrades) can resolve local dependencies correctly.
397
+ //
398
+ // Reference: https://docs.sui.io/guides/developer/packages/move-package-management
399
+ // ─────────────────────────────────────────────────────────────────────────────
400
+
401
+ export interface EphemeralPubEntry {
402
+ /** Absolute path to the package source directory */
403
+ source: string;
404
+ /** Current on-chain address of the package */
405
+ publishedAt: string;
406
+ /** Address of the first published version (same as publishedAt for v1) */
407
+ originalId: string;
408
+ /** Object ID of the upgrade capability */
409
+ upgradeCap: string;
410
+ /** Package version (required by Sui CLI parser) */
411
+ version?: number;
412
+ }
413
+
414
+ /**
415
+ * Return the canonical path for the ephemeral publication file.
416
+ * For localnet this is <contractsDir>/Pub.localnet.toml.
417
+ */
418
+ export function getEphemeralPubFilePath(contractsDir: string, network: string): string {
419
+ return pathJoin(contractsDir, `Pub.${network}.toml`);
209
420
  }
421
+
422
+ /**
423
+ * Update (or create) an entry in the ephemeral publication file.
424
+ * Preserves existing entries for other packages.
425
+ */
426
+ export function updateEphemeralPubFile(
427
+ pubfilePath: string,
428
+ chainId: string,
429
+ buildEnv: string,
430
+ entry: EphemeralPubEntry
431
+ ): void {
432
+ const existing: EphemeralPubEntry[] = [];
433
+ // Always use the provided buildEnv and chainId parameters.
434
+ // The chainId passed in comes from the live network and is authoritative.
435
+ const currentBuildEnv = buildEnv;
436
+ const currentChainId = chainId;
437
+
438
+ if (fs.existsSync(pubfilePath)) {
439
+ const content = fs.readFileSync(pubfilePath, 'utf-8');
440
+
441
+ // Check if the file was written for a different chain (e.g. previous localnet run).
442
+ // If chain-id changed, discard all existing entries — they belong to a dead chain.
443
+ const chainIdMatch = content.match(/^chain-id\s*=\s*"([^"]*)"/m);
444
+ const fileChainId = chainIdMatch ? chainIdMatch[1] : '';
445
+ const chainChanged = fileChainId !== '' && fileChainId !== chainId;
446
+
447
+ if (!chainChanged) {
448
+ // Same chain: parse existing [[published]] blocks and preserve them.
449
+ // source field is an inline table: source = { local = "..." }
450
+ const blockRegex = /\[\[published\]\]([\s\S]*?)(?=\[\[published\]\]|$)/g;
451
+ let blockMatch;
452
+ while ((blockMatch = blockRegex.exec(content)) !== null) {
453
+ const block = blockMatch[1];
454
+ const get = (key: string) => {
455
+ const m = block.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
456
+ return m ? m[1] : '';
457
+ };
458
+ // source = { local = "/path/to/package" }
459
+ const srcMatch = block.match(/^source\s*=\s*\{\s*local\s*=\s*"([^"]*)"\s*\}/m);
460
+ const src = srcMatch ? srcMatch[1] : '';
461
+ if (src) {
462
+ existing.push({
463
+ source: src,
464
+ publishedAt: get('published-at'),
465
+ originalId: get('original-id'),
466
+ upgradeCap: get('upgrade-cap')
467
+ });
468
+ }
469
+ }
470
+ } else {
471
+ console.log(` Pub file chain-id changed (${fileChainId} → ${chainId}), resetting entries.`);
472
+ }
473
+ }
474
+
475
+ // Update override or add the entry
476
+ const idx = existing.findIndex((e) => e.source === entry.source);
477
+ if (idx >= 0) {
478
+ existing[idx] = entry;
479
+ } else {
480
+ existing.push(entry);
481
+ }
482
+
483
+ // Write the file
484
+ let content =
485
+ '# generated by dubhe cli\n' +
486
+ '# this file contains metadata from ephemeral publications\n' +
487
+ '# this file should NOT be committed to source control\n\n';
488
+ content += `build-env = "${currentBuildEnv}"\n`;
489
+ content += `chain-id = "${currentChainId}"\n`;
490
+
491
+ for (const e of existing) {
492
+ content += '\n[[published]]\n';
493
+ // source must be a LocalDepInfo struct (not a plain string)
494
+ content += `source = { local = "${e.source}" }\n`;
495
+ content += `published-at = "${e.publishedAt}"\n`;
496
+ content += `original-id = "${e.originalId}"\n`;
497
+ content += `upgrade-cap = "${e.upgradeCap}"\n`;
498
+ // version is required by Sui CLI parser (even though docs omit it)
499
+ content += `version = 1\n`;
500
+ }
501
+
502
+ fs.writeFileSync(pubfilePath, content, 'utf-8');
503
+ console.log(
504
+ ` Updated ${pathJoin(pubfilePath.split('/').slice(-1)[0])} for ${
505
+ entry.source.split('/').slice(-1)[0]
506
+ }.`
507
+ );
508
+ }
509
+
510
+ async function checkRpcAvailability(rpcUrl: string): Promise<boolean> {
511
+ try {
512
+ const response = await fetch(rpcUrl, {
513
+ method: 'POST',
514
+ headers: {
515
+ 'Content-Type': 'application/json'
516
+ },
517
+ body: JSON.stringify({
518
+ jsonrpc: '2.0',
519
+ id: 1,
520
+ method: 'sui_getLatestCheckpointSequenceNumber',
521
+ params: []
522
+ })
523
+ });
524
+
525
+ if (!response.ok) {
526
+ return false;
527
+ }
528
+
529
+ const data = await response.json();
530
+ return !data.error;
531
+ } catch (_error) {
532
+ return false;
533
+ }
534
+ }
535
+
536
+ export async function addEnv(
537
+ network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
538
+ ): Promise<void> {
539
+ const rpcMap = {
540
+ localnet: 'http://127.0.0.1:9000',
541
+ devnet: 'https://fullnode.devnet.sui.io:443/',
542
+ testnet: 'https://fullnode.testnet.sui.io:443/',
543
+ mainnet: 'https://fullnode.mainnet.sui.io:443/'
544
+ };
545
+
546
+ const rpcUrl = rpcMap[network];
547
+
548
+ // Check RPC availability first
549
+ const isRpcAvailable = await checkRpcAvailability(rpcUrl);
550
+ if (!isRpcAvailable) {
551
+ throw new Error(
552
+ `RPC endpoint ${rpcUrl} is not available. Please check your network connection or try again later.`
553
+ );
554
+ }
555
+
556
+ return new Promise<void>((resolve, reject) => {
557
+ let errorOutput = '';
558
+ let stdoutOutput = '';
559
+
560
+ const suiProcess = spawn(
561
+ 'sui',
562
+ ['client', 'new-env', '--alias', network, '--rpc', rpcMap[network]],
563
+ {
564
+ env: { ...process.env },
565
+ stdio: 'pipe'
566
+ }
567
+ );
568
+
569
+ // Capture standard output
570
+ suiProcess.stdout.on('data', (data) => {
571
+ stdoutOutput += data.toString();
572
+ });
573
+
574
+ // Capture error output
575
+ suiProcess.stderr.on('data', (data) => {
576
+ errorOutput += data.toString();
577
+ });
578
+
579
+ // Handle process errors (e.g., command not found)
580
+ suiProcess.on('error', (error) => {
581
+ console.error(chalk.red(`\n❌ Failed to execute sui command: ${error.message}`));
582
+ reject(new Error(`Failed to execute sui command: ${error.message}`));
583
+ });
584
+
585
+ // Handle process exit
586
+ suiProcess.on('exit', (code, signal) => {
587
+ // Check if "already exists" message is present
588
+ if (errorOutput.includes('already exists') || stdoutOutput.includes('already exists')) {
589
+ console.log(chalk.yellow(`Environment ${network} already exists, proceeding...`));
590
+ resolve();
591
+ return;
592
+ }
593
+
594
+ if (code === 0) {
595
+ console.log(chalk.green(`Successfully added environment ${network}`));
596
+ resolve();
597
+ } else {
598
+ let finalError: string;
599
+ if (code === null) {
600
+ // Process was killed by a signal
601
+ finalError =
602
+ errorOutput ||
603
+ stdoutOutput ||
604
+ `Process was terminated by signal ${signal || 'unknown'}`;
605
+ } else {
606
+ finalError = errorOutput || stdoutOutput || `Process exited with code ${code}`;
607
+ }
608
+ console.error(chalk.red(`\n❌ Failed to add environment ${network}`));
609
+ console.error(chalk.red(` └─ ${finalError.trim()}`));
610
+ reject(new Error(finalError));
611
+ }
612
+ });
613
+ });
614
+ }
615
+
616
+ export type NetworkAlias = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
617
+
618
+ export interface Endpoint {
619
+ alias: NetworkAlias;
620
+ rpc: string;
621
+ ws: string | null;
622
+ basic_auth: { username: string; password: string } | null;
623
+ }
624
+
625
+ // mainly is a tuple of [endpoint list, current active alias]
626
+ export type ConfigTuple = [Endpoint[], NetworkAlias];
627
+
628
+ export async function envsJSON(): Promise<ConfigTuple> {
629
+ try {
630
+ return new Promise<ConfigTuple>((resolve, reject) => {
631
+ let errorOutput = '';
632
+ let stdoutOutput = '';
633
+
634
+ const suiProcess = spawn('sui', ['client', 'envs', '--json'], {
635
+ env: { ...process.env },
636
+ stdio: 'pipe'
637
+ });
638
+
639
+ suiProcess.stdout.on('data', (data) => {
640
+ stdoutOutput += data.toString();
641
+ });
642
+
643
+ suiProcess.stderr.on('data', (data) => {
644
+ errorOutput += data.toString();
645
+ });
646
+
647
+ suiProcess.on('error', (error) => {
648
+ console.error(chalk.red(`\n❌ Failed to execute sui command: ${error.message}`));
649
+ reject(new Error(`Failed to execute sui command: ${error.message}`));
650
+ });
651
+
652
+ suiProcess.on('exit', (code, signal) => {
653
+ if (code === 0) {
654
+ resolve(JSON.parse(stdoutOutput) as ConfigTuple);
655
+ } else {
656
+ let finalError: string;
657
+ if (code === null) {
658
+ // Process was killed by a signal
659
+ finalError =
660
+ errorOutput ||
661
+ stdoutOutput ||
662
+ `Process was terminated by signal ${signal || 'unknown'}`;
663
+ } else {
664
+ finalError = errorOutput || stdoutOutput || `Process exited with code ${code}`;
665
+ }
666
+ console.error(chalk.red(`\n❌ Failed to get envs`));
667
+ console.error(chalk.red(` └─ ${finalError.trim()}`));
668
+ reject(new Error(finalError));
669
+ }
670
+ });
671
+ });
672
+ } catch (error) {
673
+ // Re-throw the error for the caller to handle
674
+ throw error;
675
+ }
676
+ }
677
+
678
+ export async function getDefaultNetwork(): Promise<NetworkAlias> {
679
+ const [_, currentAlias] = await envsJSON();
680
+ return currentAlias as NetworkAlias;
681
+ }
682
+
210
683
  export async function switchEnv(network: 'mainnet' | 'testnet' | 'devnet' | 'localnet') {
211
684
  try {
685
+ // First, try to add the environment
686
+ await addEnv(network);
687
+
688
+ // Then switch to the specified environment
212
689
  return new Promise<void>((resolve, reject) => {
690
+ let errorOutput = '';
691
+ let stdoutOutput = '';
692
+
213
693
  const suiProcess = spawn('sui', ['client', 'switch', '--env', network], {
214
694
  env: { ...process.env },
215
695
  stdio: 'pipe'
216
696
  });
217
697
 
698
+ suiProcess.stdout.on('data', (data) => {
699
+ stdoutOutput += data.toString();
700
+ });
701
+
702
+ suiProcess.stderr.on('data', (data) => {
703
+ errorOutput += data.toString();
704
+ });
705
+
218
706
  suiProcess.on('error', (error) => {
219
- console.error(chalk.red('\n❌ Failed to Switch Env'));
220
- console.error(chalk.red(` Error: ${error.message}`));
221
- reject(error); // Reject promise on error
707
+ console.error(chalk.red(`\n❌ Failed to execute sui command: ${error.message}`));
708
+ reject(new Error(`Failed to execute sui command: ${error.message}`));
222
709
  });
223
710
 
224
- suiProcess.on('exit', (code) => {
225
- if (code !== 0) {
226
- console.error(chalk.red(`\n❌ Process exited with code: ${code}`));
227
- reject(new Error(`Process exited with code: ${code}`));
711
+ suiProcess.on('exit', (code, signal) => {
712
+ if (code === 0) {
713
+ console.log(chalk.green(`Successfully switched to environment ${network}`));
714
+ resolve();
228
715
  } else {
229
- resolve(); // Resolve promise on successful exit
716
+ let finalError: string;
717
+ if (code === null) {
718
+ // Process was killed by a signal
719
+ finalError =
720
+ errorOutput ||
721
+ stdoutOutput ||
722
+ `Process was terminated by signal ${signal || 'unknown'}`;
723
+ } else {
724
+ finalError = errorOutput || stdoutOutput || `Process exited with code ${code}`;
725
+ }
726
+ console.error(chalk.red(`\n❌ Failed to switch to environment ${network}`));
727
+ console.error(chalk.red(` └─ ${finalError.trim()}`));
728
+ reject(new Error(finalError));
230
729
  }
231
730
  });
232
731
  });
233
732
  } catch (error) {
234
- console.error(chalk.red('\n❌ Failed to Switch Env'));
235
- console.error(chalk.red(` └─ Error: ${error}`));
733
+ // Re-throw the error for the caller to handle
734
+ throw error;
236
735
  }
237
736
  }
238
737
 
@@ -272,3 +771,220 @@ export function initializeDubhe({
272
771
  metadata
273
772
  });
274
773
  }
774
+
775
+ export function generateConfigJson(config: DubheConfig): string {
776
+ const components = Object.entries(config.components).map(([name, component]) => {
777
+ if (typeof component === 'string') {
778
+ return {
779
+ [name]: {
780
+ fields: [{ entity_id: 'address' }, { value: component }],
781
+ keys: ['entity_id'],
782
+ offchain: false
783
+ }
784
+ };
785
+ }
786
+
787
+ if (Object.keys(component as object).length === 0) {
788
+ return {
789
+ [name]: {
790
+ fields: [{ entity_id: 'address' }],
791
+ keys: ['entity_id'],
792
+ offchain: false
793
+ }
794
+ };
795
+ }
796
+
797
+ const fields = (component as any).fields || {};
798
+ const keys = (component as any).keys || ['entity_id'];
799
+ const offchain = (component as any).offchain ?? false;
800
+
801
+ // ensure entity_id field exists
802
+ if (!fields.entity_id && keys.includes('entity_id')) {
803
+ fields.entity_id = 'address';
804
+ }
805
+
806
+ // prepare fields with entity_id first
807
+ const fieldEntries = Object.entries(fields);
808
+ const entityIdField = fieldEntries.find(([key]) => key === 'entity_id');
809
+ const otherFields = fieldEntries.filter(([key]) => key !== 'entity_id');
810
+ const orderedFields = entityIdField ? [entityIdField, ...otherFields] : otherFields;
811
+
812
+ return {
813
+ [name]: {
814
+ fields: orderedFields.map(([fieldName, fieldType]) => ({
815
+ [fieldName]: fieldType
816
+ })),
817
+ keys: keys,
818
+ offchain: offchain
819
+ }
820
+ };
821
+ });
822
+
823
+ const resources = Object.entries(config.resources).map(([name, resource]) => {
824
+ // Simple type shorthand (e.g., counter1: 'u32') – entity-keyed by account (entity_id: String).
825
+ if (typeof resource === 'string') {
826
+ return {
827
+ [name]: {
828
+ fields: [{ entity_id: 'String' }, { value: resource }],
829
+ keys: ['entity_id'],
830
+ offchain: false
831
+ }
832
+ };
833
+ }
834
+
835
+ // Empty resource object – only the implicit entity key.
836
+ if (Object.keys(resource as object).length === 0) {
837
+ return {
838
+ [name]: {
839
+ fields: [{ entity_id: 'String' }],
840
+ keys: ['entity_id'],
841
+ offchain: false
842
+ }
843
+ };
844
+ }
845
+
846
+ const fields = (resource as any).fields || {};
847
+ const keys = (resource as any).keys || [];
848
+ const offchain = (resource as any).offchain ?? false;
849
+
850
+ // Full Component format with no explicit keys: auto-inject 'entity_id: String'.
851
+ if (keys.length === 0) {
852
+ const fieldEntries = Object.entries(fields);
853
+ const orderedFields: [string, unknown][] = [['entity_id', 'String'], ...fieldEntries];
854
+ return {
855
+ [name]: {
856
+ fields: orderedFields.map(([fieldName, fieldType]) => ({
857
+ [fieldName]: fieldType
858
+ })),
859
+ keys: ['entity_id'],
860
+ offchain: offchain
861
+ }
862
+ };
863
+ }
864
+
865
+ // Full Component format with explicit custom keys: inject 'entity_id: String' as the first
866
+ // field and first key so that key_tuple[0] (the BCS-encoded account injected by the indexer)
867
+ // maps correctly, followed by the user-defined keys.
868
+ const fieldEntries = Object.entries(fields);
869
+ const orderedFields: [string, unknown][] = [['entity_id', 'String'], ...fieldEntries];
870
+ return {
871
+ [name]: {
872
+ fields: orderedFields.map(([fieldName, fieldType]) => ({
873
+ [fieldName]: fieldType
874
+ })),
875
+ keys: ['entity_id', ...keys],
876
+ offchain: offchain
877
+ }
878
+ };
879
+ });
880
+
881
+ // Auto-append Dubhe framework fee state resource (entity-keyed by account string).
882
+ if (!resources.some((resource) => 'dapp_fee_state' in resource)) {
883
+ resources.push({
884
+ dapp_fee_state: {
885
+ fields: [
886
+ { entity_id: 'String' },
887
+ { base_fee: 'u256' },
888
+ { byte_fee: 'u256' },
889
+ { free_credit: 'u256' },
890
+ { total_bytes_size: 'u256' },
891
+ { total_recharged: 'u256' },
892
+ { total_paid: 'u256' }
893
+ ],
894
+ keys: ['entity_id'],
895
+ offchain: false
896
+ }
897
+ });
898
+ }
899
+
900
+ // handle enums
901
+ const enums = Object.entries(config.enums || {}).map(([name, enumFields]) => {
902
+ // Sort enum values by first letter
903
+ const sortedFields = enumFields.sort((a, b) => a.localeCompare(b)).map((value) => value);
904
+
905
+ return {
906
+ [name]: sortedFields
907
+ };
908
+ });
909
+
910
+ return JSON.stringify(
911
+ {
912
+ components,
913
+ resources,
914
+ enums
915
+ },
916
+ null,
917
+ 2
918
+ );
919
+ }
920
+
921
+ /**
922
+ * Updates the dubhe address and published-at in Move.toml file
923
+ * @param path - Directory path containing Move.toml file
924
+ * @param packageAddress - New dubhe package address to set
925
+ *
926
+ * Logic:
927
+ * - If packageAddress is "0x0": only set dubhe = "0x0", remove published-at line
928
+ * - Otherwise: set both dubhe and published-at to packageAddress
929
+ */
930
+ export function updateMoveTomlAddress(path: string, packageAddress: string) {
931
+ const moveTomlPath = `${path}/Move.toml`;
932
+ const moveTomlContent = fs.readFileSync(moveTomlPath, 'utf-8');
933
+
934
+ let updatedContent = moveTomlContent;
935
+
936
+ if (packageAddress === '0x0') {
937
+ // Case 1: Address is "0x0" - set dubhe to "0x0" and remove published-at line
938
+ updatedContent = updatedContent.replace(/dubhe\s*=\s*"[^"]*"/, `dubhe = "0x0"`);
939
+
940
+ // Remove published-at line (including the line break)
941
+ updatedContent = updatedContent.replace(/published-at\s*=\s*"[^"]*"\r?\n?/, '');
942
+ } else {
943
+ // Case 2: Address is not "0x0" - set both dubhe and published-at
944
+ updatedContent = updatedContent.replace(/dubhe\s*=\s*"[^"]*"/, `dubhe = "${packageAddress}"`);
945
+
946
+ // Check if published-at already exists
947
+ if (/published-at\s*=\s*"[^"]*"/.test(updatedContent)) {
948
+ // Replace existing published-at
949
+ updatedContent = updatedContent.replace(
950
+ /published-at\s*=\s*"[^"]*"/,
951
+ `published-at = "${packageAddress}"`
952
+ );
953
+ } else {
954
+ // Add published-at after [package] line if it doesn't exist
955
+ updatedContent = updatedContent.replace(
956
+ /(\[package\][^\n]*\n)/,
957
+ `$1published-at = "${packageAddress}"\n`
958
+ );
959
+ }
960
+ }
961
+
962
+ fs.writeFileSync(moveTomlPath, updatedContent, 'utf-8');
963
+ }
964
+
965
+ export function updateGenesisUpgradeFunction(path: string, tables: string[]) {
966
+ const genesisPath = `${path}/sources/codegen/genesis.move`;
967
+ const genesisContent = fs.readFileSync(genesisPath, 'utf-8');
968
+
969
+ // Match the first pair of // ========================================== lines (with any content, including empty, between them)
970
+ const separatorRegex =
971
+ /(\/\/ ==========================================)[\s\S]*?(\/\/ ==========================================)/;
972
+ const match = genesisContent.match(separatorRegex);
973
+
974
+ if (!match) {
975
+ throw new Error('Could not find separator comments in genesis.move');
976
+ }
977
+
978
+ // Generate new table registration code
979
+ const registerTablesCode = tables
980
+ .map((table) => ` ${table}::register_table(dapp_hub, ctx);`)
981
+ .join('\n');
982
+
983
+ // Build new content, preserve separators, replace middle content
984
+ const newContent = `${match[1]}\n${registerTablesCode}\n${match[2]}`;
985
+
986
+ // Replace matched content
987
+ const updatedContent = genesisContent.replace(separatorRegex, newContent);
988
+
989
+ fs.writeFileSync(genesisPath, updatedContent, 'utf-8');
990
+ }