@0xobelisk/sui-cli 1.2.0-pre.113 → 1.2.0-pre.115

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xobelisk/sui-cli",
3
- "version": "1.2.0-pre.113",
3
+ "version": "1.2.0-pre.115",
4
4
  "description": "Tookit for interacting with move eps framework",
5
5
  "keywords": [
6
6
  "sui",
@@ -47,8 +47,8 @@
47
47
  "yargs": "^17.7.1",
48
48
  "zod": "^3.22.3",
49
49
  "zod-validation-error": "^1.3.0",
50
- "@0xobelisk/sui-client": "1.2.0-pre.113",
51
- "@0xobelisk/sui-common": "1.2.0-pre.113"
50
+ "@0xobelisk/sui-client": "1.2.0-pre.115",
51
+ "@0xobelisk/sui-common": "1.2.0-pre.115"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/cli-progress": "^3.11.5",
@@ -28,6 +28,7 @@ const commandModule: CommandModule<Options, Options> = {
28
28
  },
29
29
  recipient: {
30
30
  type: 'string',
31
+ alias: 'r',
31
32
  desc: 'Sui address to fund'
32
33
  }
33
34
  });
@@ -132,11 +132,12 @@ const ShellCommand: CommandModule<Options, Options> = {
132
132
  } else {
133
133
  yargsInstance.options(builder);
134
134
  }
135
+ const userArgs = parts.slice(1);
136
+ const hasNetworkFlag = userArgs.includes('--network') || userArgs.includes('-n');
135
137
  const argv = yargsInstance.parseSync([
136
138
  commandName,
137
- '--network',
138
- network,
139
- ...parts.slice(1)
139
+ ...(hasNetworkFlag ? [] : ['--network', network]),
140
+ ...userArgs
140
141
  ]);
141
142
  if (handler) {
142
143
  await handler(argv);
@@ -1,5 +1,9 @@
1
- export const TESTNET_DUBHE_HUB_OBJECT_ID =
2
- '0xfef203de9d3a2980429e91df535a0503ccf8d3c05aa3815936984243dc96f19f';
1
+ export {
2
+ TESTNET_DUBHE_HUB_OBJECT_ID,
3
+ TESTNET_DUBHE_FRAMEWORK_PACKAGE_ID,
4
+ MAINNET_DUBHE_HUB_OBJECT_ID,
5
+ MAINNET_DUBHE_FRAMEWORK_PACKAGE_ID
6
+ } from '@0xobelisk/sui-client';
3
7
 
4
- export const TESTNET_ORIGINAL_DUBHE_PACKAGE_ID =
5
- '0x8817b4976b6c607da01cea49d728f71d09274c82e9b163fa20c2382586f8aefc';
8
+ // Keep legacy alias for backwards compatibility within this package
9
+ export { TESTNET_DUBHE_FRAMEWORK_PACKAGE_ID as TESTNET_ORIGINAL_DUBHE_PACKAGE_ID } from '@0xobelisk/sui-client';
@@ -265,6 +265,13 @@ async function publishContract(
265
265
  const pubfilePath =
266
266
  network === 'localnet' ? getEphemeralPubFilePath(process.cwd(), network) : undefined;
267
267
 
268
+ // Move.toml paths — declared early so both the Published.toml handling block and
269
+ // the localnet env-patching block can reference them.
270
+ const contractMoveTomlPath = `${projectPath}/Move.toml`;
271
+ const dubheMoveTomlPath = path.join(path.dirname(projectPath), 'dubhe', 'Move.toml');
272
+ let savedContractMoveToml: string | null = null;
273
+ let savedDubheMoveToml: string | null = null;
274
+
268
275
  // So the build uses package address 0x0: for localnet always remove the contract's
269
276
  // Published.toml; for testnet/mainnet/devnet only when --force (clear current network entry).
270
277
  // Otherwise Sui CLI bakes the existing [published.<network>] address into the bytecode and
@@ -278,11 +285,20 @@ async function publishContract(
278
285
  if (network === 'localnet' && fs.existsSync(contractPublishedTomlPath)) {
279
286
  savedContractPublishedToml = fs.readFileSync(contractPublishedTomlPath, 'utf-8');
280
287
  fs.unlinkSync(contractPublishedTomlPath);
281
- } else if (force && (network === 'testnet' || network === 'mainnet' || network === 'devnet')) {
288
+ } else if (network === 'testnet' || network === 'mainnet' || network === 'devnet') {
282
289
  const entry = getPublishedTomlEntry(projectPath, network);
283
- if (entry) {
290
+ if (entry && force) {
291
+ // Existing entry + --force: clear it so the build uses 0x0 instead of the old address.
284
292
  savedContractPublishedEntry = { network, entry };
285
293
  clearPublishedTomlEntry(projectPath, network);
294
+ } else if (!entry) {
295
+ // No Published.toml entry for this network (first-time deploy to this network).
296
+ // The Sui CLI has no per-network override and falls back to Move.toml's [addresses]
297
+ // value, which may be non-zero from a previous deployment on a different network,
298
+ // causing PublishErrorNonZeroAddress.
299
+ // Temporarily zero out Move.toml before building so the self-address is 0x0.
300
+ savedContractMoveToml = fs.readFileSync(contractMoveTomlPath, 'utf-8');
301
+ updateMoveTomlAddress(projectPath, '0x0');
286
302
  }
287
303
  }
288
304
 
@@ -300,10 +316,6 @@ async function publishContract(
300
316
  // Sui CLI 1.40+ checks that the active environment is declared in Move.toml
301
317
  // even when --build-env is specified. Temporarily inject localnet into [environments]
302
318
  // 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
319
  if (network === 'localnet') {
308
320
  savedContractMoveToml = patchMoveTomlWithLocalnetEnv(contractMoveTomlPath, chainId);
309
321
  savedDubheMoveToml = patchMoveTomlWithLocalnetEnv(dubheMoveTomlPath, chainId);
@@ -450,7 +462,11 @@ async function publishContract(
450
462
  upgradeCapId,
451
463
  version,
452
464
  resources,
453
- enums
465
+ enums,
466
+ // localnet: persist the locally deployed framework ID so the SDK can be
467
+ // initialised without hardcoding it. testnet/mainnet use a well-known
468
+ // constant already embedded in the SDK defaults, so we store undefined.
469
+ network === 'localnet' ? await getOriginalDubhePackageId(network) : undefined
454
470
  );
455
471
 
456
472
  await saveMetadata(dubheConfig.name, network, packageId);
@@ -459,7 +475,11 @@ async function publishContract(
459
475
  let config = JSON.parse(fs.readFileSync(`${process.cwd()}/dubhe.config.json`, 'utf-8'));
460
476
  config.original_package_id = packageId;
461
477
  config.dubhe_object_id = dubheDappHub;
462
- config.original_dubhe_package_id = await getOriginalDubhePackageId(network);
478
+ // When deploying the dubhe framework itself, the "original dubhe package ID" is
479
+ // the package we just published. For user packages, look up the well-known
480
+ // framework address for the target network from the client config.
481
+ config.original_dubhe_package_id =
482
+ dubheConfig.name === 'dubhe' ? packageId : await getOriginalDubhePackageId(network);
463
483
  config.start_checkpoint = startCheckpoint;
464
484
 
465
485
  fs.writeFileSync(`${process.cwd()}/dubhe.config.json`, JSON.stringify(config, null, 2));
@@ -625,7 +645,10 @@ export async function publishDubheFramework(
625
645
  upgradeCapId,
626
646
  version,
627
647
  {},
628
- {}
648
+ {},
649
+ // Store the localnet framework package ID so other packages can read it
650
+ // from deployment JSON and pass it to the Dubhe client constructor.
651
+ network === 'localnet' ? packageId : undefined
629
652
  );
630
653
 
631
654
  updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
@@ -1,16 +1,31 @@
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, getDubheDappHub, getOriginalDubhePackageId } from './utils';
5
5
 
6
6
  async function storeConfig(network: string, packageId: string, outputPath: string) {
7
7
  const dubheDappHub = await getDubheDappHub(network);
8
- let code = `type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
8
+
9
+ // Mirror getDubheDappHub: for localnet the framework is deployed ephemerally so we
10
+ // read its package ID from src/dubhe/.history/sui_localnet/latest.json (same source
11
+ // as DUBHE_SCHEMA_ID above). For testnet/mainnet the SDK resolves the framework
12
+ // address automatically via getDefaultConfig(), so we emit undefined.
13
+ let frameworkPackageId: string | undefined;
14
+ if (network === 'localnet') {
15
+ frameworkPackageId = await getOriginalDubhePackageId(network);
16
+ }
17
+
18
+ const frameworkIdLine =
19
+ frameworkPackageId !== undefined
20
+ ? `\n// Published package ID of the dubhe framework — required for proxy operations.\nexport const FRAMEWORK_PACKAGE_ID: string | undefined = '${frameworkPackageId}';\n`
21
+ : `\n// Published package ID of the dubhe framework — required for proxy operations.\n// For testnet/mainnet the SDK resolves this automatically via getDefaultConfig().\nexport const FRAMEWORK_PACKAGE_ID: string | undefined = undefined;\n`;
22
+
23
+ const code = `type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
9
24
 
10
25
  export const NETWORK: NetworkType = '${network}';
11
26
  export const PACKAGE_ID = '${packageId}';
12
27
  export const DUBHE_SCHEMA_ID = '${dubheDappHub}';
13
- `;
28
+ ${frameworkIdLine}`;
14
29
 
15
30
  writeOutput(code, outputPath, 'storeConfig');
16
31
  }
@@ -11,14 +11,15 @@ import {
11
11
  initializeDubhe,
12
12
  getOnchainResources,
13
13
  getStartCheckpoint,
14
- updateGenesisUpgradeFunction,
14
+ appendMigrateFunction,
15
15
  getDubheDappHub,
16
16
  updatePublishedToml,
17
17
  clearPublishedTomlEntry,
18
18
  restorePublishedTomlEntry,
19
19
  readPublishedToml,
20
20
  updateEphemeralPubFile,
21
- getEphemeralPubFilePath
21
+ getEphemeralPubFilePath,
22
+ updateMoveTomlAddress
22
23
  } from './utils';
23
24
  import * as fs from 'fs';
24
25
  import * as path from 'path';
@@ -96,12 +97,14 @@ export async function upgradeHandler(
96
97
  });
97
98
 
98
99
  const tables = pendingMigration.map((migration) => migration.name);
99
- // Only update genesis.move when there are new tables to register.
100
- // When tables is empty, there are no schema changes so no migration needed.
101
- // Note: updating genesis.move also requires separator comments inserted by
102
- // `dubhe schemagen`; if they are missing, the update will throw.
100
+ // When new resources are detected, generate the migrate_to_vN entry function in
101
+ // migrate.move so the on-chain migration transaction can register the new package
102
+ // ID and version via dapp_system::upgrade_dapp.
103
+ // Note: the current framework stores data as dynamic fields and does NOT use
104
+ // register_table; genesis::migrate() is intentionally left empty and is called
105
+ // from migrate_to_vN solely to keep the extension point for future use.
103
106
  if (tables.length > 0) {
104
- updateGenesisUpgradeFunction(projectPath, tables);
107
+ appendMigrateFunction(projectPath, config.name, oldVersion + 1);
105
108
  }
106
109
 
107
110
  const original_published_id = replaceEnvField(
@@ -120,8 +123,11 @@ export async function upgradeHandler(
120
123
 
121
124
  // For localnet upgrades: refresh Pub.localnet.toml with dubhe's current address
122
125
  // so that the build can resolve the dubhe dependency.
126
+ // Skip this step when upgrading dubhe itself — dubhe has no local dubhe dependency,
127
+ // and adding its own address to the pubfile causes PublishErrorNonZeroAddress because
128
+ // the Sui CLI treats the root package's pubfile entry as a non-zero self-address.
123
129
  const cwd = process.cwd();
124
- if (network === 'localnet') {
130
+ if (network === 'localnet' && name !== 'dubhe') {
125
131
  const dubheProjectPath = `${cwd}/src/dubhe`;
126
132
  const dubheEntries = readPublishedToml(dubheProjectPath);
127
133
  const dubheEntry = dubheEntries['localnet'];
@@ -139,15 +145,32 @@ export async function upgradeHandler(
139
145
 
140
146
  try {
141
147
  let modules: any, dependencies: any, digest: any;
148
+ // When upgrading dubhe itself on localnet, Move.toml has a non-zero 'dubhe' address
149
+ // (the mainnet/testnet deploy address). This gets baked into the upgrade bytecode as the
150
+ // self-address, causing PublishErrorNonZeroAddress. Zero it out before the build and
151
+ // restore it afterwards — mirroring what publishDubheFramework does.
152
+ let savedDubheMoveTomlContent: string | null = null;
153
+ if (network === 'localnet' && name === 'dubhe') {
154
+ const moveTomlPath = `${projectPath}/Move.toml`;
155
+ savedDubheMoveTomlContent = fs.readFileSync(moveTomlPath, 'utf-8');
156
+ updateMoveTomlAddress(projectPath, '0x0');
157
+ }
142
158
  try {
143
159
  // For localnet: use --build-env testnet --pubfile-path Pub.localnet.toml
144
160
  // so the package compiles with address 0x0 (not in pubfile) and links
145
161
  // against the already-published dubhe dependency (from pubfile).
162
+ // Exception: when upgrading dubhe itself, skip the pubfile — dubhe has no
163
+ // local dubhe dependency and including itself in the pubfile causes
164
+ // PublishErrorNonZeroAddress.
146
165
  // For testnet/mainnet: use -e <network> as usual.
147
166
  let buildCmd: string;
148
167
  if (network === 'localnet') {
149
- const pubfilePath = getEphemeralPubFilePath(cwd, network);
150
- buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking --build-env testnet --pubfile-path ${pubfilePath} --path ${path}/src/${name}`;
168
+ if (name !== 'dubhe') {
169
+ const pubfilePath = getEphemeralPubFilePath(cwd, network);
170
+ buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking --build-env testnet --pubfile-path ${pubfilePath} --path ${path}/src/${name}`;
171
+ } else {
172
+ buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking --build-env testnet --path ${path}/src/${name}`;
173
+ }
151
174
  } else {
152
175
  buildCmd = `sui move build --dump-bytecode-as-base64 --no-tree-shaking -e ${network} --path ${path}/src/${name}`;
153
176
  }
@@ -167,6 +190,11 @@ export async function upgradeHandler(
167
190
  restorePublishedTomlEntry(projectPath, network, savedPublishedEntry);
168
191
  }
169
192
  throw new UpgradeError(error.stdout || error.stderr || error.message);
193
+ } finally {
194
+ // Always restore dubhe Move.toml after build (success or error)
195
+ if (savedDubheMoveTomlContent !== null) {
196
+ fs.writeFileSync(`${projectPath}/Move.toml`, savedDubheMoveTomlContent, 'utf-8');
197
+ }
170
198
  }
171
199
 
172
200
  console.log('\n🚀 Starting Upgrade Process...');
@@ -6,10 +6,15 @@ import { FsIibError } from './errors';
6
6
  import * as fs from 'fs';
7
7
  import chalk from 'chalk';
8
8
  import { spawn } from 'child_process';
9
- import { Dubhe, NetworkType, SuiMoveNormalizedModules, loadMetadata } from '@0xobelisk/sui-client';
9
+ import {
10
+ Dubhe,
11
+ NetworkType,
12
+ SuiMoveNormalizedModules,
13
+ loadMetadata,
14
+ getDefaultConfig
15
+ } from '@0xobelisk/sui-client';
10
16
  import { DubheCliError } from './errors';
11
17
  import { Component, MoveType, DubheConfig } from '@0xobelisk/sui-common';
12
- import { TESTNET_DUBHE_HUB_OBJECT_ID, TESTNET_ORIGINAL_DUBHE_PACKAGE_ID } from './constants';
13
18
 
14
19
  export type DeploymentJsonType = {
15
20
  projectName: string;
@@ -21,6 +26,12 @@ export type DeploymentJsonType = {
21
26
  version: number;
22
27
  resources: Record<string, Component | MoveType>;
23
28
  enums?: Record<string, string[]>;
29
+ /**
30
+ * Published package ID of the Dubhe framework used by this deployment.
31
+ * Populated for localnet (ephemeral deploy); undefined for testnet/mainnet
32
+ * where the SDK already knows the well-known constant.
33
+ */
34
+ frameworkPackageId?: string;
24
35
  };
25
36
 
26
37
  export function validatePrivateKey(privateKey: string): false | string {
@@ -96,36 +107,36 @@ export async function getDubheDappHub(network: string) {
96
107
  const path = process.cwd();
97
108
  const contractPath = `${path}/src/dubhe`;
98
109
 
99
- switch (network) {
100
- case 'mainnet':
101
- return TESTNET_DUBHE_HUB_OBJECT_ID;
102
- case 'testnet':
103
- return TESTNET_DUBHE_HUB_OBJECT_ID;
104
- case 'devnet':
105
- return TESTNET_DUBHE_HUB_OBJECT_ID;
106
- case 'localnet':
107
- return await getDeploymentDappHub(contractPath, 'localnet');
108
- default:
109
- throw new Error(`Invalid network: ${network}`);
110
+ if (network === 'localnet') {
111
+ return await getDeploymentDappHub(contractPath, 'localnet');
112
+ }
113
+
114
+ const config = getDefaultConfig(network as NetworkType);
115
+ if (!config.dappHubId) {
116
+ throw new Error(
117
+ `DappHub object ID is not configured for network "${network}". ` +
118
+ `Update MAINNET_DUBHE_HUB_OBJECT_ID / TESTNET_DUBHE_HUB_OBJECT_ID in @0xobelisk/sui-client.`
119
+ );
110
120
  }
121
+ return config.dappHubId;
111
122
  }
112
123
 
113
124
  export async function getOriginalDubhePackageId(network: string) {
114
125
  const path = process.cwd();
115
126
  const contractPath = `${path}/src/dubhe`;
116
127
 
117
- switch (network) {
118
- case 'mainnet':
119
- return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
120
- case 'testnet':
121
- return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
122
- case 'devnet':
123
- return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
124
- case 'localnet':
125
- return await getOldPackageId(contractPath, network);
126
- default:
127
- throw new Error(`Invalid network: ${network}`);
128
+ if (network === 'localnet') {
129
+ return await getOldPackageId(contractPath, network);
130
+ }
131
+
132
+ const config = getDefaultConfig(network as NetworkType);
133
+ if (!config.frameworkPackageId) {
134
+ throw new Error(
135
+ `Framework package ID is not configured for network "${network}". ` +
136
+ `Update MAINNET_DUBHE_FRAMEWORK_PACKAGE_ID / TESTNET_DUBHE_FRAMEWORK_PACKAGE_ID in @0xobelisk/sui-client.`
137
+ );
128
138
  }
139
+ return config.frameworkPackageId;
129
140
  }
130
141
  export async function getOnchainResources(
131
142
  projectPath: string,
@@ -158,6 +169,14 @@ export async function getDappHub(projectPath: string, network: string): Promise<
158
169
  return deployment.dappHub;
159
170
  }
160
171
 
172
+ export async function getFrameworkPackageIdFromDeployment(
173
+ projectPath: string,
174
+ network: string
175
+ ): Promise<string | undefined> {
176
+ const deployment = await getDeploymentJson(projectPath, network);
177
+ return deployment.frameworkPackageId;
178
+ }
179
+
161
180
  export async function getUpgradeCap(projectPath: string, network: string): Promise<string> {
162
181
  const deployment = await getDeploymentJson(projectPath, network);
163
182
  return deployment.upgradeCap;
@@ -177,7 +196,8 @@ export async function saveContractData(
177
196
  upgradeCap: string,
178
197
  version: number,
179
198
  resources: Record<string, Component | MoveType>,
180
- enums?: Record<string, string[]>
199
+ enums?: Record<string, string[]>,
200
+ frameworkPackageId?: string
181
201
  ) {
182
202
  const DeploymentData: DeploymentJsonType = {
183
203
  projectName,
@@ -188,7 +208,8 @@ export async function saveContractData(
188
208
  upgradeCap,
189
209
  version,
190
210
  resources,
191
- enums
211
+ enums,
212
+ frameworkPackageId
192
213
  };
193
214
 
194
215
  const path = process.cwd();
@@ -929,3 +950,53 @@ export function updateGenesisUpgradeFunction(path: string, tables: string[]) {
929
950
 
930
951
  fs.writeFileSync(genesisPath, updatedContent, 'utf-8');
931
952
  }
953
+
954
+ /**
955
+ * Appends a `migrate_to_vN` entry function to the package's migrate.move.
956
+ *
957
+ * This function is called by upgradeHandler when new resources are detected
958
+ * (pendingMigration.length > 0). The generated function delegates to
959
+ * `genesis::migrate`, which provides an extension point (separator comments)
960
+ * for any future resource-registration steps. The `new_package_id` and
961
+ * `new_version` arguments are kept in the signature to match the on-chain
962
+ * call emitted by upgradeHandler even though dapp_system::upgrade_dapp is
963
+ * public(package) in dubhe and cannot be called from external packages.
964
+ */
965
+ export function appendMigrateFunction(
966
+ projectPath: string,
967
+ packageName: string,
968
+ newVersion: number
969
+ ): void {
970
+ const migratePath = `${projectPath}/sources/scripts/migrate.move`;
971
+ if (!fs.existsSync(migratePath)) {
972
+ throw new Error(`migrate.move not found at ${migratePath}`);
973
+ }
974
+
975
+ const content = fs.readFileSync(migratePath, 'utf-8');
976
+
977
+ // Idempotency: skip if the function already exists
978
+ if (content.includes(`migrate_to_v${newVersion}`)) {
979
+ return;
980
+ }
981
+
982
+ const migrateFunction = `
983
+ public entry fun migrate_to_v${newVersion}(
984
+ dapp_hub: &mut dubhe::dapp_service::DappHub,
985
+ _new_package_id: address,
986
+ _new_version: u32,
987
+ ctx: &mut TxContext
988
+ ) {
989
+ ${packageName}::genesis::migrate(dapp_hub, ctx);
990
+ }
991
+ `;
992
+
993
+ // Insert the new function before the closing brace of the module
994
+ const closingBraceIdx = content.lastIndexOf('}');
995
+ if (closingBraceIdx === -1) {
996
+ throw new Error(`Could not find closing brace in ${migratePath}`);
997
+ }
998
+
999
+ const updated =
1000
+ content.slice(0, closingBraceIdx) + migrateFunction + content.slice(closingBraceIdx);
1001
+ fs.writeFileSync(migratePath, updated, 'utf-8');
1002
+ }