@0xobelisk/sui-cli 1.2.0-pre.98 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,127 +6,48 @@ import {
6
6
  updateMoveTomlAddress,
7
7
  switchEnv,
8
8
  delay,
9
- getDubheDappHub,
9
+ getDubheDappHubId,
10
10
  initializeDubhe,
11
11
  saveMetadata,
12
12
  getOriginalDubhePackageId,
13
13
  updatePublishedToml,
14
+ syncDubheFrameworkAddress,
14
15
  updateEphemeralPubFile,
15
- getEphemeralPubFilePath
16
+ getEphemeralPubFilePath,
17
+ getPublishedTomlEntry,
18
+ clearPublishedTomlEntry,
19
+ restorePublishedTomlEntry,
20
+ removeEnvFromMoveLock
16
21
  } from './utils';
17
22
  import { DubheConfig } from '@0xobelisk/sui-common';
18
23
  import * as fs from 'fs';
19
24
  import * as path from 'path';
20
25
 
21
- async function removeEnvContent(
22
- filePath: string,
23
- networkType: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
24
- ): Promise<void> {
25
- if (!fs.existsSync(filePath)) {
26
- return;
27
- }
28
- const content = fs.readFileSync(filePath, 'utf-8');
29
- const regex = new RegExp(`\\[env\\.${networkType}\\][\\s\\S]*?(?=\\[|$)`, 'g');
30
- const updatedContent = content.replace(regex, '');
31
- fs.writeFileSync(filePath, updatedContent, 'utf-8');
32
- }
26
+ /**
27
+ * Temporarily add localnet to Move.toml [environments] section before building.
28
+ * Sui CLI 1.40+ requires the active environment to be declared in Move.toml even
29
+ * when --build-env is specified. This patches the file and returns the original
30
+ * content so the caller can restore it in a finally block.
31
+ * Returns null if no changes were needed.
32
+ */
33
+ function patchMoveTomlWithLocalnetEnv(moveTomlPath: string, chainId: string): string | null {
34
+ if (!fs.existsSync(moveTomlPath)) return null;
35
+ const content = fs.readFileSync(moveTomlPath, 'utf-8');
33
36
 
34
- interface EnvConfig {
35
- chainId: string;
36
- originalPublishedId: string;
37
- latestPublishedId: string;
38
- publishedVersion: number;
39
- }
37
+ if (content.includes('localnet')) {
38
+ return null;
39
+ }
40
40
 
41
- function updateEnvFile(
42
- filePath: string,
43
- networkType: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
44
- operation: 'publish' | 'upgrade',
45
- chainId: string,
46
- publishedId: string
47
- ): void {
48
- const envFilePath = path.resolve(filePath);
49
- const envContent = fs.readFileSync(envFilePath, 'utf-8');
50
- const envLines = envContent.split('\n');
51
-
52
- const networkSectionIndex = envLines.findIndex((line) => line.trim() === `[env.${networkType}]`);
53
- const config: EnvConfig = {
54
- chainId: chainId,
55
- originalPublishedId: '',
56
- latestPublishedId: '',
57
- publishedVersion: 0
58
- };
59
-
60
- if (networkSectionIndex === -1) {
61
- // If network section is not found, add a new section
62
- if (operation === 'publish') {
63
- config.originalPublishedId = publishedId;
64
- config.latestPublishedId = publishedId;
65
- config.publishedVersion = 1;
66
- } else {
67
- throw new Error(
68
- `Network type [env.${networkType}] not found in the file and cannot upgrade.`
69
- );
70
- }
41
+ let updatedContent: string;
42
+ if (content.includes('[environments]')) {
43
+ updatedContent = content.replace('[environments]', `[environments]\nlocalnet = "${chainId}"`);
71
44
  } else {
72
- for (let i = networkSectionIndex + 1; i < envLines.length; i++) {
73
- const line = envLines[i].trim();
74
- if (line.startsWith('[')) break; // End of the current network section
75
-
76
- const [key, value] = line.split('=').map((part) => part.trim().replace(/"/g, ''));
77
- switch (key) {
78
- case 'original-published-id':
79
- config.originalPublishedId = value;
80
- break;
81
- case 'latest-published-id':
82
- config.latestPublishedId = value;
83
- break;
84
- case 'published-version':
85
- config.publishedVersion = parseInt(value, 10);
86
- break;
87
- }
88
- }
89
-
90
- if (operation === 'publish') {
91
- config.originalPublishedId = publishedId;
92
- config.latestPublishedId = publishedId;
93
- config.publishedVersion = 1;
94
- } else if (operation === 'upgrade') {
95
- config.latestPublishedId = publishedId;
96
- config.publishedVersion += 1;
97
- }
45
+ updatedContent = content.trimEnd() + `\n\n[environments]\nlocalnet = "${chainId}"\n`;
98
46
  }
99
47
 
100
- const updatedSection = `
101
- [env.${networkType}]
102
- chain-id = "${config.chainId}"
103
- original-published-id = "${config.originalPublishedId}"
104
- latest-published-id = "${config.latestPublishedId}"
105
- published-version = "${config.publishedVersion}"
106
- `;
107
-
108
- const newEnvContent =
109
- networkSectionIndex === -1
110
- ? envContent + updatedSection
111
- : envLines.slice(0, networkSectionIndex).join('\n') + updatedSection;
112
-
113
- fs.writeFileSync(envFilePath, newEnvContent, 'utf-8');
48
+ fs.writeFileSync(moveTomlPath, updatedContent, 'utf-8');
49
+ return content;
114
50
  }
115
- // function capitalizeAndRemoveUnderscores(input: string): string {
116
- // return input
117
- // .split('_')
118
- // .map((word, index) => {
119
- // return index === 0
120
- // ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
121
- // : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
122
- // })
123
- // .join('');
124
- // }
125
- //
126
- // function getLastSegment(input: string): string {
127
- // const segments = input.split('::');
128
- // return segments.length > 0 ? segments[segments.length - 1] : '';
129
- // }
130
51
 
131
52
  /**
132
53
  * Build a Move package and return [modules, dependencies] as base64 arrays.
@@ -216,7 +137,9 @@ async function publishContract(
216
137
  dubheConfig: DubheConfig,
217
138
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
218
139
  projectPath: string,
219
- gasBudget?: number
140
+ gasBudget?: number,
141
+ force?: boolean,
142
+ fullnodeUrls?: string[]
220
143
  ) {
221
144
  console.log('\n🚀 Starting Contract Publication...');
222
145
  console.log(` ├─ Project: ${projectPath}`);
@@ -226,23 +149,74 @@ async function publishContract(
226
149
  const chainId = await waitForNode(dubhe);
227
150
  console.log(' ├─ Validating Environment...');
228
151
 
229
- await removeEnvContent(`${projectPath}/Move.lock`, network);
152
+ removeEnvFromMoveLock(`${projectPath}/Move.lock`, network);
230
153
  console.log(` └─ Account: ${dubhe.getAddress()}`);
231
154
 
155
+ // Ensure src/dubhe/Published.toml references the canonical framework address
156
+ // for this network before building. This is a no-op when the address is
157
+ // already current, and automatically corrects stale entries whenever the
158
+ // framework is redeployed on testnet/mainnet without a manual file update.
159
+ if (dubheConfig.name !== 'dubhe') {
160
+ syncDubheFrameworkAddress(process.cwd(), network, chainId);
161
+ }
162
+
232
163
  console.log('\n📦 Building Contract...');
233
164
  // For localnet: pass the ephemeral pubfile so the build system can resolve
234
165
  // the dubhe dependency that was just published in publishDubheFramework().
235
- const pubfilePath =
166
+ // If the file was written for a different chain (node restarted with
167
+ // --force-regenesis), discard it to avoid a chain-id mismatch build error.
168
+ let pubfilePath =
236
169
  network === 'localnet' ? getEphemeralPubFilePath(process.cwd(), network) : undefined;
170
+ if (pubfilePath && fs.existsSync(pubfilePath)) {
171
+ const pubfileContent = fs.readFileSync(pubfilePath, 'utf-8');
172
+ const chainIdMatch = pubfileContent.match(/^chain-id\s*=\s*"([^"]*)"/m);
173
+ const pubfileChainId = chainIdMatch ? chainIdMatch[1] : '';
174
+ if (pubfileChainId && pubfileChainId !== chainId) {
175
+ console.log(
176
+ chalk.yellow(
177
+ ` ├─ Stale Pub.localnet.toml (chain ${pubfileChainId} → ${chainId}), discarding`
178
+ )
179
+ );
180
+ fs.unlinkSync(pubfilePath);
181
+ pubfilePath = undefined;
182
+ }
183
+ }
237
184
 
238
- // For localnet: temporarily remove Published.toml to prevent the testnet address
239
- // (if it exists) from being picked up by --build-env testnet, which would cause
240
- // PublishErrorNonZeroAddress. The pubfile supplies dubhe's localnet address instead.
185
+ // Move.toml paths declared early so both the Published.toml handling block and
186
+ // the localnet env-patching block can reference them.
187
+ const contractMoveTomlPath = `${projectPath}/Move.toml`;
188
+ const dubheMoveTomlPath = path.join(path.dirname(projectPath), 'dubhe', 'Move.toml');
189
+ let savedContractMoveToml: string | null = null;
190
+ let savedDubheMoveToml: string | null = null;
191
+
192
+ // So the build uses package address 0x0: for localnet always remove the contract's
193
+ // Published.toml; for testnet/mainnet/devnet only when --force (clear current network entry).
194
+ // Otherwise Sui CLI bakes the existing [published.<network>] address into the bytecode and
195
+ // the chain rejects with PublishErrorNonZeroAddress.
241
196
  const contractPublishedTomlPath = `${projectPath}/Published.toml`;
242
197
  let savedContractPublishedToml: string | null = null;
198
+ let savedContractPublishedEntry: {
199
+ network: string;
200
+ entry: Exclude<ReturnType<typeof getPublishedTomlEntry>, undefined>;
201
+ } | null = null;
243
202
  if (network === 'localnet' && fs.existsSync(contractPublishedTomlPath)) {
244
203
  savedContractPublishedToml = fs.readFileSync(contractPublishedTomlPath, 'utf-8');
245
204
  fs.unlinkSync(contractPublishedTomlPath);
205
+ } else if (network === 'testnet' || network === 'mainnet' || network === 'devnet') {
206
+ const entry = getPublishedTomlEntry(projectPath, network);
207
+ if (entry && force) {
208
+ // Existing entry + --force: clear it so the build uses 0x0 instead of the old address.
209
+ savedContractPublishedEntry = { network, entry };
210
+ clearPublishedTomlEntry(projectPath, network);
211
+ } else if (!entry) {
212
+ // No Published.toml entry for this network (first-time deploy to this network).
213
+ // The Sui CLI has no per-network override and falls back to Move.toml's [addresses]
214
+ // value, which may be non-zero from a previous deployment on a different network,
215
+ // causing PublishErrorNonZeroAddress.
216
+ // Temporarily zero out Move.toml before building so the self-address is 0x0.
217
+ savedContractMoveToml = fs.readFileSync(contractMoveTomlPath, 'utf-8');
218
+ updateMoveTomlAddress(projectPath, '0x0');
219
+ }
246
220
  }
247
221
 
248
222
  // For localnet: also temporarily remove dubhe's Published.toml when building the
@@ -256,6 +230,14 @@ async function publishContract(
256
230
  fs.unlinkSync(dubhePublishedTomlPath);
257
231
  }
258
232
 
233
+ // Sui CLI 1.40+ checks that the active environment is declared in Move.toml
234
+ // even when --build-env is specified. Temporarily inject localnet into [environments]
235
+ // for both the contract and its dubhe dependency.
236
+ if (network === 'localnet') {
237
+ savedContractMoveToml = patchMoveTomlWithLocalnetEnv(contractMoveTomlPath, chainId);
238
+ savedDubheMoveToml = patchMoveTomlWithLocalnetEnv(dubheMoveTomlPath, chainId);
239
+ }
240
+
259
241
  let modules: any, dependencies: any;
260
242
  try {
261
243
  [modules, dependencies] = buildContract(projectPath, network, pubfilePath);
@@ -263,9 +245,22 @@ async function publishContract(
263
245
  if (savedContractPublishedToml !== null) {
264
246
  fs.writeFileSync(contractPublishedTomlPath, savedContractPublishedToml, 'utf-8');
265
247
  }
248
+ if (savedContractPublishedEntry !== null) {
249
+ restorePublishedTomlEntry(
250
+ projectPath,
251
+ savedContractPublishedEntry.network,
252
+ savedContractPublishedEntry.entry
253
+ );
254
+ }
266
255
  if (savedDubhePublishedToml !== null) {
267
256
  fs.writeFileSync(dubhePublishedTomlPath, savedDubhePublishedToml, 'utf-8');
268
257
  }
258
+ if (savedContractMoveToml !== null) {
259
+ fs.writeFileSync(contractMoveTomlPath, savedContractMoveToml, 'utf-8');
260
+ }
261
+ if (savedDubheMoveToml !== null) {
262
+ fs.writeFileSync(dubheMoveTomlPath, savedDubheMoveToml, 'utf-8');
263
+ }
269
264
  }
270
265
 
271
266
  console.log('\n🔄 Publishing Contract...');
@@ -282,6 +277,17 @@ async function publishContract(
282
277
  } catch (error: any) {
283
278
  console.error(chalk.red(' └─ Publication failed'));
284
279
  console.error(error.message);
280
+ if (
281
+ !force &&
282
+ (network === 'testnet' || network === 'mainnet' || network === 'devnet') &&
283
+ /PublishErrorNonZeroAddress/i.test(String(error?.message))
284
+ ) {
285
+ console.error(
286
+ chalk.yellow(
287
+ ' 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.'
288
+ )
289
+ );
290
+ }
285
291
  throw new Error(`Contract publication failed: ${error.message}`);
286
292
  }
287
293
 
@@ -292,9 +298,8 @@ async function publishContract(
292
298
  console.log(' ├─ Processing publication results...');
293
299
  let version = 1;
294
300
  let packageId = '';
295
- let dappHub = '';
296
- let components = dubheConfig.components;
297
- let resources = dubheConfig.resources;
301
+ let dappHubId = '';
302
+ let resources = dubheConfig.resources ?? {};
298
303
  let enums = dubheConfig.enums;
299
304
  let upgradeCapId = '';
300
305
  let startCheckpoint = '';
@@ -319,7 +324,7 @@ async function publishContract(
319
324
  object.objectType &&
320
325
  object.objectType.includes('dapp_service::DappHub')
321
326
  ) {
322
- dappHub = object.objectId || '';
327
+ dappHubId = object.objectId || '';
323
328
  }
324
329
  if (object.type === 'created') {
325
330
  printObjects.push(object);
@@ -328,7 +333,6 @@ async function publishContract(
328
333
 
329
334
  console.log(` └─ Transaction: ${result.digest}`);
330
335
 
331
- updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
332
336
  updatePublishedToml(projectPath, network, chainId, packageId, packageId, 1);
333
337
 
334
338
  console.log('\n⚡ Executing Deploy Hook...');
@@ -338,9 +342,14 @@ async function publishContract(
338
342
 
339
343
  const deployHookTx = new Transaction();
340
344
  let args = [];
341
- let dubheDappHub = dubheConfig.name === 'dubhe' ? dappHub : await getDubheDappHub(network);
342
- args.push(deployHookTx.object(dubheDappHub));
343
- args.push(deployHookTx.object('0x6'));
345
+ let frameworkDappHubId =
346
+ dubheConfig.name === 'dubhe' ? dappHubId : await getDubheDappHubId(network);
347
+ args.push(deployHookTx.object(frameworkDappHubId));
348
+ // Dubhe framework genesis::run(dapp_hub, ctx) does not take clock.
349
+ // DApp genesis::run(dapp_hub, clock, ctx) still takes clock.
350
+ if (dubheConfig.name !== 'dubhe') {
351
+ args.push(deployHookTx.object('0x6'));
352
+ }
344
353
  deployHookTx.moveCall({
345
354
  target: `${packageId}::genesis::run`,
346
355
  arguments: args
@@ -359,6 +368,20 @@ async function publishContract(
359
368
  console.log(' ├─ Hook execution successful');
360
369
  console.log(` ├─ Transaction: ${deployHookResult.digest}`);
361
370
 
371
+ // Capture the DappStorage object created by genesis::run so we can persist
372
+ // its ID for later use in migrate_to_vN transactions during upgrades.
373
+ let dappStorageId = '';
374
+ deployHookResult.objectChanges!.map((object: ObjectChange) => {
375
+ if (
376
+ object.type === 'created' &&
377
+ object.objectType &&
378
+ object.objectType.includes('dapp_service::DappStorage')
379
+ ) {
380
+ dappStorageId = object.objectId || '';
381
+ console.log(` ├─ DappStorage: ${dappStorageId}`);
382
+ }
383
+ });
384
+
362
385
  console.log('\n📋 Created Objects:');
363
386
  printObjects.map((object: ObjectChange) => {
364
387
  console.log(` ├─ ID: ${object.objectId}`);
@@ -370,22 +393,48 @@ async function publishContract(
370
393
  network,
371
394
  startCheckpoint,
372
395
  packageId,
373
- dubheDappHub,
396
+ packageId, // originalPackageId: first publish, so original == current
397
+ frameworkDappHubId,
374
398
  upgradeCapId,
375
399
  version,
376
- components,
377
400
  resources,
378
- enums
401
+ enums,
402
+ // localnet: persist the locally deployed framework ID so the SDK can be
403
+ // initialised without hardcoding it. testnet/mainnet use a well-known
404
+ // constant already embedded in the SDK defaults, so we store undefined.
405
+ network === 'localnet' ? await getOriginalDubhePackageId(network) : undefined,
406
+ dappStorageId || undefined
379
407
  );
380
408
 
381
- await saveMetadata(dubheConfig.name, network, packageId);
409
+ await saveMetadata(dubheConfig.name, network, packageId, fullnodeUrls);
382
410
 
383
411
  // Insert package id to dubhe config
384
412
  let config = JSON.parse(fs.readFileSync(`${process.cwd()}/dubhe.config.json`, 'utf-8'));
385
413
  config.original_package_id = packageId;
386
- config.dubhe_object_id = dubheDappHub;
387
- config.original_dubhe_package_id = await getOriginalDubhePackageId(network);
414
+ // Initialize the trusted package address list. The indexer uses this list to verify
415
+ // event.type_.address so that forged events from other contracts are rejected even
416
+ // when they embed the same dapp_key string.
417
+ config.package_ids = [packageId];
418
+ config.dubhe_object_id = frameworkDappHubId;
419
+ // When deploying the dubhe framework itself, the "original dubhe package ID" is
420
+ // the package we just published. For user packages, look up the well-known
421
+ // framework address for the target network from the client config.
422
+ // devnet has no fixed framework deployment so we skip this field.
423
+ if (dubheConfig.name === 'dubhe') {
424
+ config.original_dubhe_package_id = packageId;
425
+ } else if (network !== 'devnet') {
426
+ config.original_dubhe_package_id = await getOriginalDubhePackageId(network);
427
+ }
388
428
  config.start_checkpoint = startCheckpoint;
429
+ // Canonical dapp_key type string: stable across upgrades, no "0x" prefix, padded to 64 hex chars.
430
+ // Matches the Move type_name::with_defining_ids<DappKey>().into_string() format.
431
+ const pkgHex = packageId.replace(/^0x/i, '').padStart(64, '0');
432
+ config.dapp_key = `${pkgHex}::dapp_key::DappKey`;
433
+ // Persist the DappStorage object ID so store-config can include it in deployment.ts
434
+ // and upgrade transactions can reference it without reading from .history.
435
+ if (dappStorageId) {
436
+ config.dapp_storage_id = dappStorageId;
437
+ }
389
438
 
390
439
  fs.writeFileSync(`${process.cwd()}/dubhe.config.json`, JSON.stringify(config, null, 2));
391
440
 
@@ -400,10 +449,10 @@ async function checkDubheFramework(projectPath: string): Promise<boolean> {
400
449
  console.log(chalk.yellow('\nℹ️ Dubhe Framework Files Not Found'));
401
450
  console.log(chalk.yellow(' ├─ Expected Path:'), projectPath);
402
451
  console.log(chalk.yellow(' ├─ To set up Dubhe Framework:'));
403
- console.log(chalk.yellow(' │ 1. Create directory: mkdir -p contracts/dubhe'));
452
+ console.log(chalk.yellow(' │ 1. Create directory: mkdir -p src/dubhe'));
404
453
  console.log(
405
454
  chalk.yellow(
406
- ' │ 2. Clone repository: git clone https://github.com/0xobelisk/dubhe contracts/dubhe'
455
+ ' │ 2. Clone repository: git clone https://github.com/0xobelisk/dubhe src/dubhe'
407
456
  )
408
457
  );
409
458
  console.log(chalk.yellow(' │ 3. Or download from: https://github.com/0xobelisk/dubhe'));
@@ -429,7 +478,15 @@ export async function publishDubheFramework(
429
478
 
430
479
  const chainId = await waitForNode(dubhe);
431
480
 
432
- await removeEnvContent(`${projectPath}/Move.lock`, network);
481
+ removeEnvFromMoveLock(`${projectPath}/Move.lock`, network);
482
+ if (network === 'localnet') {
483
+ // When building with --build-env testnet, Sui CLI reads Move.lock's [env.testnet] section
484
+ // and bakes its original-published-id (non-zero for a previously published dubhe) into the
485
+ // bytecode as the package self-address. Publishing then fails with PublishErrorNonZeroAddress
486
+ // because Sui requires the self-address to be 0x0 for a first-time publish.
487
+ // Fix: clear the testnet env section before building so the CLI uses 0x0 from Move.toml.
488
+ removeEnvFromMoveLock(`${projectPath}/Move.lock`, 'testnet');
489
+ }
433
490
  await updateMoveTomlAddress(projectPath, '0x0');
434
491
 
435
492
  const startCheckpoint =
@@ -447,16 +504,25 @@ export async function publishDubheFramework(
447
504
  fs.unlinkSync(publishedTomlPath);
448
505
  }
449
506
 
507
+ // Sui CLI 1.40+ checks that the active environment is declared in Move.toml
508
+ // even when --build-env is specified. Temporarily inject localnet into [environments].
509
+ const moveTomlPath = `${projectPath}/Move.toml`;
510
+ let savedMoveTomlContent: string | null = null;
511
+ if (network === 'localnet') {
512
+ savedMoveTomlContent = patchMoveTomlWithLocalnetEnv(moveTomlPath, chainId);
513
+ }
514
+
450
515
  let modules: any, dependencies: any;
451
516
  try {
452
- // For localnet: use --build-env testnet (no pubfile needed — dubhe has no local deps).
453
- // For testnet/mainnet: use -e <network> as usual.
454
517
  [modules, dependencies] = buildContract(projectPath, network);
455
518
  } finally {
456
- // Always restore Published.toml (successful build or error)
519
+ // Always restore Published.toml and Move.toml (successful build or error)
457
520
  if (savedPublishedTomlContent !== null) {
458
521
  fs.writeFileSync(publishedTomlPath, savedPublishedTomlContent, 'utf-8');
459
522
  }
523
+ if (savedMoveTomlContent !== null) {
524
+ fs.writeFileSync(moveTomlPath, savedMoveTomlContent, 'utf-8');
525
+ }
460
526
  }
461
527
 
462
528
  const tx = new Transaction();
@@ -478,7 +544,7 @@ export async function publishDubheFramework(
478
544
 
479
545
  let version = 1;
480
546
  let packageId = '';
481
- let dappHub = '';
547
+ let dappHubId = '';
482
548
  let upgradeCapId = '';
483
549
 
484
550
  result.objectChanges!.map((object: ObjectChange) => {
@@ -497,15 +563,16 @@ export async function publishDubheFramework(
497
563
  object.objectType &&
498
564
  object.objectType.includes('dapp_service::DappHub')
499
565
  ) {
500
- dappHub = object.objectId || '';
566
+ dappHubId = object.objectId || '';
501
567
  }
502
568
  });
503
569
 
504
570
  await delay(3000);
505
571
  const deployHookTx = new Transaction();
572
+ // Dubhe framework genesis::run(dapp_hub, ctx) — clock no longer required.
506
573
  deployHookTx.moveCall({
507
574
  target: `${packageId}::genesis::run`,
508
- arguments: [deployHookTx.object(dappHub), deployHookTx.object('0x6')]
575
+ arguments: [deployHookTx.object(dappHubId)]
509
576
  });
510
577
 
511
578
  let deployHookResult;
@@ -527,15 +594,17 @@ export async function publishDubheFramework(
527
594
  network,
528
595
  startCheckpoint,
529
596
  packageId,
530
- dappHub,
597
+ packageId, // originalPackageId: first publish, original == current
598
+ dappHubId,
531
599
  upgradeCapId,
532
600
  version,
533
601
  {},
534
602
  {},
535
- {}
603
+ // Store the localnet framework package ID so other packages can read it
604
+ // from deployment JSON and pass it to the Dubhe client constructor.
605
+ network === 'localnet' ? packageId : undefined
536
606
  );
537
607
 
538
- updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
539
608
  updatePublishedToml(projectPath, network, chainId, packageId, packageId, 1);
540
609
 
541
610
  // For localnet: write dubhe's published address to Pub.localnet.toml so that
@@ -554,13 +623,15 @@ export async function publishDubheFramework(
554
623
  export async function publishHandler(
555
624
  dubheConfig: DubheConfig,
556
625
  network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
557
- _force: boolean,
558
- gasBudget?: number
626
+ force: boolean,
627
+ gasBudget?: number,
628
+ fullnodeUrls?: string[]
559
629
  ) {
560
- await switchEnv(network);
630
+ await switchEnv(network, fullnodeUrls?.[0]);
561
631
 
562
632
  const dubhe = initializeDubhe({
563
- network
633
+ network,
634
+ fullnodeUrls
564
635
  });
565
636
 
566
637
  const path = process.cwd();
@@ -570,5 +641,5 @@ export async function publishHandler(
570
641
  await publishDubheFramework(dubhe, network);
571
642
  }
572
643
 
573
- await publishContract(dubhe, dubheConfig, network, projectPath, gasBudget);
644
+ await publishContract(dubhe, dubheConfig, network, projectPath, gasBudget, force, fullnodeUrls);
574
645
  }
@@ -1,16 +1,53 @@
1
1
  import { mkdirSync, writeFileSync } from 'fs';
2
2
  import { dirname } from 'path';
3
3
  import { DubheConfig } from '@0xobelisk/sui-common';
4
- import { getDeploymentJson, getDubheDappHub } from './utils';
4
+ import { getDeploymentJson, getDubheDappHubId, getOriginalDubhePackageId } from './utils';
5
5
 
6
- async function storeConfig(network: string, packageId: string, outputPath: string) {
7
- const dubheDappHub = await getDubheDappHub(network);
8
- let code = `type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
6
+ /** Derive the stable dapp_key type string from the original (genesis) package ID.
7
+ * Matches the Move-side `type_name::with_defining_ids<DappKey>().into_string()` output:
8
+ * "<hex64>::dapp_key::DappKey" (no "0x" prefix, address zero-padded to 64 hex chars).
9
+ */
10
+ function buildDappKey(originalPackageId: string): string {
11
+ const hex = originalPackageId.replace(/^0x/i, '').padStart(64, '0');
12
+ return `${hex}::dapp_key::DappKey`;
13
+ }
14
+
15
+ async function storeConfig(
16
+ network: string,
17
+ packageId: string,
18
+ originalPackageId: string,
19
+ dappStorageId: string,
20
+ outputPath: string
21
+ ) {
22
+ const dappHubId = await getDubheDappHubId(network);
23
+
24
+ // Mirror getDubheDappHubId: for localnet the framework is deployed ephemerally so we
25
+ // read its package ID from src/dubhe/.history/sui_localnet/latest.json.
26
+ // For testnet/mainnet the hardcoded ID from @0xobelisk/sui-client defaultConfig is used.
27
+ // devnet has no fixed framework deployment, so frameworkPackageId stays undefined.
28
+ let frameworkPackageId: string | undefined;
29
+ if (network !== 'devnet') {
30
+ frameworkPackageId = await getOriginalDubhePackageId(network);
31
+ }
32
+
33
+ const frameworkIdLine =
34
+ frameworkPackageId !== undefined
35
+ ? `\n// Published package ID of the dubhe framework — required for proxy operations.\nexport const FrameworkPackageId: string | undefined = '${frameworkPackageId}';\n`
36
+ : `\n// Published package ID of the dubhe framework — required for proxy operations.\n// Not available for devnet (no fixed framework deployment).\nexport const FrameworkPackageId: string | undefined = undefined;\n`;
37
+
38
+ const dappKey = buildDappKey(originalPackageId);
39
+
40
+ const code = `type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
9
41
 
10
- export const NETWORK: NetworkType = '${network}';
11
- export const PACKAGE_ID = '${packageId}';
12
- export const DUBHE_SCHEMA_ID = '${dubheDappHub}';
13
- `;
42
+ export const Network: NetworkType = '${network}';
43
+ export const PackageId = '${packageId}';
44
+ /** The first-published (original) package ID — stable across upgrades. Used for dapp_key and indexer filtering. */
45
+ export const OriginalPackageId = '${originalPackageId}';
46
+ /** Canonical dapp_key type string derived from OriginalPackageId. Pass to the Dubhe SDK and GraphQL queries. */
47
+ export const DappKey = '${dappKey}';
48
+ export const DappHubId = '${dappHubId}';
49
+ export const DappStorageId = '${dappStorageId}';
50
+ ${frameworkIdLine}`;
14
51
 
15
52
  writeOutput(code, outputPath, 'storeConfig');
16
53
  }
@@ -36,5 +73,14 @@ export async function storeConfigHandler(
36
73
  const path = process.cwd();
37
74
  const contractPath = `${path}/src/${dubheConfig.name}`;
38
75
  const deployment = await getDeploymentJson(contractPath, network);
39
- await storeConfig(deployment.network, deployment.packageId, outputPath);
76
+ // Prefer the persisted originalPackageId; fall back to packageId for old deployments
77
+ // that were created before this field was introduced.
78
+ const originalPackageId = deployment.originalPackageId ?? deployment.packageId;
79
+ await storeConfig(
80
+ deployment.network,
81
+ deployment.packageId,
82
+ originalPackageId,
83
+ deployment.dappStorageId ?? '',
84
+ outputPath
85
+ );
40
86
  }