@0xobelisk/sui-cli 1.2.0-pre.1 → 1.2.0-pre.100

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 +125 -66
  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 -12
  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 +295 -290
  34. package/src/utils/queryStorage.ts +141 -141
  35. package/src/utils/startNode.ts +165 -108
  36. package/src/utils/storeConfig.ts +6 -12
  37. package/src/utils/upgradeHandler.ts +147 -86
  38. package/src/utils/utils.ts +771 -54
@@ -3,19 +3,46 @@ import { execSync } from 'child_process';
3
3
  import chalk from 'chalk';
4
4
  import {
5
5
  saveContractData,
6
- updateDubheDependency,
6
+ updateMoveTomlAddress,
7
7
  switchEnv,
8
8
  delay,
9
- getDubheSchemaId,
10
- initializeDubhe
9
+ getDubheDappHub,
10
+ initializeDubhe,
11
+ saveMetadata,
12
+ getOriginalDubhePackageId,
13
+ updatePublishedToml,
14
+ updateEphemeralPubFile,
15
+ getEphemeralPubFilePath
11
16
  } from './utils';
12
17
  import { DubheConfig } from '@0xobelisk/sui-common';
13
18
  import * as fs from 'fs';
14
19
  import * as path from 'path';
15
20
 
16
- const MAX_RETRIES = 60; // 60s timeout
17
- const RETRY_INTERVAL = 1000; // 1s retry interval
18
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
21
+ /**
22
+ * Temporarily add localnet to Move.toml [environments] section before building.
23
+ * Sui CLI 1.40+ requires the active environment to be declared in Move.toml even
24
+ * when --build-env is specified. This patches the file and returns the original
25
+ * content so the caller can restore it in a finally block.
26
+ * Returns null if no changes were needed.
27
+ */
28
+ function patchMoveTomlWithLocalnetEnv(moveTomlPath: string, chainId: string): string | null {
29
+ if (!fs.existsSync(moveTomlPath)) return null;
30
+ const content = fs.readFileSync(moveTomlPath, 'utf-8');
31
+
32
+ if (content.includes('localnet')) {
33
+ return null;
34
+ }
35
+
36
+ let updatedContent: string;
37
+ if (content.includes('[environments]')) {
38
+ updatedContent = content.replace('[environments]', `[environments]\nlocalnet = "${chainId}"`);
39
+ } else {
40
+ updatedContent = content.trimEnd() + `\n\n[environments]\nlocalnet = "${chainId}"\n`;
41
+ }
42
+
43
+ fs.writeFileSync(moveTomlPath, updatedContent, 'utf-8');
44
+ return content;
45
+ }
19
46
 
20
47
  async function removeEnvContent(
21
48
  filePath: string,
@@ -127,21 +154,53 @@ published-version = "${config.publishedVersion}"
127
154
  // return segments.length > 0 ? segments[segments.length - 1] : '';
128
155
  // }
129
156
 
130
- function buildContract(projectPath: string): string[][] {
157
+ /**
158
+ * Build a Move package and return [modules, dependencies] as base64 arrays.
159
+ *
160
+ * For localnet (ephemeral) networks:
161
+ * - Uses --build-env testnet so dependency addresses are resolved via testnet
162
+ * Published.toml (no need to add 'localnet' to Move.toml [environments]).
163
+ * - Optionally reads a Pub.localnet.toml pubfile for already-published local deps.
164
+ * - This matches the Sui docs approach:
165
+ * https://docs.sui.io/guides/developer/packages/move-package-management
166
+ *
167
+ * For persistent networks (testnet/mainnet/devnet): uses -e <network> as before.
168
+ */
169
+ function buildContract(
170
+ projectPath: string,
171
+ network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
172
+ pubfilePath?: string
173
+ ): string[][] {
131
174
  let modules: any, dependencies: any;
132
175
  try {
176
+ let buildEnvFlag: string;
177
+ if (network === 'localnet') {
178
+ // Ephemeral approach: resolve deps via testnet configuration.
179
+ // --pubfile-path supplies already-published local dep addresses (e.g. dubhe).
180
+ buildEnvFlag = ' --build-env testnet';
181
+ if (pubfilePath) {
182
+ buildEnvFlag += ` --pubfile-path ${pubfilePath}`;
183
+ }
184
+ } else {
185
+ buildEnvFlag = network ? ` -e ${network}` : '';
186
+ }
187
+
188
+ // --no-tree-shaking avoids on-chain RPC calls during build.
133
189
  const buildResult = JSON.parse(
134
- execSync(`sui move build --dump-bytecode-as-base64 --path ${projectPath}`, {
135
- encoding: 'utf-8',
136
- stdio: 'pipe'
137
- })
190
+ execSync(
191
+ `sui move build --dump-bytecode-as-base64 --no-tree-shaking${buildEnvFlag} --path ${projectPath}`,
192
+ {
193
+ encoding: 'utf-8',
194
+ stdio: 'pipe'
195
+ }
196
+ )
138
197
  );
139
198
  modules = buildResult.modules;
140
199
  dependencies = buildResult.dependencies;
141
200
  } catch (error: any) {
142
201
  console.error(chalk.red(' └─ Build failed'));
143
- console.error(error.stdout);
144
- process.exit(1);
202
+ console.error(error.stdout || error.stderr);
203
+ throw new Error(`Build failed: ${error.stdout || error.stderr || error.message}`);
145
204
  }
146
205
  return [modules, dependencies];
147
206
  }
@@ -154,132 +213,28 @@ interface ObjectChange {
154
213
  }
155
214
 
156
215
  async function waitForNode(dubhe: Dubhe): Promise<string> {
157
- let retryCount = 0;
158
- let spinnerIndex = 0;
159
- const startTime = Date.now();
160
- let isInterrupted = false;
161
- let chainId = '';
162
- let hasShownBalanceWarning = false;
163
-
164
- const handleInterrupt = () => {
165
- isInterrupted = true;
166
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
167
- console.log('\n └─ Operation cancelled by user');
168
- process.exit(0);
169
- };
170
- process.on('SIGINT', handleInterrupt);
171
-
172
- try {
173
- // 第一阶段:等待获取 chainId
174
- while (retryCount < MAX_RETRIES && !isInterrupted && !chainId) {
175
- try {
176
- chainId = await dubhe.suiInteractor.currentClient.getChainIdentifier();
177
- } catch (error) {
178
- // 忽略错误,继续重试
179
- }
180
-
181
- if (isInterrupted) break;
182
-
183
- if (!chainId) {
184
- retryCount++;
185
- if (retryCount === MAX_RETRIES) {
186
- console.log(chalk.red(` └─ Failed to connect to node after ${MAX_RETRIES} attempts`));
187
- console.log(chalk.red(' └─ Please check if the Sui node is running.'));
188
- process.exit(1);
189
- }
190
-
191
- const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
192
- const spinner = SPINNER[spinnerIndex % SPINNER.length];
193
- spinnerIndex++;
194
-
195
- process.stdout.write(`\r ├─ ${spinner} Waiting for node... (${elapsedTime}s)`);
196
- await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
197
- }
198
- }
199
-
200
- // 显示 chainId
201
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
202
- console.log(` ├─ ChainId: ${chainId}`);
203
-
204
- // 第二阶段:检查部署账户余额
205
- retryCount = 0;
206
- while (retryCount < MAX_RETRIES && !isInterrupted) {
207
- try {
208
- const address = dubhe.getAddress();
209
- const coins = await dubhe.suiInteractor.currentClient.getCoins({
210
- owner: address,
211
- coinType: '0x2::sui::SUI'
212
- });
213
-
214
- if (coins.data.length > 0) {
215
- const balance = coins.data.reduce((sum, coin) => sum + Number(coin.balance), 0);
216
- if (balance > 0) {
217
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
218
- console.log(` ├─ Deployer balance: ${balance / 10 ** 9} SUI`);
219
- return chainId;
220
- } else if (!hasShownBalanceWarning) {
221
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
222
- console.log(
223
- chalk.yellow(
224
- ` ├─ Deployer balance: 0 SUI, please ensure your account has sufficient SUI balance`
225
- )
226
- );
227
- hasShownBalanceWarning = true;
228
- }
229
- } else if (!hasShownBalanceWarning) {
230
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
231
- console.log(
232
- chalk.yellow(
233
- ` ├─ No SUI coins found in deployer account, please ensure your account has sufficient SUI balance`
234
- )
235
- );
236
- hasShownBalanceWarning = true;
237
- }
238
-
239
- retryCount++;
240
- if (retryCount === MAX_RETRIES) {
241
- console.log(
242
- chalk.red(` └─ Deployer account has no SUI balance after ${MAX_RETRIES} attempts`)
243
- );
244
- console.log(chalk.red(' └─ Please ensure your account has sufficient SUI balance.'));
245
- process.exit(1);
246
- }
247
-
248
- const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
249
- const spinner = SPINNER[spinnerIndex % SPINNER.length];
250
- spinnerIndex++;
251
-
252
- process.stdout.write(`\r ├─ ${spinner} Checking deployer balance... (${elapsedTime}s)`);
253
- await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
254
- } catch (error) {
255
- if (isInterrupted) break;
256
-
257
- retryCount++;
258
- if (retryCount === MAX_RETRIES) {
259
- console.log(
260
- chalk.red(` └─ Failed to check deployer balance after ${MAX_RETRIES} attempts`)
261
- );
262
- console.log(chalk.red(' └─ Please check your account and network connection.'));
263
- process.exit(1);
264
- }
265
-
266
- const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
267
- const spinner = SPINNER[spinnerIndex % SPINNER.length];
268
- spinnerIndex++;
216
+ const chainId = await dubhe.suiInteractor.currentClient.getChainIdentifier();
217
+ console.log(` ├─ ChainId: ${chainId}`);
218
+ const address = dubhe.getAddress();
219
+ const coins = await dubhe.suiInteractor.currentClient.getCoins({
220
+ owner: address,
221
+ coinType: '0x2::sui::SUI'
222
+ });
269
223
 
270
- process.stdout.write(`\r ├─ ${spinner} Checking deployer balance... (${elapsedTime}s)`);
271
- await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
272
- }
224
+ if (coins.data.length > 0) {
225
+ const balance = coins.data.reduce((sum, coin) => sum + Number(coin.balance), 0);
226
+ if (balance > 0) {
227
+ console.log(` ├─ Deployer balance: ${balance / 10 ** 9} SUI`);
228
+ return chainId;
229
+ } else {
230
+ console.log(
231
+ chalk.yellow(
232
+ ` ├─ Deployer balance: 0 SUI, please ensure your account has sufficient SUI balance`
233
+ )
234
+ );
273
235
  }
274
- } finally {
275
- process.removeListener('SIGINT', handleInterrupt);
276
236
  }
277
-
278
- if (isInterrupted) {
279
- process.exit(0);
280
- }
281
-
282
- throw new Error('Failed to connect to node');
237
+ return chainId;
283
238
  }
284
239
 
285
240
  async function publishContract(
@@ -301,7 +256,61 @@ async function publishContract(
301
256
  console.log(` └─ Account: ${dubhe.getAddress()}`);
302
257
 
303
258
  console.log('\n📦 Building Contract...');
304
- const [modules, dependencies] = buildContract(projectPath);
259
+ // For localnet: pass the ephemeral pubfile so the build system can resolve
260
+ // the dubhe dependency that was just published in publishDubheFramework().
261
+ const pubfilePath =
262
+ network === 'localnet' ? getEphemeralPubFilePath(process.cwd(), network) : undefined;
263
+
264
+ // For localnet: temporarily remove Published.toml to prevent the testnet address
265
+ // (if it exists) from being picked up by --build-env testnet, which would cause
266
+ // PublishErrorNonZeroAddress. The pubfile supplies dubhe's localnet address instead.
267
+ const contractPublishedTomlPath = `${projectPath}/Published.toml`;
268
+ let savedContractPublishedToml: string | null = null;
269
+ if (network === 'localnet' && fs.existsSync(contractPublishedTomlPath)) {
270
+ savedContractPublishedToml = fs.readFileSync(contractPublishedTomlPath, 'utf-8');
271
+ fs.unlinkSync(contractPublishedTomlPath);
272
+ }
273
+
274
+ // For localnet: also temporarily remove dubhe's Published.toml when building the
275
+ // contract package. After publishDubheFramework restores dubhe/Published.toml, its
276
+ // testnet entry's original-id may be "0x0", which collides with the contract package's
277
+ // own address (also 0x0 before publish), triggering a spurious cyclic-dependency error.
278
+ const dubhePublishedTomlPath = path.join(path.dirname(projectPath), 'dubhe', 'Published.toml');
279
+ let savedDubhePublishedToml: string | null = null;
280
+ if (network === 'localnet' && fs.existsSync(dubhePublishedTomlPath)) {
281
+ savedDubhePublishedToml = fs.readFileSync(dubhePublishedTomlPath, 'utf-8');
282
+ fs.unlinkSync(dubhePublishedTomlPath);
283
+ }
284
+
285
+ // Sui CLI 1.40+ checks that the active environment is declared in Move.toml
286
+ // even when --build-env is specified. Temporarily inject localnet into [environments]
287
+ // for both the contract and its dubhe dependency.
288
+ const contractMoveTomlPath = `${projectPath}/Move.toml`;
289
+ const dubheMoveTomlPath = path.join(path.dirname(projectPath), 'dubhe', 'Move.toml');
290
+ let savedContractMoveToml: string | null = null;
291
+ let savedDubheMoveToml: string | null = null;
292
+ if (network === 'localnet') {
293
+ savedContractMoveToml = patchMoveTomlWithLocalnetEnv(contractMoveTomlPath, chainId);
294
+ savedDubheMoveToml = patchMoveTomlWithLocalnetEnv(dubheMoveTomlPath, chainId);
295
+ }
296
+
297
+ let modules: any, dependencies: any;
298
+ try {
299
+ [modules, dependencies] = buildContract(projectPath, network, pubfilePath);
300
+ } finally {
301
+ if (savedContractPublishedToml !== null) {
302
+ fs.writeFileSync(contractPublishedTomlPath, savedContractPublishedToml, 'utf-8');
303
+ }
304
+ if (savedDubhePublishedToml !== null) {
305
+ fs.writeFileSync(dubhePublishedTomlPath, savedDubhePublishedToml, 'utf-8');
306
+ }
307
+ if (savedContractMoveToml !== null) {
308
+ fs.writeFileSync(contractMoveTomlPath, savedContractMoveToml, 'utf-8');
309
+ }
310
+ if (savedDubheMoveToml !== null) {
311
+ fs.writeFileSync(dubheMoveTomlPath, savedDubheMoveToml, 'utf-8');
312
+ }
313
+ }
305
314
 
306
315
  console.log('\n🔄 Publishing Contract...');
307
316
  const tx = new Transaction();
@@ -311,63 +320,30 @@ async function publishContract(
311
320
  const [upgradeCap] = tx.publish({ modules, dependencies });
312
321
  tx.transferObjects([upgradeCap], dubhe.getAddress());
313
322
 
314
- let result: any = null;
315
- let retryCount = 0;
316
- let spinnerIndex = 0;
317
- const startTime = Date.now();
318
- let isInterrupted = false;
319
-
320
- const handleInterrupt = () => {
321
- isInterrupted = true;
322
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
323
- console.log('\n └─ Operation cancelled by user');
324
- process.exit(0);
325
- };
326
- process.on('SIGINT', handleInterrupt);
327
-
323
+ let result;
328
324
  try {
329
- while (retryCount < MAX_RETRIES && !result && !isInterrupted) {
330
- try {
331
- result = await dubhe.signAndSendTxn({ tx });
332
- } catch (error) {
333
- if (isInterrupted) break;
334
-
335
- retryCount++;
336
- if (retryCount === MAX_RETRIES) {
337
- console.log(chalk.red(` └─ Publication failed after ${MAX_RETRIES} attempts`));
338
- console.log(chalk.red(' └─ Please check your network connection and try again later.'));
339
- process.exit(1);
340
- }
341
-
342
- const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
343
- const spinner = SPINNER[spinnerIndex % SPINNER.length];
344
- spinnerIndex++;
345
-
346
- process.stdout.write(`\r ├─ ${spinner} Retrying... (${elapsedTime}s)`);
347
- await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
348
- }
349
- }
350
- } finally {
351
- process.removeListener('SIGINT', handleInterrupt);
352
- }
353
-
354
- if (isInterrupted) {
355
- process.exit(0);
325
+ result = await dubhe.signAndSendTxn({ tx });
326
+ } catch (error: any) {
327
+ console.error(chalk.red(' └─ Publication failed'));
328
+ console.error(error.message);
329
+ throw new Error(`Contract publication failed: ${error.message}`);
356
330
  }
357
331
 
358
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
359
-
360
332
  if (!result || result.effects?.status.status === 'failure') {
361
- console.log(chalk.red(' └─ Publication failed'));
362
- process.exit(1);
333
+ throw new Error('Contract publication transaction failed');
363
334
  }
364
335
 
365
336
  console.log(' ├─ Processing publication results...');
366
337
  let version = 1;
367
338
  let packageId = '';
368
- let schemaId = '';
369
- let schemas = dubheConfig.schemas;
339
+ let dappHub = '';
340
+ let components = dubheConfig.components;
341
+ let resources = dubheConfig.resources;
342
+ let enums = dubheConfig.enums;
370
343
  let upgradeCapId = '';
344
+ let startCheckpoint = '';
345
+
346
+ let printObjects: any[] = [];
371
347
 
372
348
  result.objectChanges!.map((object: ObjectChange) => {
373
349
  if (object.type === 'published') {
@@ -382,24 +358,35 @@ async function publishContract(
382
358
  console.log(` ├─ Upgrade Cap: ${object.objectId}`);
383
359
  upgradeCapId = object.objectId || '';
384
360
  }
361
+ if (
362
+ object.type === 'created' &&
363
+ object.objectType &&
364
+ object.objectType.includes('dapp_service::DappHub')
365
+ ) {
366
+ dappHub = object.objectId || '';
367
+ }
368
+ if (object.type === 'created') {
369
+ printObjects.push(object);
370
+ }
385
371
  });
386
372
 
387
373
  console.log(` └─ Transaction: ${result.digest}`);
388
374
 
389
375
  updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
376
+ updatePublishedToml(projectPath, network, chainId, packageId, packageId, 1);
390
377
 
391
378
  console.log('\n⚡ Executing Deploy Hook...');
392
379
  await delay(5000);
393
380
 
381
+ startCheckpoint = await dubhe.suiInteractor.currentClient.getLatestCheckpointSequenceNumber();
382
+
394
383
  const deployHookTx = new Transaction();
395
384
  let args = [];
396
- if (dubheConfig.name !== 'dubhe') {
397
- let dubheSchemaId = await getDubheSchemaId(network);
398
- args.push(deployHookTx.object(dubheSchemaId));
399
- }
385
+ let dubheDappHub = dubheConfig.name === 'dubhe' ? dappHub : await getDubheDappHub(network);
386
+ args.push(deployHookTx.object(dubheDappHub));
400
387
  args.push(deployHookTx.object('0x6'));
401
388
  deployHookTx.moveCall({
402
- target: `${packageId}::${dubheConfig.name}_genesis::run`,
389
+ target: `${packageId}::genesis::run`,
403
390
  arguments: args
404
391
  });
405
392
 
@@ -409,49 +396,46 @@ async function publishContract(
409
396
  } catch (error: any) {
410
397
  console.error(chalk.red(' └─ Deploy hook execution failed'));
411
398
  console.error(error.message);
412
- process.exit(1);
399
+ throw new Error(`genesis::run failed: ${error.message}`);
413
400
  }
414
401
 
415
402
  if (deployHookResult.effects?.status.status === 'success') {
416
403
  console.log(' ├─ Hook execution successful');
417
404
  console.log(` ├─ Transaction: ${deployHookResult.digest}`);
418
405
 
419
- console.log('\n📋 Created Schemas:');
420
- deployHookResult.objectChanges?.map((object: ObjectChange) => {
421
- if (
422
- object.type === 'created' &&
423
- object.objectType &&
424
- object.objectType.includes('schema::Schema')
425
- ) {
426
- schemaId = object.objectId || '';
427
- }
428
- if (
429
- object.type === 'created' &&
430
- object.objectType &&
431
- object.objectType.includes('schema') &&
432
- !object.objectType.includes('dynamic_field')
433
- ) {
434
- console.log(` ├─ Type: ${object.objectType}`);
435
- console.log(` └─ ID: ${object.objectId}`);
436
- }
406
+ console.log('\n📋 Created Objects:');
407
+ printObjects.map((object: ObjectChange) => {
408
+ console.log(` ├─ ID: ${object.objectId}`);
409
+ console.log(` └─ Type: ${object.objectType}`);
437
410
  });
438
411
 
439
- saveContractData(
412
+ await saveContractData(
440
413
  dubheConfig.name,
441
414
  network,
415
+ startCheckpoint,
442
416
  packageId,
443
- schemaId,
417
+ dubheDappHub,
444
418
  upgradeCapId,
445
419
  version,
446
- schemas
420
+ components,
421
+ resources,
422
+ enums
447
423
  );
424
+
425
+ await saveMetadata(dubheConfig.name, network, packageId);
426
+
427
+ // Insert package id to dubhe config
428
+ let config = JSON.parse(fs.readFileSync(`${process.cwd()}/dubhe.config.json`, 'utf-8'));
429
+ config.original_package_id = packageId;
430
+ config.dubhe_object_id = dubheDappHub;
431
+ config.original_dubhe_package_id = await getOriginalDubhePackageId(network);
432
+ config.start_checkpoint = startCheckpoint;
433
+
434
+ fs.writeFileSync(`${process.cwd()}/dubhe.config.json`, JSON.stringify(config, null, 2));
435
+
448
436
  console.log('\n✅ Contract Publication Complete\n');
449
437
  } else {
450
- console.log(chalk.yellow(' └─ Deploy hook execution failed'));
451
- console.log(chalk.yellow(' Please republish or manually call deploy_hook::run'));
452
- console.log(chalk.yellow(' Please check the transaction digest:'));
453
- console.log(chalk.yellow(` ${deployHookResult.digest}`));
454
- process.exit(1);
438
+ throw new Error(`genesis::run transaction failed. Digest: ${deployHookResult.digest}`);
455
439
  }
456
440
  }
457
441
 
@@ -460,15 +444,13 @@ async function checkDubheFramework(projectPath: string): Promise<boolean> {
460
444
  console.log(chalk.yellow('\nℹ️ Dubhe Framework Files Not Found'));
461
445
  console.log(chalk.yellow(' ├─ Expected Path:'), projectPath);
462
446
  console.log(chalk.yellow(' ├─ To set up Dubhe Framework:'));
463
- console.log(chalk.yellow(' │ 1. Create directory: mkdir -p contracts/dubhe-framework'));
447
+ console.log(chalk.yellow(' │ 1. Create directory: mkdir -p contracts/dubhe'));
464
448
  console.log(
465
449
  chalk.yellow(
466
- ' │ 2. Clone repository: git clone https://github.com/0xobelisk/dubhe-framework contracts/dubhe-framework'
450
+ ' │ 2. Clone repository: git clone https://github.com/0xobelisk/dubhe contracts/dubhe'
467
451
  )
468
452
  );
469
- console.log(
470
- chalk.yellow(' │ 3. Or download from: https://github.com/0xobelisk/dubhe-framework')
471
- );
453
+ console.log(chalk.yellow(' │ 3. Or download from: https://github.com/0xobelisk/dubhe'));
472
454
  console.log(chalk.yellow(' └─ After setup, restart the local node'));
473
455
  return false;
474
456
  }
@@ -479,8 +461,8 @@ export async function publishDubheFramework(
479
461
  dubhe: Dubhe,
480
462
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
481
463
  ) {
482
- const path = process.cwd();
483
- const projectPath = `${path}/contracts/dubhe-framework`;
464
+ const cwd = process.cwd();
465
+ const projectPath = `${cwd}/src/dubhe`;
484
466
 
485
467
  if (!(await checkDubheFramework(projectPath))) {
486
468
  return;
@@ -492,66 +474,66 @@ export async function publishDubheFramework(
492
474
  const chainId = await waitForNode(dubhe);
493
475
 
494
476
  await removeEnvContent(`${projectPath}/Move.lock`, network);
495
- const [modules, dependencies] = buildContract(projectPath);
496
- const tx = new Transaction();
497
- const [upgradeCap] = tx.publish({ modules, dependencies });
498
- tx.transferObjects([upgradeCap], dubhe.getAddress());
477
+ await updateMoveTomlAddress(projectPath, '0x0');
478
+
479
+ const startCheckpoint =
480
+ await dubhe.suiInteractor.currentClient.getLatestCheckpointSequenceNumber();
481
+
482
+ // For localnet: --build-env testnet is used to resolve git dependencies, but the
483
+ // Move CLI will also read Published.toml and use any existing testnet address for
484
+ // dubhe — causing PublishErrorNonZeroAddress if a testnet entry already exists.
485
+ // Fix: temporarily remove Published.toml before the build, then restore it.
486
+ // This ensures the dubhe package compiles with address 0x0 (from Move.toml).
487
+ const publishedTomlPath = `${projectPath}/Published.toml`;
488
+ let savedPublishedTomlContent: string | null = null;
489
+ if (network === 'localnet' && fs.existsSync(publishedTomlPath)) {
490
+ savedPublishedTomlContent = fs.readFileSync(publishedTomlPath, 'utf-8');
491
+ fs.unlinkSync(publishedTomlPath);
492
+ }
499
493
 
500
- let result: any = null;
501
- let retryCount = 0;
502
- let spinnerIndex = 0;
503
- const startTime = Date.now();
504
- let isInterrupted = false;
505
-
506
- const handleInterrupt = () => {
507
- isInterrupted = true;
508
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
509
- console.log('\n └─ Operation cancelled by user');
510
- process.exit(0);
511
- };
512
- process.on('SIGINT', handleInterrupt);
494
+ // Sui CLI 1.40+ checks that the active environment is declared in Move.toml
495
+ // even when --build-env is specified. Temporarily inject localnet into [environments].
496
+ const moveTomlPath = `${projectPath}/Move.toml`;
497
+ let savedMoveTomlContent: string | null = null;
498
+ if (network === 'localnet') {
499
+ savedMoveTomlContent = patchMoveTomlWithLocalnetEnv(moveTomlPath, chainId);
500
+ }
513
501
 
502
+ let modules: any, dependencies: any;
514
503
  try {
515
- while (retryCount < MAX_RETRIES && !result && !isInterrupted) {
516
- try {
517
- result = await dubhe.signAndSendTxn({ tx });
518
- } catch (error) {
519
- if (isInterrupted) break;
520
-
521
- retryCount++;
522
- if (retryCount === MAX_RETRIES) {
523
- console.log(chalk.red(` └─ Publication failed after ${MAX_RETRIES} attempts`));
524
- console.log(chalk.red(' └─ Please check your network connection and try again later.'));
525
- process.exit(1);
526
- }
527
-
528
- const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
529
- const spinner = SPINNER[spinnerIndex % SPINNER.length];
530
- spinnerIndex++;
531
-
532
- process.stdout.write(`\r ├─ ${spinner} Retrying... (${elapsedTime}s)`);
533
- await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
534
- }
535
- }
504
+ // For localnet: use --build-env testnet (no pubfile needed dubhe has no local deps).
505
+ // For testnet/mainnet: use -e <network> as usual.
506
+ [modules, dependencies] = buildContract(projectPath, network);
536
507
  } finally {
537
- process.removeListener('SIGINT', handleInterrupt);
508
+ // Always restore Published.toml and Move.toml (successful build or error)
509
+ if (savedPublishedTomlContent !== null) {
510
+ fs.writeFileSync(publishedTomlPath, savedPublishedTomlContent, 'utf-8');
511
+ }
512
+ if (savedMoveTomlContent !== null) {
513
+ fs.writeFileSync(moveTomlPath, savedMoveTomlContent, 'utf-8');
514
+ }
538
515
  }
539
516
 
540
- if (isInterrupted) {
541
- process.exit(0);
542
- }
517
+ const tx = new Transaction();
518
+ const [upgradeCap] = tx.publish({ modules, dependencies });
519
+ tx.transferObjects([upgradeCap], dubhe.getAddress());
543
520
 
544
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
521
+ let result;
522
+ try {
523
+ result = await dubhe.signAndSendTxn({ tx });
524
+ } catch (error: any) {
525
+ console.error(chalk.red(' └─ Publication failed'));
526
+ console.error(error.message);
527
+ throw new Error(`Dubhe framework publication failed: ${error.message}`);
528
+ }
545
529
 
546
530
  if (!result || result.effects?.status.status === 'failure') {
547
- console.log(chalk.red(' └─ Publication failed'));
548
- process.exit(1);
531
+ throw new Error('Dubhe framework publication transaction failed');
549
532
  }
550
533
 
551
534
  let version = 1;
552
535
  let packageId = '';
553
- let schemaId = '';
554
- let schemas: Record<string, string> = {};
536
+ let dappHub = '';
555
537
  let upgradeCapId = '';
556
538
 
557
539
  result.objectChanges!.map((object: ObjectChange) => {
@@ -565,14 +547,20 @@ export async function publishDubheFramework(
565
547
  ) {
566
548
  upgradeCapId = object.objectId || '';
567
549
  }
550
+ if (
551
+ object.type === 'created' &&
552
+ object.objectType &&
553
+ object.objectType.includes('dapp_service::DappHub')
554
+ ) {
555
+ dappHub = object.objectId || '';
556
+ }
568
557
  });
569
558
 
570
559
  await delay(3000);
571
-
572
560
  const deployHookTx = new Transaction();
573
561
  deployHookTx.moveCall({
574
- target: `${packageId}::dubhe_genesis::run`,
575
- arguments: [deployHookTx.object('0x6')]
562
+ target: `${packageId}::genesis::run`,
563
+ arguments: [deployHookTx.object(dappHub), deployHookTx.object('0x6')]
576
564
  });
577
565
 
578
566
  let deployHookResult;
@@ -581,30 +569,47 @@ export async function publishDubheFramework(
581
569
  } catch (error: any) {
582
570
  console.error(chalk.red(' └─ Deploy hook execution failed'));
583
571
  console.error(error.message);
584
- process.exit(1);
572
+ throw new Error(`Dubhe genesis::run failed: ${error.message}`);
585
573
  }
586
574
 
587
- if (deployHookResult.effects?.status.status === 'success') {
588
- deployHookResult.objectChanges?.map((object: ObjectChange) => {
589
- if (
590
- object.type === 'created' &&
591
- object.objectType &&
592
- object.objectType.includes('dubhe_schema::Schema')
593
- ) {
594
- schemaId = object.objectId || '';
595
- }
596
- });
575
+ if (deployHookResult.effects?.status.status !== 'success') {
576
+ throw new Error('Deploy hook execution failed');
597
577
  }
598
578
 
599
- saveContractData('dubhe-framework', network, packageId, schemaId, upgradeCapId, version, schemas);
579
+ await updateMoveTomlAddress(projectPath, packageId);
580
+ await saveContractData(
581
+ 'dubhe',
582
+ network,
583
+ startCheckpoint,
584
+ packageId,
585
+ dappHub,
586
+ upgradeCapId,
587
+ version,
588
+ {},
589
+ {},
590
+ {}
591
+ );
600
592
 
601
593
  updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
602
- await delay(1000);
594
+ updatePublishedToml(projectPath, network, chainId, packageId, packageId, 1);
595
+
596
+ // For localnet: write dubhe's published address to Pub.localnet.toml so that
597
+ // the counter package build (next step) can resolve the dubhe dependency.
598
+ if (network === 'localnet') {
599
+ const pubfilePath = getEphemeralPubFilePath(cwd, network);
600
+ updateEphemeralPubFile(pubfilePath, chainId, 'testnet', {
601
+ source: projectPath,
602
+ publishedAt: packageId,
603
+ originalId: packageId,
604
+ upgradeCap: upgradeCapId
605
+ });
606
+ }
603
607
  }
604
608
 
605
609
  export async function publishHandler(
606
610
  dubheConfig: DubheConfig,
607
611
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
612
+ _force: boolean,
608
613
  gasBudget?: number
609
614
  ) {
610
615
  await switchEnv(network);
@@ -613,12 +618,12 @@ export async function publishHandler(
613
618
  network
614
619
  });
615
620
 
616
- if (network === 'localnet') {
621
+ const path = process.cwd();
622
+ const projectPath = `${path}/src/${dubheConfig.name}`;
623
+
624
+ if (network === 'localnet' && dubheConfig.name !== 'dubhe') {
617
625
  await publishDubheFramework(dubhe, network);
618
626
  }
619
627
 
620
- const path = process.cwd();
621
- const projectPath = `${path}/contracts/${dubheConfig.name}`;
622
- await updateDubheDependency(`${projectPath}/Move.toml`, network);
623
628
  await publishContract(dubhe, dubheConfig, network, projectPath, gasBudget);
624
629
  }