@0xobelisk/sui-cli 1.2.0-pre.118 → 1.2.0-pre.119

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.118",
3
+ "version": "1.2.0-pre.119",
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.118",
51
- "@0xobelisk/sui-common": "1.2.0-pre.118"
50
+ "@0xobelisk/sui-client": "1.2.0-pre.119",
51
+ "@0xobelisk/sui-common": "1.2.0-pre.119"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/cli-progress": "^3.11.5",
@@ -4,7 +4,7 @@ import nodePath from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { DubheConfig, loadConfig } from '@0xobelisk/sui-common';
6
6
  import { handlerExit } from './shell';
7
- import { getDefaultNetwork, switchEnv } from '../utils';
7
+ import { getDefaultNetwork, switchEnv, lintSystemGuards, formatLintWarnings } from '../utils';
8
8
 
9
9
  type Options = {
10
10
  'config-path': string;
@@ -78,6 +78,12 @@ const commandModule: CommandModule<Options, Options> = {
78
78
  console.log('šŸš€ Running move build');
79
79
  const dubheConfig = (await loadConfig(configPath)) as DubheConfig;
80
80
  await switchEnv(network);
81
+
82
+ const projectPath = nodePath.join(process.cwd(), 'src', dubheConfig.name);
83
+ const lintResults = lintSystemGuards(projectPath);
84
+ const warnings = formatLintWarnings(lintResults);
85
+ if (warnings) process.stdout.write(warnings);
86
+
81
87
  const output = await buildHandler(dubheConfig, network, dumpBytecodeAsBase64);
82
88
  console.log(output);
83
89
  exec(`pnpm dubhe convert-json --config-path ${configPath}`);
@@ -10,6 +10,33 @@ type Options = {
10
10
  'output-path': string;
11
11
  };
12
12
 
13
+ const RUNTIME_FIELDS = [
14
+ 'original_package_id',
15
+ 'dubhe_object_id',
16
+ 'original_dubhe_package_id',
17
+ 'dapp_key',
18
+ 'start_checkpoint'
19
+ ];
20
+
21
+ export function mergeConfigJsonRuntimeFields(
22
+ schemaJson: Record<string, unknown>,
23
+ existing: Record<string, unknown>
24
+ ): Record<string, unknown> {
25
+ const merged: Record<string, unknown> = { ...schemaJson };
26
+ for (const field of RUNTIME_FIELDS) {
27
+ if (existing[field] !== undefined) {
28
+ merged[field] = existing[field];
29
+ }
30
+ }
31
+ // If dapp_key is missing but original_package_id is present, compute it.
32
+ // This handles configs created before dapp_key was introduced.
33
+ if (!merged['dapp_key'] && merged['original_package_id']) {
34
+ const hex = (merged['original_package_id'] as string).replace(/^0x/i, '').padStart(64, '0');
35
+ merged['dapp_key'] = `${hex}::dapp_key::DappKey`;
36
+ }
37
+ return merged;
38
+ }
39
+
13
40
  const commandModule: CommandModule<Options, Options> = {
14
41
  command: 'convert-json',
15
42
  describe: 'Convert JSON from Dubhe config to config.json',
@@ -34,14 +61,6 @@ const commandModule: CommandModule<Options, Options> = {
34
61
  const dubheConfig = (await loadConfig(configPath)) as DubheConfig;
35
62
  const schemaJson = JSON.parse(generateConfigJson(dubheConfig));
36
63
 
37
- // Preserve runtime fields written by publishHandler (package IDs, checkpoint, etc.)
38
- // so that re-running convert-json after publish does not wipe deployment info.
39
- const RUNTIME_FIELDS = [
40
- 'original_package_id',
41
- 'dubhe_object_id',
42
- 'original_dubhe_package_id',
43
- 'start_checkpoint'
44
- ];
45
64
  let existing: Record<string, unknown> = {};
46
65
  if (fs.existsSync(outputPath)) {
47
66
  try {
@@ -50,12 +69,7 @@ const commandModule: CommandModule<Options, Options> = {
50
69
  // ignore parse errors – start fresh
51
70
  }
52
71
  }
53
- const merged: Record<string, unknown> = { ...schemaJson };
54
- for (const field of RUNTIME_FIELDS) {
55
- if (existing[field] !== undefined) {
56
- merged[field] = existing[field];
57
- }
58
- }
72
+ const merged = mergeConfigJsonRuntimeFields(schemaJson, existing);
59
73
 
60
74
  fs.writeFileSync(outputPath, JSON.stringify(merged, null, 2));
61
75
  } catch (error: any) {
@@ -1,5 +1,5 @@
1
1
  import type { CommandModule } from 'yargs';
2
- import { schemaGen, loadConfig, DubheConfig } from '@0xobelisk/sui-common';
2
+ import { codegen, loadConfig, DubheConfig } from '@0xobelisk/sui-common';
3
3
  import chalk from 'chalk';
4
4
  import path from 'node:path';
5
5
  import { handlerExit } from './shell';
@@ -8,11 +8,13 @@ import { getDefaultNetwork } from '../utils';
8
8
  type Options = {
9
9
  'config-path'?: string;
10
10
  network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet' | 'default';
11
+ mode?: 'user_pays' | 'dapp_subsidizes';
11
12
  };
12
13
 
13
14
  const commandModule: CommandModule<Options, Options> = {
15
+ command: 'generate',
14
16
  // 'schemagen' kept as a deprecated alias for backward compatibility
15
- command: 'generate|schemagen',
17
+ aliases: ['schemagen'],
16
18
 
17
19
  describe: 'Generate Move code from dubhe.config.ts',
18
20
 
@@ -27,10 +29,16 @@ const commandModule: CommandModule<Options, Options> = {
27
29
  choices: ['mainnet', 'testnet', 'devnet', 'localnet', 'default'] as const,
28
30
  default: 'default',
29
31
  desc: 'Node network (mainnet/testnet/devnet/localnet)'
32
+ },
33
+ mode: {
34
+ type: 'string',
35
+ choices: ['user_pays', 'dapp_subsidizes'] as const,
36
+ default: 'user_pays',
37
+ desc: 'Initial settlement mode for this DApp (only applies on first generate)'
30
38
  }
31
39
  },
32
40
 
33
- async handler({ 'config-path': configPath, network }) {
41
+ async handler({ 'config-path': configPath, network, mode }) {
34
42
  try {
35
43
  if (!configPath) throw new Error('Config path is required');
36
44
  if (network == 'default') {
@@ -39,7 +47,8 @@ const commandModule: CommandModule<Options, Options> = {
39
47
  }
40
48
  const dubheConfig = (await loadConfig(configPath)) as DubheConfig;
41
49
  const rootDir = path.dirname(configPath);
42
- await schemaGen(rootDir, dubheConfig, network);
50
+ const initialMode: 0 | 1 = mode === 'dapp_subsidizes' ? 0 : 1;
51
+ await codegen(rootDir, dubheConfig, network, initialMode);
43
52
  handlerExit();
44
53
  } catch (error: any) {
45
54
  console.log(chalk.red('Generate failed!'));
@@ -1,8 +1,15 @@
1
1
  import type { CommandModule } from 'yargs';
2
2
  import { logError } from '../utils/errors';
3
- import { getDefaultNetwork, publishHandler } from '../utils';
3
+ import {
4
+ getDefaultNetwork,
5
+ publishHandler,
6
+ lintSystemGuards,
7
+ formatLintWarnings,
8
+ confirm
9
+ } from '../utils';
4
10
  import { loadConfig, DubheConfig } from '@0xobelisk/sui-common';
5
11
  import { execSync } from 'child_process';
12
+ import { join as pathJoin } from 'path';
6
13
  import { handlerExit } from './shell';
7
14
  import chalk from 'chalk';
8
15
 
@@ -51,6 +58,21 @@ const commandModule: CommandModule<Options, Options> = {
51
58
  console.log(chalk.yellow(`Use default network: [${network}]`));
52
59
  }
53
60
  const dubheConfig = (await loadConfig(configPath)) as DubheConfig;
61
+
62
+ const projectPath = pathJoin(process.cwd(), 'src', dubheConfig.name);
63
+ const lintResults = lintSystemGuards(projectPath);
64
+ if (lintResults.length > 0) {
65
+ process.stdout.write(formatLintWarnings(lintResults));
66
+ const proceed = await confirm(
67
+ 'Some entry functions are missing ensure_latest_version. Proceed with publish anyway?'
68
+ );
69
+ if (!proceed) {
70
+ console.log(chalk.red('Publish cancelled.'));
71
+ handlerExit(1);
72
+ return;
73
+ }
74
+ }
75
+
54
76
  execSync(`pnpm dubhe convert-json --config-path ${configPath}`, { encoding: 'utf-8' });
55
77
  await publishHandler(dubheConfig, network, force, gasBudget);
56
78
  } catch (error: any) {
@@ -2,6 +2,7 @@ import type { CommandModule } from 'yargs';
2
2
  import { execFileSync, execSync } from 'child_process';
3
3
  import { DubheConfig, loadConfig } from '@0xobelisk/sui-common';
4
4
  import { handlerExit } from './shell';
5
+ import { lintSystemGuards, formatLintWarnings } from '../utils';
5
6
 
6
7
  /**
7
8
  * Returns the active Sui client environment (e.g. "localnet", "testnet").
@@ -135,6 +136,11 @@ const commandModule: CommandModule<CliOptions, CliOptions> = {
135
136
  console.log(list ? 'šŸš€ Listing Move unit tests' : 'šŸš€ Running move test');
136
137
  const dubheConfig = (await loadConfig(configPath)) as DubheConfig;
137
138
 
139
+ const projectPath = `${process.cwd()}/src/${dubheConfig.name}`;
140
+ const lintResults = lintSystemGuards(projectPath);
141
+ const warnings = formatLintWarnings(lintResults);
142
+ if (warnings) process.stdout.write(warnings);
143
+
138
144
  const activeEnv = getActiveSuiEnv();
139
145
  const buildEnv = activeEnv === 'localnet' || activeEnv === 'devnet' ? 'testnet' : undefined;
140
146
 
@@ -3,12 +3,14 @@ import { logError } from '../utils/errors';
3
3
  import { upgradeHandler } from '../utils/upgradeHandler';
4
4
  import { DubheConfig, loadConfig } from '@0xobelisk/sui-common';
5
5
  import { handlerExit } from './shell';
6
- import { getDefaultNetwork } from '../utils';
6
+ import { getDefaultNetwork, lintSystemGuards, formatLintWarnings, confirm } from '../utils';
7
+ import { join as pathJoin } from 'path';
7
8
  import chalk from 'chalk';
8
9
 
9
10
  type Options = {
10
11
  network: any;
11
12
  'config-path': string;
13
+ 'bump-version': boolean;
12
14
  };
13
15
 
14
16
  const commandModule: CommandModule<Options, Options> = {
@@ -28,18 +30,38 @@ const commandModule: CommandModule<Options, Options> = {
28
30
  type: 'string',
29
31
  default: 'dubhe.config.ts',
30
32
  decs: 'Path to the config file'
33
+ },
34
+ 'bump-version': {
35
+ type: 'boolean',
36
+ default: false,
37
+ desc: 'Force a version bump even when no new resources were added (use for breaking logic changes or security fixes that must invalidate old clients)'
31
38
  }
32
39
  });
33
40
  },
34
41
 
35
- async handler({ network, 'config-path': configPath }) {
42
+ async handler({ network, 'config-path': configPath, 'bump-version': bumpVersion }) {
36
43
  try {
37
44
  if (network == 'default') {
38
45
  network = await getDefaultNetwork();
39
46
  console.log(chalk.yellow(`Use default network: [${network}]`));
40
47
  }
41
48
  const dubheConfig = (await loadConfig(configPath)) as DubheConfig;
42
- await upgradeHandler(dubheConfig, dubheConfig.name, network);
49
+
50
+ const projectPath = pathJoin(process.cwd(), 'src', dubheConfig.name);
51
+ const lintResults = lintSystemGuards(projectPath);
52
+ if (lintResults.length > 0) {
53
+ process.stdout.write(formatLintWarnings(lintResults));
54
+ const proceed = await confirm(
55
+ 'Some entry functions are missing ensure_latest_version. Proceed with upgrade anyway?'
56
+ );
57
+ if (!proceed) {
58
+ console.log(chalk.red('Upgrade cancelled.'));
59
+ handlerExit(1);
60
+ return;
61
+ }
62
+ }
63
+
64
+ await upgradeHandler(dubheConfig, dubheConfig.name, network, bumpVersion);
43
65
  } catch (error: any) {
44
66
  logError(error);
45
67
  handlerExit(1);
@@ -493,6 +493,7 @@ async function publishContract(
493
493
  network,
494
494
  startCheckpoint,
495
495
  packageId,
496
+ packageId, // originalPackageId: first publish, so original == current
496
497
  frameworkDappHubId,
497
498
  upgradeCapId,
498
499
  version,
@@ -517,6 +518,10 @@ async function publishContract(
517
518
  config.original_dubhe_package_id =
518
519
  dubheConfig.name === 'dubhe' ? packageId : await getOriginalDubhePackageId(network);
519
520
  config.start_checkpoint = startCheckpoint;
521
+ // Canonical dapp_key type string: stable across upgrades, no "0x" prefix, padded to 64 hex chars.
522
+ // Matches the Move type_name::with_defining_ids<DappKey>().into_string() format.
523
+ const pkgHex = packageId.replace(/^0x/i, '').padStart(64, '0');
524
+ config.dapp_key = `${pkgHex}::dapp_key::DappKey`;
520
525
  // Persist the DappStorage object ID so store-config can include it in deployment.ts
521
526
  // and upgrade transactions can reference it without reading from .history.
522
527
  if (dappStorageId) {
@@ -601,8 +606,6 @@ export async function publishDubheFramework(
601
606
 
602
607
  let modules: any, dependencies: any;
603
608
  try {
604
- // For localnet: use --build-env testnet (no pubfile needed — dubhe has no local deps).
605
- // For testnet/mainnet: use -e <network> as usual.
606
609
  [modules, dependencies] = buildContract(projectPath, network);
607
610
  } finally {
608
611
  // Always restore Published.toml and Move.toml (successful build or error)
@@ -683,6 +686,7 @@ export async function publishDubheFramework(
683
686
  network,
684
687
  startCheckpoint,
685
688
  packageId,
689
+ packageId, // originalPackageId: first publish, original == current
686
690
  dappHubId,
687
691
  upgradeCapId,
688
692
  version,
@@ -3,9 +3,19 @@ import { dirname } from 'path';
3
3
  import { DubheConfig } from '@0xobelisk/sui-common';
4
4
  import { getDeploymentJson, getDubheDappHubId, getOriginalDubhePackageId } from './utils';
5
5
 
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
+
6
15
  async function storeConfig(
7
16
  network: string,
8
17
  packageId: string,
18
+ originalPackageId: string,
9
19
  dappStorageId: string,
10
20
  outputPath: string
11
21
  ) {
@@ -25,10 +35,16 @@ async function storeConfig(
25
35
  ? `\n// Published package ID of the dubhe framework — required for proxy operations.\nexport const FrameworkPackageId: string | undefined = '${frameworkPackageId}';\n`
26
36
  : `\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 FrameworkPackageId: string | undefined = undefined;\n`;
27
37
 
38
+ const dappKey = buildDappKey(originalPackageId);
39
+
28
40
  const code = `type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
29
41
 
30
42
  export const Network: NetworkType = '${network}';
31
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}';
32
48
  export const DappHubId = '${dappHubId}';
33
49
  export const DappStorageId = '${dappStorageId}';
34
50
  ${frameworkIdLine}`;
@@ -57,9 +73,13 @@ export async function storeConfigHandler(
57
73
  const path = process.cwd();
58
74
  const contractPath = `${path}/src/${dubheConfig.name}`;
59
75
  const deployment = await getDeploymentJson(contractPath, network);
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;
60
79
  await storeConfig(
61
80
  deployment.network,
62
81
  deployment.packageId,
82
+ originalPackageId,
63
83
  deployment.dappStorageId ?? '',
64
84
  outputPath
65
85
  );
@@ -75,7 +75,8 @@ function replaceEnvField(
75
75
  export async function upgradeHandler(
76
76
  config: DubheConfig,
77
77
  name: string,
78
- network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
78
+ network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
79
+ bumpVersion: boolean = false
79
80
  ) {
80
81
  await switchEnv(network);
81
82
 
@@ -103,13 +104,15 @@ export async function upgradeHandler(
103
104
  });
104
105
 
105
106
  const tables = pendingMigration.map((migration) => migration.name);
106
- // When new resources are detected, generate the migrate_to_vN entry function in
107
- // migrate.move so the on-chain migration transaction can register the new package
108
- // ID and version via dapp_system::upgrade_dapp.
109
- // Note: the current framework stores data as dynamic fields and does NOT use
110
- // register_table; genesis::migrate() is intentionally left empty and is called
111
- // from migrate_to_vN solely to keep the extension point for future use.
112
- if (tables.length > 0) {
107
+ // Trigger migration when new resources were detected (schema change) OR when the
108
+ // caller explicitly requested a version bump (--bump-version flag). The latter
109
+ // covers breaking logic changes and security fixes that must invalidate old clients
110
+ // even though no new resources were added.
111
+ const needsMigration = tables.length > 0 || bumpVersion;
112
+ if (needsMigration) {
113
+ if (bumpVersion && tables.length === 0) {
114
+ console.log(chalk.yellow('--bump-version: forcing version bump with no schema changes.'));
115
+ }
113
116
  appendMigrateFunction(projectPath, config.name, oldVersion + 1);
114
117
  }
115
118
 
@@ -278,6 +281,7 @@ export async function upgradeHandler(
278
281
  network,
279
282
  startCheckpoint,
280
283
  newPackageId,
284
+ original_published_id, // originalPackageId: stable across all upgrades
281
285
  dappHubId,
282
286
  upgradeCap,
283
287
  oldVersion + 1,
@@ -287,16 +291,26 @@ export async function upgradeHandler(
287
291
  dappStorageId || undefined
288
292
  );
289
293
 
290
- // Only run the migration transaction if there are pending schema changes.
291
- // A "bug-fix" upgrade with no new fields does not need a migration call.
292
- // The migrate_to_vX function is only generated by `dubhe generate` when
293
- // new components / resources are added.
294
- if (pendingMigration.length > 0) {
295
- console.log(`\nšŸš€ Starting Migration Process...`);
296
- pendingMigration.forEach((migration) => {
297
- console.log('šŸ“‹ Added Fields:', JSON.stringify(migration, null, 2));
298
- });
299
- await new Promise((resolve) => setTimeout(resolve, 5000));
294
+ // Only run the migration transaction if there are pending schema changes or a
295
+ // forced version bump was requested via --bump-version.
296
+ // A pure "bug-fix" upgrade with no new fields and no --bump-version flag does
297
+ // not need a migration call — old and new package can coexist safely.
298
+ if (needsMigration) {
299
+ if (pendingMigration.length > 0) {
300
+ console.log(`\nšŸš€ Starting Migration Process...`);
301
+ pendingMigration.forEach((migration) => {
302
+ console.log('šŸ“‹ Added Fields:', JSON.stringify(migration, null, 2));
303
+ });
304
+ } else {
305
+ console.log(`\nšŸš€ Starting Migration Process (forced version bump)...`);
306
+ }
307
+
308
+ // On localnet the indexer may lag behind the chain; give it time to register
309
+ // the newly upgraded package before the migration transaction references it.
310
+ // On testnet/mainnet the validator processes state immediately — no delay needed.
311
+ if (network === 'localnet') {
312
+ await new Promise((resolve) => setTimeout(resolve, 5000));
313
+ }
300
314
 
301
315
  if (!dappStorageId) {
302
316
  console.warn(
@@ -314,8 +328,7 @@ export async function upgradeHandler(
314
328
  arguments: [
315
329
  migrateTx.object(dappHubId),
316
330
  migrateTx.object(dappStorageId),
317
- migrateTx.pure.address(newPackageId),
318
- migrateTx.pure.u32(newVersion)
331
+ migrateTx.pure.address(newPackageId)
319
332
  ]
320
333
  });
321
334
 
@@ -335,7 +348,9 @@ export async function upgradeHandler(
335
348
  console.log(`\nāœ… No schema changes — migration step skipped.`);
336
349
  // Brief delay to allow localnet to index the upgraded package before
337
350
  // subsequent on-chain queries.
338
- await new Promise((resolve) => setTimeout(resolve, 2000));
351
+ if (network === 'localnet') {
352
+ await new Promise((resolve) => setTimeout(resolve, 2000));
353
+ }
339
354
  }
340
355
  } catch (error: any) {
341
356
  // Restore Published.toml to original state on failure (persistent networks only)