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