@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.
@@ -10,19 +10,23 @@ import {
10
10
  switchEnv,
11
11
  initializeDubhe,
12
12
  getOnchainResources,
13
- getOnchainComponents,
14
13
  getStartCheckpoint,
15
- updateGenesisUpgradeFunction,
16
- getDubheDappHub,
14
+ appendMigrateFunction,
15
+ getDubheDappHubId,
16
+ getDappStorageId,
17
+ getOriginalDubhePackageId,
17
18
  updatePublishedToml,
19
+ syncDubheFrameworkAddress,
18
20
  clearPublishedTomlEntry,
19
21
  restorePublishedTomlEntry,
20
22
  readPublishedToml,
21
23
  updateEphemeralPubFile,
22
- getEphemeralPubFilePath
24
+ getEphemeralPubFilePath,
25
+ updateMoveTomlAddress,
26
+ appendPackageIdToConfig,
27
+ removeEnvFromMoveLock
23
28
  } from './utils';
24
29
  import * as fs from 'fs';
25
- import * as path from 'path';
26
30
  import { DubheConfig } from '@0xobelisk/sui-common';
27
31
 
28
32
  type Migration = {
@@ -30,93 +34,56 @@ type Migration = {
30
34
  fields: any;
31
35
  };
32
36
 
33
- function replaceEnvField(
34
- filePath: string,
35
- networkType: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
36
- field: 'original-published-id' | 'latest-published-id' | 'published-version',
37
- newValue: string
38
- ): string {
39
- const envFilePath = path.resolve(filePath);
40
- const envContent = fs.readFileSync(envFilePath, 'utf-8');
41
- const envLines = envContent.split('\n');
42
-
43
- const networkSectionIndex = envLines.findIndex((line) => line.trim() === `[env.${networkType}]`);
44
- if (networkSectionIndex === -1) {
45
- console.log(`Network type [env.${networkType}] not found in the file.`);
46
- return '';
47
- }
48
-
49
- let fieldIndex = -1;
50
- let previousValue: string = '';
51
- for (let i = networkSectionIndex + 1; i < envLines.length; i++) {
52
- const line = envLines[i].trim();
53
- if (line.startsWith('[')) break; // End of the current network section
54
-
55
- if (line.startsWith(field)) {
56
- fieldIndex = i;
57
- previousValue = line.split('=')[1].trim().replace(/"/g, '');
58
- break;
59
- }
60
- }
61
-
62
- if (fieldIndex !== -1) {
63
- envLines[fieldIndex] = `${field} = "${newValue}"`;
64
- const newEnvContent = envLines.join('\n');
65
- fs.writeFileSync(envFilePath, newEnvContent, 'utf-8');
66
- } else {
67
- console.log(`${field} not found for [env.${networkType}].`);
68
- }
69
-
70
- return previousValue;
71
- }
72
-
73
37
  export async function upgradeHandler(
74
38
  config: DubheConfig,
75
39
  name: string,
76
- network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
40
+ network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
41
+ bumpVersion: boolean = false,
42
+ fullnodeUrls?: string[]
77
43
  ) {
78
- await switchEnv(network);
44
+ await switchEnv(network, fullnodeUrls?.[0]);
79
45
 
80
46
  const path = process.cwd();
81
47
  const projectPath = `${path}/src/${name}`;
82
48
 
83
- const dubhe = initializeDubhe({ network });
49
+ const dubhe = initializeDubhe({ network, fullnodeUrls });
84
50
 
85
51
  let oldVersion = Number(await getVersion(projectPath, network));
86
52
  let oldPackageId = await getOldPackageId(projectPath, network);
87
53
  let upgradeCap = await getUpgradeCap(projectPath, network);
88
54
  let startCheckpoint = await getStartCheckpoint(projectPath, network);
89
- let dappHub = await getDubheDappHub(network);
55
+ let dappHubId = await getDubheDappHubId(network);
56
+ let dappStorageId = await getDappStorageId(projectPath, network);
57
+ // For localnet the framework is deployed ephemerally; preserve its package ID in .history.
58
+ const frameworkPackageId =
59
+ network === 'localnet' ? await getOriginalDubhePackageId(network) : undefined;
90
60
  let onchainResources = await getOnchainResources(projectPath, network);
91
- let onchainComponents = await getOnchainComponents(projectPath, network);
92
61
 
93
62
  let pendingMigration: Migration[] = [];
94
- Object.entries(config.resources).forEach(([key, value]) => {
63
+ Object.entries(config.resources ?? {}).forEach(([key, value]) => {
95
64
  if (!onchainResources.hasOwnProperty(key)) {
96
65
  pendingMigration.push({ name: key, fields: value });
97
66
  }
98
67
  });
99
- Object.entries(config.components).forEach(([key, value]) => {
100
- if (!onchainComponents.hasOwnProperty(key)) {
101
- pendingMigration.push({ name: key, fields: value });
102
- }
103
- });
104
68
 
105
69
  const tables = pendingMigration.map((migration) => migration.name);
106
- // Only update genesis.move when there are new tables to register.
107
- // When tables is empty, there are no schema changes so no migration needed.
108
- // Note: updating genesis.move also requires separator comments inserted by
109
- // `dubhe schemagen`; if they are missing, the update will throw.
110
- if (tables.length > 0) {
111
- updateGenesisUpgradeFunction(projectPath, tables);
70
+ // Trigger migration when new resources were detected (schema change) OR when the
71
+ // caller explicitly requested a version bump (--bump-version flag). The latter
72
+ // covers breaking logic changes and security fixes that must invalidate old clients
73
+ // even though no new resources were added.
74
+ const needsMigration = tables.length > 0 || bumpVersion;
75
+ if (needsMigration) {
76
+ if (bumpVersion && tables.length === 0) {
77
+ console.log(chalk.yellow('--bump-version: forcing version bump with no schema changes.'));
78
+ }
79
+ appendMigrateFunction(projectPath, config.name, oldVersion + 1);
112
80
  }
113
81
 
114
- const original_published_id = replaceEnvField(
115
- `${projectPath}/Move.lock`,
116
- network,
117
- 'original-published-id',
118
- '0x0000000000000000000000000000000000000000000000000000000000000000'
119
- );
82
+ // Read original_published_id from Published.toml before clearing it.
83
+ // Published.toml is the canonical source of truth for deployed addresses;
84
+ // Move.lock [env.*] is owned by the Sui CLI and must not be read or written
85
+ // by our toolchain.
86
+ const original_published_id = readPublishedToml(projectPath)[network]?.originalId ?? '';
120
87
 
121
88
  // For persistent networks (testnet/mainnet): zero out Published.toml so the
122
89
  // package compiles with address 0x0 for upgrade.
@@ -125,10 +92,28 @@ export async function upgradeHandler(
125
92
  const savedPublishedEntry =
126
93
  network !== 'localnet' ? clearPublishedTomlEntry(projectPath, network) : undefined;
127
94
 
95
+ // Clear Move.lock [env.*] so the Sui CLI does not pick up a stale non-zero
96
+ // address and fail with PublishErrorNonZeroAddress during build.
97
+ // We intentionally do NOT write it back afterwards — Published.toml is the
98
+ // source of truth and the Sui CLI will regenerate the lock section if needed.
99
+ removeEnvFromMoveLock(`${projectPath}/Move.lock`, network);
100
+
101
+ // For testnet/mainnet: auto-sync src/dubhe/Published.toml to the canonical
102
+ // framework address from the SDK before building. Prevents
103
+ // VMVerificationOrDeserializationError when the framework was redeployed
104
+ // since the Published.toml was last written. Skip when upgrading dubhe itself.
105
+ if (name !== 'dubhe' && (network === 'testnet' || network === 'mainnet')) {
106
+ const chainId = await dubhe.suiInteractor.currentClient.getChainIdentifier();
107
+ syncDubheFrameworkAddress(path, network, chainId);
108
+ }
109
+
128
110
  // For localnet upgrades: refresh Pub.localnet.toml with dubhe's current address
129
111
  // so that the build can resolve the dubhe dependency.
112
+ // Skip this step when upgrading dubhe itself — dubhe has no local dubhe dependency,
113
+ // and adding its own address to the pubfile causes PublishErrorNonZeroAddress because
114
+ // the Sui CLI treats the root package's pubfile entry as a non-zero self-address.
130
115
  const cwd = process.cwd();
131
- if (network === 'localnet') {
116
+ if (network === 'localnet' && name !== 'dubhe') {
132
117
  const dubheProjectPath = `${cwd}/src/dubhe`;
133
118
  const dubheEntries = readPublishedToml(dubheProjectPath);
134
119
  const dubheEntry = dubheEntries['localnet'];
@@ -146,15 +131,32 @@ export async function upgradeHandler(
146
131
 
147
132
  try {
148
133
  let modules: any, dependencies: any, digest: any;
134
+ // When upgrading dubhe itself on localnet, Move.toml has a non-zero 'dubhe' address
135
+ // (the mainnet/testnet deploy address). This gets baked into the upgrade bytecode as the
136
+ // self-address, causing PublishErrorNonZeroAddress. Zero it out before the build and
137
+ // restore it afterwards — mirroring what publishDubheFramework does.
138
+ let savedDubheMoveTomlContent: string | null = null;
139
+ if (network === 'localnet' && name === 'dubhe') {
140
+ const moveTomlPath = `${projectPath}/Move.toml`;
141
+ savedDubheMoveTomlContent = fs.readFileSync(moveTomlPath, 'utf-8');
142
+ updateMoveTomlAddress(projectPath, '0x0');
143
+ }
149
144
  try {
150
145
  // For localnet: use --build-env testnet --pubfile-path Pub.localnet.toml
151
146
  // so the package compiles with address 0x0 (not in pubfile) and links
152
147
  // against the already-published dubhe dependency (from pubfile).
148
+ // Exception: when upgrading dubhe itself, skip the pubfile — dubhe has no
149
+ // local dubhe dependency and including itself in the pubfile causes
150
+ // PublishErrorNonZeroAddress.
153
151
  // For testnet/mainnet: use -e <network> as usual.
154
152
  let buildCmd: string;
155
153
  if (network === 'localnet') {
156
- const pubfilePath = getEphemeralPubFilePath(cwd, network);
157
- buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking --build-env testnet --pubfile-path ${pubfilePath} --path ${path}/src/${name}`;
154
+ if (name !== 'dubhe') {
155
+ const pubfilePath = getEphemeralPubFilePath(cwd, network);
156
+ buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking --build-env testnet --pubfile-path ${pubfilePath} --path ${path}/src/${name}`;
157
+ } else {
158
+ buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking --build-env testnet --path ${path}/src/${name}`;
159
+ }
158
160
  } else {
159
161
  buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking -e ${network} --path ${path}/src/${name}`;
160
162
  }
@@ -174,6 +176,11 @@ export async function upgradeHandler(
174
176
  restorePublishedTomlEntry(projectPath, network, savedPublishedEntry);
175
177
  }
176
178
  throw new UpgradeError(error.stdout || error.stderr || error.message);
179
+ } finally {
180
+ // Always restore dubhe Move.toml after build (success or error)
181
+ if (savedDubheMoveTomlContent !== null) {
182
+ fs.writeFileSync(`${projectPath}/Move.toml`, savedDubheMoveTomlContent, 'utf-8');
183
+ }
177
184
  }
178
185
 
179
186
  console.log('\n🚀 Starting Upgrade Process...');
@@ -223,15 +230,6 @@ export async function upgradeHandler(
223
230
  }
224
231
  });
225
232
 
226
- replaceEnvField(
227
- `${projectPath}/Move.lock`,
228
- network,
229
- 'original-published-id',
230
- original_published_id
231
- );
232
- replaceEnvField(`${projectPath}/Move.lock`, network, 'latest-published-id', newPackageId);
233
- replaceEnvField(`${projectPath}/Move.lock`, network, 'published-version', oldVersion + 1 + '');
234
-
235
233
  // Update Published.toml with the new package ID after upgrade.
236
234
  // For localnet: savedPublishedEntry is undefined (we skip clearPublishedTomlEntry),
237
235
  // so fall back to reading the current Published.toml entry for chainId/originalId.
@@ -251,33 +249,62 @@ export async function upgradeHandler(
251
249
  network,
252
250
  startCheckpoint,
253
251
  newPackageId,
254
- dappHub,
252
+ original_published_id, // originalPackageId: stable across all upgrades
253
+ dappHubId,
255
254
  upgradeCap,
256
255
  oldVersion + 1,
257
- config.components,
258
- config.resources,
259
- config.enums
256
+ config.resources ?? {},
257
+ config.enums,
258
+ frameworkPackageId,
259
+ dappStorageId || undefined
260
260
  );
261
261
 
262
- // Only run the migration transaction if there are pending schema changes.
263
- // A "bug-fix" upgrade with no new fields does not need a migration call.
264
- // The migrate_to_vX function is only generated by `dubhe schemagen` when
265
- // new components / resources are added.
266
- if (pendingMigration.length > 0) {
267
- console.log(`\n🚀 Starting Migration Process...`);
268
- pendingMigration.forEach((migration) => {
269
- console.log('📋 Added Fields:', JSON.stringify(migration, null, 2));
270
- });
271
- await new Promise((resolve) => setTimeout(resolve, 5000));
262
+ // Append the new package ID to dubhe.config.json so the indexer can verify
263
+ // event.type_.address against all known package versions on next startup.
264
+ const configJsonPath = `${process.cwd()}/dubhe.config.json`;
265
+ appendPackageIdToConfig(configJsonPath, newPackageId);
266
+ if (fs.existsSync(configJsonPath)) {
267
+ console.log(chalk.green(`✅ Appended ${newPackageId} to dubhe.config.json package_ids`));
268
+ }
269
+
270
+ // Only run the migration transaction if there are pending schema changes or a
271
+ // forced version bump was requested via --bump-version.
272
+ // A pure "bug-fix" upgrade with no new fields and no --bump-version flag does
273
+ // not need a migration call — old and new package can coexist safely.
274
+ if (needsMigration) {
275
+ if (pendingMigration.length > 0) {
276
+ console.log(`\n🚀 Starting Migration Process...`);
277
+ pendingMigration.forEach((migration) => {
278
+ console.log('📋 Added Fields:', JSON.stringify(migration, null, 2));
279
+ });
280
+ } else {
281
+ console.log(`\n🚀 Starting Migration Process (forced version bump)...`);
282
+ }
283
+
284
+ // On localnet the indexer may lag behind the chain; give it time to register
285
+ // the newly upgraded package before the migration transaction references it.
286
+ // On testnet/mainnet the validator processes state immediately — no delay needed.
287
+ if (network === 'localnet') {
288
+ await new Promise((resolve) => setTimeout(resolve, 5000));
289
+ }
290
+
291
+ if (!dappStorageId) {
292
+ console.warn(
293
+ chalk.yellow(
294
+ 'Warning: dappStorageId not found in latest.json. ' +
295
+ 'Re-publish the contract to capture it, or pass DappStorage manually.'
296
+ )
297
+ );
298
+ }
272
299
 
273
300
  const migrateTx = new Transaction();
274
301
  const newVersion = oldVersion + 1;
275
302
  migrateTx.moveCall({
276
303
  target: `${newPackageId}::migrate::migrate_to_v${newVersion}`,
277
304
  arguments: [
278
- migrateTx.object(dappHub),
279
- migrateTx.pure.address(newPackageId),
280
- migrateTx.pure.u32(newVersion)
305
+ migrateTx.object(dappHubId),
306
+ migrateTx.object(dappStorageId),
307
+ migrateTx.pure.address(newPackageId)
281
308
  ]
282
309
  });
283
310
 
@@ -297,7 +324,9 @@ export async function upgradeHandler(
297
324
  console.log(`\n✅ No schema changes — migration step skipped.`);
298
325
  // Brief delay to allow localnet to index the upgraded package before
299
326
  // subsequent on-chain queries.
300
- await new Promise((resolve) => setTimeout(resolve, 2000));
327
+ if (network === 'localnet') {
328
+ await new Promise((resolve) => setTimeout(resolve, 2000));
329
+ }
301
330
  }
302
331
  } catch (error: any) {
303
332
  // Restore Published.toml to original state on failure (persistent networks only)