@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.
- package/LICENSE +92 -0
- package/README.md +5 -5
- package/dist/dubhe.js +121 -102
- package/dist/dubhe.js.map +1 -1
- package/package.json +24 -24
- package/src/commands/build.ts +15 -3
- package/src/commands/checkBalance.ts +17 -9
- package/src/commands/convertJson.ts +39 -16
- package/src/commands/faucet.ts +9 -3
- package/src/commands/{schemagen.ts → generate.ts} +16 -6
- package/src/commands/index.ts +4 -4
- package/src/commands/info.ts +8 -2
- package/src/commands/loadMetadata.ts +13 -2
- package/src/commands/publish.ts +39 -5
- package/src/commands/shell.ts +15 -5
- package/src/commands/{configStore.ts → storeConfig.ts} +2 -1
- package/src/commands/switchEnv.ts +13 -6
- package/src/commands/test.ts +106 -37
- package/src/commands/upgrade.ts +36 -3
- package/src/commands/watch.ts +7 -7
- package/src/dubhe.ts +0 -0
- package/src/utils/checkBalance.ts +6 -2
- package/src/utils/constants.ts +8 -4
- package/src/utils/metadataHandler.ts +4 -3
- package/src/utils/publishHandler.ts +213 -142
- package/src/utils/storeConfig.ts +55 -9
- package/src/utils/upgradeHandler.ts +127 -98
- package/src/utils/utils.ts +405 -123
package/src/utils/utils.ts
CHANGED
|
@@ -1,25 +1,49 @@
|
|
|
1
1
|
import * as fsAsync from 'fs/promises';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { dirname, join as pathJoin } from 'path';
|
|
4
|
+
import * as readline from 'readline';
|
|
4
5
|
import { SUI_PRIVATE_KEY_PREFIX } from '@mysten/sui/cryptography';
|
|
5
6
|
import { FsIibError } from './errors';
|
|
6
7
|
import * as fs from 'fs';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
import { spawn } from 'child_process';
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Dubhe,
|
|
12
|
+
NetworkType,
|
|
13
|
+
SuiMoveNormalizedModules,
|
|
14
|
+
loadMetadata,
|
|
15
|
+
getDefaultConfig
|
|
16
|
+
} from '@0xobelisk/sui-client';
|
|
10
17
|
import { DubheCliError } from './errors';
|
|
11
|
-
import { Component, MoveType,
|
|
12
|
-
import { TESTNET_DUBHE_HUB_OBJECT_ID, TESTNET_ORIGINAL_DUBHE_PACKAGE_ID } from './constants';
|
|
18
|
+
import { Component, MoveType, DubheConfig } from '@0xobelisk/sui-common';
|
|
13
19
|
|
|
14
20
|
export type DeploymentJsonType = {
|
|
15
21
|
projectName: string;
|
|
16
22
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
|
|
17
23
|
startCheckpoint: string;
|
|
18
24
|
packageId: string;
|
|
19
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The original (first-published) package ID of this dapp.
|
|
27
|
+
* Derived from type_name::with_defining_ids<DappKey>() in Move, so it is stable
|
|
28
|
+
* across upgrades and is the canonical identifier used in dapp_key and indexer filtering.
|
|
29
|
+
* Set once at publish time and never changed during upgrades.
|
|
30
|
+
*/
|
|
31
|
+
originalPackageId: string;
|
|
32
|
+
/** Object ID of the Dubhe framework's DappHub shared object. */
|
|
33
|
+
dappHubId: string;
|
|
34
|
+
/**
|
|
35
|
+
* Published package ID of the Dubhe framework used by this deployment.
|
|
36
|
+
* Populated for localnet (ephemeral deploy); undefined for testnet/mainnet
|
|
37
|
+
* where the SDK already knows the well-known constant.
|
|
38
|
+
*/
|
|
39
|
+
frameworkPackageId?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Object ID of the DappStorage shared object created by genesis::run.
|
|
42
|
+
* Required for calling migrate_to_vN during upgrades.
|
|
43
|
+
*/
|
|
44
|
+
dappStorageId?: string;
|
|
20
45
|
upgradeCap: string;
|
|
21
46
|
version: number;
|
|
22
|
-
components: Record<string, Component | MoveType | EmptyComponent>;
|
|
23
47
|
resources: Record<string, Component | MoveType>;
|
|
24
48
|
enums?: Record<string, string[]>;
|
|
25
49
|
};
|
|
@@ -80,62 +104,57 @@ export async function getDeploymentJson(
|
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
export async function
|
|
107
|
+
export async function getDeploymentDappHubId(
|
|
108
|
+
projectPath: string,
|
|
109
|
+
network: string
|
|
110
|
+
): Promise<string> {
|
|
84
111
|
try {
|
|
85
112
|
const data = await fsAsync.readFile(
|
|
86
113
|
`${projectPath}/.history/sui_${network}/latest.json`,
|
|
87
114
|
'utf8'
|
|
88
115
|
);
|
|
89
116
|
const deployment = JSON.parse(data) as DeploymentJsonType;
|
|
90
|
-
return deployment.
|
|
117
|
+
return deployment.dappHubId;
|
|
91
118
|
} catch (_error) {
|
|
92
119
|
return '';
|
|
93
120
|
}
|
|
94
121
|
}
|
|
95
122
|
|
|
96
|
-
export async function
|
|
123
|
+
export async function getDubheDappHubId(network: string) {
|
|
97
124
|
const path = process.cwd();
|
|
98
125
|
const contractPath = `${path}/src/dubhe`;
|
|
99
126
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
throw new Error(`Invalid network: ${network}`);
|
|
127
|
+
if (network === 'localnet') {
|
|
128
|
+
return await getDeploymentDappHubId(contractPath, 'localnet');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const config = getDefaultConfig(network as NetworkType);
|
|
132
|
+
if (!config.dappHubId) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`DappHub object ID is not configured for network "${network}". ` +
|
|
135
|
+
`Update MAINNET_DUBHE_HUB_OBJECT_ID / TESTNET_DUBHE_HUB_OBJECT_ID in @0xobelisk/sui-client.`
|
|
136
|
+
);
|
|
111
137
|
}
|
|
138
|
+
return config.dappHubId;
|
|
112
139
|
}
|
|
113
140
|
|
|
114
141
|
export async function getOriginalDubhePackageId(network: string) {
|
|
115
142
|
const path = process.cwd();
|
|
116
143
|
const contractPath = `${path}/src/dubhe`;
|
|
117
144
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
|
|
121
|
-
case 'testnet':
|
|
122
|
-
return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
|
|
123
|
-
case 'devnet':
|
|
124
|
-
return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
|
|
125
|
-
case 'localnet':
|
|
126
|
-
return await getOldPackageId(contractPath, network);
|
|
127
|
-
default:
|
|
128
|
-
throw new Error(`Invalid network: ${network}`);
|
|
145
|
+
if (network === 'localnet') {
|
|
146
|
+
return await getOldPackageId(contractPath, network);
|
|
129
147
|
}
|
|
130
|
-
}
|
|
131
|
-
export async function getOnchainComponents(
|
|
132
|
-
projectPath: string,
|
|
133
|
-
network: string
|
|
134
|
-
): Promise<Record<string, Component | MoveType | EmptyComponent>> {
|
|
135
|
-
const deployment = await getDeploymentJson(projectPath, network);
|
|
136
|
-
return deployment.components;
|
|
137
|
-
}
|
|
138
148
|
|
|
149
|
+
const config = getDefaultConfig(network as NetworkType);
|
|
150
|
+
if (!config.frameworkPackageId) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Framework package ID is not configured for network "${network}". ` +
|
|
153
|
+
`Update MAINNET_DUBHE_FRAMEWORK_PACKAGE_ID / TESTNET_DUBHE_FRAMEWORK_PACKAGE_ID in @0xobelisk/sui-client.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return config.frameworkPackageId;
|
|
157
|
+
}
|
|
139
158
|
export async function getOnchainResources(
|
|
140
159
|
projectPath: string,
|
|
141
160
|
network: string
|
|
@@ -162,9 +181,22 @@ export async function getOldPackageId(projectPath: string, network: string): Pro
|
|
|
162
181
|
return deployment.packageId;
|
|
163
182
|
}
|
|
164
183
|
|
|
165
|
-
export async function
|
|
184
|
+
export async function getDappHubId(projectPath: string, network: string): Promise<string> {
|
|
166
185
|
const deployment = await getDeploymentJson(projectPath, network);
|
|
167
|
-
return deployment.
|
|
186
|
+
return deployment.dappHubId;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function getFrameworkPackageIdFromDeployment(
|
|
190
|
+
projectPath: string,
|
|
191
|
+
network: string
|
|
192
|
+
): Promise<string | undefined> {
|
|
193
|
+
const deployment = await getDeploymentJson(projectPath, network);
|
|
194
|
+
return deployment.frameworkPackageId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function getDappStorageId(projectPath: string, network: string): Promise<string> {
|
|
198
|
+
const deployment = await getDeploymentJson(projectPath, network);
|
|
199
|
+
return deployment.dappStorageId ?? '';
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
export async function getUpgradeCap(projectPath: string, network: string): Promise<string> {
|
|
@@ -182,22 +214,26 @@ export async function saveContractData(
|
|
|
182
214
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
183
215
|
startCheckpoint: string,
|
|
184
216
|
packageId: string,
|
|
185
|
-
|
|
217
|
+
originalPackageId: string,
|
|
218
|
+
dappHubId: string,
|
|
186
219
|
upgradeCap: string,
|
|
187
220
|
version: number,
|
|
188
|
-
components: Record<string, Component | MoveType | EmptyComponent>,
|
|
189
221
|
resources: Record<string, Component | MoveType>,
|
|
190
|
-
enums?: Record<string, string[]
|
|
222
|
+
enums?: Record<string, string[]>,
|
|
223
|
+
frameworkPackageId?: string,
|
|
224
|
+
dappStorageId?: string
|
|
191
225
|
) {
|
|
192
226
|
const DeploymentData: DeploymentJsonType = {
|
|
193
227
|
projectName,
|
|
194
228
|
network,
|
|
195
229
|
startCheckpoint,
|
|
196
230
|
packageId,
|
|
197
|
-
|
|
231
|
+
originalPackageId,
|
|
232
|
+
dappHubId,
|
|
233
|
+
frameworkPackageId,
|
|
234
|
+
dappStorageId,
|
|
198
235
|
upgradeCap,
|
|
199
236
|
version,
|
|
200
|
-
components,
|
|
201
237
|
resources,
|
|
202
238
|
enums
|
|
203
239
|
};
|
|
@@ -214,13 +250,14 @@ export async function saveContractData(
|
|
|
214
250
|
export async function saveMetadata(
|
|
215
251
|
projectName: string,
|
|
216
252
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
217
|
-
packageId: string
|
|
253
|
+
packageId: string,
|
|
254
|
+
fullnodeUrls?: string[]
|
|
218
255
|
) {
|
|
219
256
|
const path = process.cwd();
|
|
220
257
|
|
|
221
258
|
// Save metadata files
|
|
222
259
|
try {
|
|
223
|
-
const metadata = await loadMetadata(network, packageId);
|
|
260
|
+
const metadata = await loadMetadata(network, packageId, fullnodeUrls);
|
|
224
261
|
if (metadata) {
|
|
225
262
|
const metadataJson = JSON.stringify(metadata, null, 2);
|
|
226
263
|
|
|
@@ -360,6 +397,46 @@ export function getPublishedTomlEntry(
|
|
|
360
397
|
return entries[network];
|
|
361
398
|
}
|
|
362
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Syncs the Dubhe framework address in `src/dubhe/Published.toml` with the
|
|
402
|
+
* canonical package ID from the SDK's `getDefaultConfig` for the given network.
|
|
403
|
+
*
|
|
404
|
+
* This prevents `VMVerificationOrDeserializationError` during `publish` and
|
|
405
|
+
* `upgrade` when the framework has been redeployed on testnet/mainnet but the
|
|
406
|
+
* local `Published.toml` still references the old address. The function is a
|
|
407
|
+
* no-op for localnet and devnet (no stable canonical address exists there).
|
|
408
|
+
*
|
|
409
|
+
* @param contractsRootDir - The contracts working directory (process.cwd() in CLI context)
|
|
410
|
+
* @param network - Target network
|
|
411
|
+
* @param chainId - Live chain identifier obtained from the node
|
|
412
|
+
*/
|
|
413
|
+
export function syncDubheFrameworkAddress(
|
|
414
|
+
contractsRootDir: string,
|
|
415
|
+
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
416
|
+
chainId: string
|
|
417
|
+
): void {
|
|
418
|
+
if (network === 'localnet' || network === 'devnet') return;
|
|
419
|
+
|
|
420
|
+
const frameworkPackageId = getDefaultConfig(network as NetworkType).frameworkPackageId;
|
|
421
|
+
if (!frameworkPackageId) return;
|
|
422
|
+
|
|
423
|
+
const dubhePath = pathJoin(contractsRootDir, 'src', 'dubhe');
|
|
424
|
+
if (!fs.existsSync(dubhePath)) return;
|
|
425
|
+
|
|
426
|
+
const existing = getPublishedTomlEntry(dubhePath, network);
|
|
427
|
+
if (existing?.publishedAt === frameworkPackageId) return;
|
|
428
|
+
|
|
429
|
+
updatePublishedToml(dubhePath, network, chainId, frameworkPackageId, frameworkPackageId, 1);
|
|
430
|
+
console.log(
|
|
431
|
+
chalk.gray(
|
|
432
|
+
` ├─ Auto-synced dubhe framework address for ${network}: ${frameworkPackageId.slice(
|
|
433
|
+
0,
|
|
434
|
+
10
|
|
435
|
+
)}...`
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
363
440
|
export function clearPublishedTomlEntry(
|
|
364
441
|
packagePath: string,
|
|
365
442
|
network: string
|
|
@@ -534,7 +611,8 @@ async function checkRpcAvailability(rpcUrl: string): Promise<boolean> {
|
|
|
534
611
|
}
|
|
535
612
|
|
|
536
613
|
export async function addEnv(
|
|
537
|
-
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
|
|
614
|
+
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
615
|
+
rpcUrl?: string
|
|
538
616
|
): Promise<void> {
|
|
539
617
|
const rpcMap = {
|
|
540
618
|
localnet: 'http://127.0.0.1:9000',
|
|
@@ -543,13 +621,13 @@ export async function addEnv(
|
|
|
543
621
|
mainnet: 'https://fullnode.mainnet.sui.io:443/'
|
|
544
622
|
};
|
|
545
623
|
|
|
546
|
-
const
|
|
624
|
+
const resolvedRpcUrl = rpcUrl || rpcMap[network];
|
|
547
625
|
|
|
548
626
|
// Check RPC availability first
|
|
549
|
-
const isRpcAvailable = await checkRpcAvailability(
|
|
627
|
+
const isRpcAvailable = await checkRpcAvailability(resolvedRpcUrl);
|
|
550
628
|
if (!isRpcAvailable) {
|
|
551
629
|
throw new Error(
|
|
552
|
-
`RPC endpoint ${
|
|
630
|
+
`RPC endpoint ${resolvedRpcUrl} is not available. Please check your network connection or try again later.`
|
|
553
631
|
);
|
|
554
632
|
}
|
|
555
633
|
|
|
@@ -559,7 +637,7 @@ export async function addEnv(
|
|
|
559
637
|
|
|
560
638
|
const suiProcess = spawn(
|
|
561
639
|
'sui',
|
|
562
|
-
['client', 'new-env', '--alias', network, '--rpc',
|
|
640
|
+
['client', 'new-env', '--alias', network, '--rpc', resolvedRpcUrl],
|
|
563
641
|
{
|
|
564
642
|
env: { ...process.env },
|
|
565
643
|
stdio: 'pipe'
|
|
@@ -680,10 +758,13 @@ export async function getDefaultNetwork(): Promise<NetworkAlias> {
|
|
|
680
758
|
return currentAlias as NetworkAlias;
|
|
681
759
|
}
|
|
682
760
|
|
|
683
|
-
export async function switchEnv(
|
|
761
|
+
export async function switchEnv(
|
|
762
|
+
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
763
|
+
rpcUrl?: string
|
|
764
|
+
) {
|
|
684
765
|
try {
|
|
685
766
|
// First, try to add the environment
|
|
686
|
-
await addEnv(network);
|
|
767
|
+
await addEnv(network, rpcUrl);
|
|
687
768
|
|
|
688
769
|
// Then switch to the specified environment
|
|
689
770
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -757,70 +838,31 @@ export function loadKey(): string {
|
|
|
757
838
|
export function initializeDubhe({
|
|
758
839
|
network,
|
|
759
840
|
packageId,
|
|
760
|
-
metadata
|
|
841
|
+
metadata,
|
|
842
|
+
fullnodeUrls
|
|
761
843
|
}: {
|
|
762
844
|
network: NetworkType;
|
|
763
845
|
packageId?: string;
|
|
764
846
|
metadata?: SuiMoveNormalizedModules;
|
|
847
|
+
fullnodeUrls?: string[];
|
|
765
848
|
}): Dubhe {
|
|
766
849
|
const privateKey = loadKey();
|
|
767
850
|
return new Dubhe({
|
|
768
851
|
networkType: network,
|
|
769
852
|
secretKey: privateKey,
|
|
770
853
|
packageId,
|
|
771
|
-
metadata
|
|
854
|
+
metadata,
|
|
855
|
+
fullnodeUrls
|
|
772
856
|
});
|
|
773
857
|
}
|
|
774
858
|
|
|
775
859
|
export function generateConfigJson(config: DubheConfig): string {
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
fields: [{ entity_id: 'address' }, { value: component }],
|
|
781
|
-
keys: ['entity_id'],
|
|
782
|
-
offchain: false
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (Object.keys(component as object).length === 0) {
|
|
788
|
-
return {
|
|
789
|
-
[name]: {
|
|
790
|
-
fields: [{ entity_id: 'address' }],
|
|
791
|
-
keys: ['entity_id'],
|
|
792
|
-
offchain: false
|
|
793
|
-
}
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const fields = (component as any).fields || {};
|
|
798
|
-
const keys = (component as any).keys || ['entity_id'];
|
|
799
|
-
const offchain = (component as any).offchain ?? false;
|
|
800
|
-
|
|
801
|
-
// ensure entity_id field exists
|
|
802
|
-
if (!fields.entity_id && keys.includes('entity_id')) {
|
|
803
|
-
fields.entity_id = 'address';
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// prepare fields with entity_id first
|
|
807
|
-
const fieldEntries = Object.entries(fields);
|
|
808
|
-
const entityIdField = fieldEntries.find(([key]) => key === 'entity_id');
|
|
809
|
-
const otherFields = fieldEntries.filter(([key]) => key !== 'entity_id');
|
|
810
|
-
const orderedFields = entityIdField ? [entityIdField, ...otherFields] : otherFields;
|
|
811
|
-
|
|
812
|
-
return {
|
|
813
|
-
[name]: {
|
|
814
|
-
fields: orderedFields.map(([fieldName, fieldType]) => ({
|
|
815
|
-
[fieldName]: fieldType
|
|
816
|
-
})),
|
|
817
|
-
keys: keys,
|
|
818
|
-
offchain: offchain
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
});
|
|
860
|
+
const serializeFields = (fields: Record<string, unknown> = {}) =>
|
|
861
|
+
Object.entries(fields).map(([fieldName, fieldType]) => ({
|
|
862
|
+
[fieldName]: fieldType
|
|
863
|
+
}));
|
|
822
864
|
|
|
823
|
-
const resources = Object.entries(config.resources).map(([name, resource]) => {
|
|
865
|
+
const resources = Object.entries(config.resources ?? {}).map(([name, resource]) => {
|
|
824
866
|
// Simple type shorthand (e.g., counter1: 'u32') – entity-keyed by account (entity_id: String).
|
|
825
867
|
if (typeof resource === 'string') {
|
|
826
868
|
return {
|
|
@@ -878,25 +920,6 @@ export function generateConfigJson(config: DubheConfig): string {
|
|
|
878
920
|
};
|
|
879
921
|
});
|
|
880
922
|
|
|
881
|
-
// Auto-append Dubhe framework fee state resource (entity-keyed by account string).
|
|
882
|
-
if (!resources.some((resource) => 'dapp_fee_state' in resource)) {
|
|
883
|
-
resources.push({
|
|
884
|
-
dapp_fee_state: {
|
|
885
|
-
fields: [
|
|
886
|
-
{ entity_id: 'String' },
|
|
887
|
-
{ base_fee: 'u256' },
|
|
888
|
-
{ byte_fee: 'u256' },
|
|
889
|
-
{ free_credit: 'u256' },
|
|
890
|
-
{ total_bytes_size: 'u256' },
|
|
891
|
-
{ total_recharged: 'u256' },
|
|
892
|
-
{ total_paid: 'u256' }
|
|
893
|
-
],
|
|
894
|
-
keys: ['entity_id'],
|
|
895
|
-
offchain: false
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
|
|
900
923
|
// handle enums
|
|
901
924
|
const enums = Object.entries(config.enums || {}).map(([name, enumFields]) => {
|
|
902
925
|
// Sort enum values by first letter
|
|
@@ -907,10 +930,34 @@ export function generateConfigJson(config: DubheConfig): string {
|
|
|
907
930
|
};
|
|
908
931
|
});
|
|
909
932
|
|
|
933
|
+
const objects = Object.entries(config.objects ?? {}).map(([name, object]) => ({
|
|
934
|
+
[name]: {
|
|
935
|
+
fields: serializeFields(object.fields),
|
|
936
|
+
accepts: object.accepts ?? [],
|
|
937
|
+
acceptsFrom: object.acceptsFrom ?? [],
|
|
938
|
+
adminOnly: object.adminOnly ?? false
|
|
939
|
+
}
|
|
940
|
+
}));
|
|
941
|
+
|
|
942
|
+
const scenes = Object.entries(config.scenes ?? {}).map(([name, scene]) => ({
|
|
943
|
+
[name]: {
|
|
944
|
+
fields: serializeFields(scene.fields),
|
|
945
|
+
authorization: scene.authorization,
|
|
946
|
+
accepts: scene.accepts ?? [],
|
|
947
|
+
acceptsFrom: scene.acceptsFrom ?? []
|
|
948
|
+
}
|
|
949
|
+
}));
|
|
950
|
+
|
|
951
|
+
const permits = Object.entries(config.permits ?? {}).map(([name, permit]) => ({
|
|
952
|
+
[name]: permit ?? {}
|
|
953
|
+
}));
|
|
954
|
+
|
|
910
955
|
return JSON.stringify(
|
|
911
956
|
{
|
|
912
|
-
components,
|
|
913
957
|
resources,
|
|
958
|
+
objects,
|
|
959
|
+
scenes,
|
|
960
|
+
permits,
|
|
914
961
|
enums
|
|
915
962
|
},
|
|
916
963
|
null,
|
|
@@ -988,3 +1035,238 @@ export function updateGenesisUpgradeFunction(path: string, tables: string[]) {
|
|
|
988
1035
|
|
|
989
1036
|
fs.writeFileSync(genesisPath, updatedContent, 'utf-8');
|
|
990
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Appends a `migrate_to_vN` entry function to the package's migrate.move and
|
|
1041
|
+
* bumps `ON_CHAIN_VERSION` to `newVersion`.
|
|
1042
|
+
*
|
|
1043
|
+
* Called by upgradeHandler when new resources are detected (pendingMigration.length > 0).
|
|
1044
|
+
* The generated function:
|
|
1045
|
+
* 1. Reads the new package ID via `dapp_key::package_id()` — available on the new package.
|
|
1046
|
+
* 2. Reads the target version via `migrate::on_chain_version()` — equals newVersion after
|
|
1047
|
+
* this function bumps the constant.
|
|
1048
|
+
* 3. Calls `dapp_system::upgrade_dapp` to register the new package ID and bump
|
|
1049
|
+
* `DappStorage.version`.
|
|
1050
|
+
* 4. Calls `genesis::migrate` for any custom migration logic (extension point).
|
|
1051
|
+
*
|
|
1052
|
+
* `upgrade_dapp` accepts the new package's DappKey because its check was changed to compare
|
|
1053
|
+
* the caller's package ID against the registered list OR the incoming new_package_id, rather
|
|
1054
|
+
* than doing a full type-string comparison that would always fail after an upgrade.
|
|
1055
|
+
*/
|
|
1056
|
+
export function appendMigrateFunction(
|
|
1057
|
+
projectPath: string,
|
|
1058
|
+
packageName: string,
|
|
1059
|
+
newVersion: number
|
|
1060
|
+
): void {
|
|
1061
|
+
const migratePath = `${projectPath}/sources/scripts/migrate.move`;
|
|
1062
|
+
if (!fs.existsSync(migratePath)) {
|
|
1063
|
+
throw new Error(`migrate.move not found at ${migratePath}`);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
let content = fs.readFileSync(migratePath, 'utf-8');
|
|
1067
|
+
|
|
1068
|
+
// Idempotency: skip entirely if the function already exists
|
|
1069
|
+
if (content.includes(`migrate_to_v${newVersion}`)) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// ── Step 1: bump ON_CHAIN_VERSION to newVersion ──────────────────────────────
|
|
1074
|
+
// Replace the first `ON_CHAIN_VERSION: u32 = <N>` constant in the file.
|
|
1075
|
+
// This ensures on_chain_version() returns the correct value when upgrade_dapp
|
|
1076
|
+
// reads it inside the generated migrate_to_vN function.
|
|
1077
|
+
content = content.replace(
|
|
1078
|
+
/const ON_CHAIN_VERSION:\s*u32\s*=\s*\d+\s*;/,
|
|
1079
|
+
`const ON_CHAIN_VERSION: u32 = ${newVersion};`
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
// ── Step 2: append migrate_to_vN ─────────────────────────────────────────────
|
|
1083
|
+
// new_package_id must be passed as a parameter because type_name::get<T>() in
|
|
1084
|
+
// Sui Move always returns the ORIGINAL (genesis) package ID, not the upgraded one.
|
|
1085
|
+
// The TypeScript upgradeHandler supplies the actual new package ID after the upgrade
|
|
1086
|
+
// transaction completes and the on-chain package address is known.
|
|
1087
|
+
const migrateFunction = `
|
|
1088
|
+
public entry fun migrate_to_v${newVersion}(
|
|
1089
|
+
dapp_hub: &mut dubhe::dapp_service::DappHub,
|
|
1090
|
+
dapp_storage: &mut dubhe::dapp_service::DappStorage,
|
|
1091
|
+
new_package_id: address,
|
|
1092
|
+
ctx: &mut TxContext
|
|
1093
|
+
) {
|
|
1094
|
+
let new_version = ${packageName}::migrate::on_chain_version();
|
|
1095
|
+
dubhe::dapp_system::upgrade_dapp<${packageName}::dapp_key::DappKey>(
|
|
1096
|
+
dapp_hub, dapp_storage, new_package_id, new_version, ctx
|
|
1097
|
+
);
|
|
1098
|
+
${packageName}::genesis::migrate(dapp_hub, dapp_storage, ctx);
|
|
1099
|
+
}
|
|
1100
|
+
`;
|
|
1101
|
+
|
|
1102
|
+
// Insert the new function before the closing brace of the module
|
|
1103
|
+
const closingBraceIdx = content.lastIndexOf('}');
|
|
1104
|
+
if (closingBraceIdx === -1) {
|
|
1105
|
+
throw new Error(`Could not find closing brace in ${migratePath}`);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const updated =
|
|
1109
|
+
content.slice(0, closingBraceIdx) + migrateFunction + content.slice(closingBraceIdx);
|
|
1110
|
+
fs.writeFileSync(migratePath, updated, 'utf-8');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
// Guard lint
|
|
1115
|
+
// ---------------------------------------------------------------------------
|
|
1116
|
+
|
|
1117
|
+
export type MissingGuardResult = {
|
|
1118
|
+
/** Relative path to the Move source file (for display). */
|
|
1119
|
+
file: string;
|
|
1120
|
+
/** Name of the entry function missing the guard. */
|
|
1121
|
+
fn: string;
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Scans every `*.move` file under `<projectPath>/sources/systems/` and returns
|
|
1126
|
+
* the list of `public entry fun` declarations that:
|
|
1127
|
+
* 1. Accept a `DappStorage` parameter (so a version check is applicable), AND
|
|
1128
|
+
* 2. Do NOT call `ensure_latest_version` anywhere in their body.
|
|
1129
|
+
*
|
|
1130
|
+
* The implementation uses brace-balancing to extract each function body rather
|
|
1131
|
+
* than a full AST parse, which is sufficient for this structural check.
|
|
1132
|
+
*/
|
|
1133
|
+
export function lintSystemGuards(projectPath: string): MissingGuardResult[] {
|
|
1134
|
+
const systemsDir = pathJoin(projectPath, 'sources', 'systems');
|
|
1135
|
+
if (!fs.existsSync(systemsDir)) return [];
|
|
1136
|
+
|
|
1137
|
+
const results: MissingGuardResult[] = [];
|
|
1138
|
+
const files = fs.readdirSync(systemsDir).filter((f) => f.endsWith('.move'));
|
|
1139
|
+
|
|
1140
|
+
for (const file of files) {
|
|
1141
|
+
const fullPath = pathJoin(systemsDir, file);
|
|
1142
|
+
const src = fs.readFileSync(fullPath, 'utf-8');
|
|
1143
|
+
|
|
1144
|
+
// Find every `public entry fun <name>` position.
|
|
1145
|
+
const entryFunRe = /public\s+entry\s+fun\s+(\w+)\s*\(/g;
|
|
1146
|
+
let match: RegExpExecArray | null;
|
|
1147
|
+
|
|
1148
|
+
while ((match = entryFunRe.exec(src)) !== null) {
|
|
1149
|
+
const fnName = match[1];
|
|
1150
|
+
const parenStart = match.index + match[0].length - 1; // position of '('
|
|
1151
|
+
|
|
1152
|
+
// Extract the parameter list (between the outermost parentheses).
|
|
1153
|
+
let depth = 0;
|
|
1154
|
+
let parenEnd = parenStart;
|
|
1155
|
+
for (let i = parenStart; i < src.length; i++) {
|
|
1156
|
+
if (src[i] === '(') depth++;
|
|
1157
|
+
else if (src[i] === ')') {
|
|
1158
|
+
depth--;
|
|
1159
|
+
if (depth === 0) {
|
|
1160
|
+
parenEnd = i;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const paramList = src.slice(parenStart + 1, parenEnd);
|
|
1166
|
+
|
|
1167
|
+
// Only flag functions that receive a DappStorage parameter.
|
|
1168
|
+
if (!/DappStorage/.test(paramList)) continue;
|
|
1169
|
+
|
|
1170
|
+
// Extract the function body (between the outermost braces after the params).
|
|
1171
|
+
const braceStart = src.indexOf('{', parenEnd);
|
|
1172
|
+
if (braceStart === -1) continue;
|
|
1173
|
+
|
|
1174
|
+
depth = 0;
|
|
1175
|
+
let braceEnd = braceStart;
|
|
1176
|
+
for (let i = braceStart; i < src.length; i++) {
|
|
1177
|
+
if (src[i] === '{') depth++;
|
|
1178
|
+
else if (src[i] === '}') {
|
|
1179
|
+
depth--;
|
|
1180
|
+
if (depth === 0) {
|
|
1181
|
+
braceEnd = i;
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const body = src.slice(braceStart, braceEnd + 1);
|
|
1187
|
+
|
|
1188
|
+
if (!/ensure_latest_version/.test(body)) {
|
|
1189
|
+
results.push({ file, fn: fnName });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return results;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Formats lint results as a human-readable warning block.
|
|
1199
|
+
* Returns an empty string when there are no issues.
|
|
1200
|
+
*/
|
|
1201
|
+
export function formatLintWarnings(results: MissingGuardResult[]): string {
|
|
1202
|
+
if (results.length === 0) return '';
|
|
1203
|
+
const lines: string[] = [
|
|
1204
|
+
chalk.yellow('⚠️ Missing ensure_latest_version in the following entry functions:'),
|
|
1205
|
+
chalk.yellow(' Old-package callers can still invoke these functions after an upgrade.'),
|
|
1206
|
+
''
|
|
1207
|
+
];
|
|
1208
|
+
for (const r of results) {
|
|
1209
|
+
lines.push(chalk.yellow(` • ${r.file} → ${r.fn}()`));
|
|
1210
|
+
}
|
|
1211
|
+
lines.push('');
|
|
1212
|
+
lines.push(
|
|
1213
|
+
chalk.yellow(
|
|
1214
|
+
' Fix: add dubhe::dapp_system::ensure_latest_version(dapp_storage); at the top of each function.'
|
|
1215
|
+
)
|
|
1216
|
+
);
|
|
1217
|
+
lines.push('');
|
|
1218
|
+
return lines.join('\n');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Prompts the user for a yes/no confirmation on stdout/stdin.
|
|
1223
|
+
* Resolves `true` for "y/Y", `false` for everything else.
|
|
1224
|
+
*/
|
|
1225
|
+
export function confirm(question: string): Promise<boolean> {
|
|
1226
|
+
return new Promise((resolve) => {
|
|
1227
|
+
const rl = readline.createInterface({
|
|
1228
|
+
input: process.stdin,
|
|
1229
|
+
output: process.stdout
|
|
1230
|
+
});
|
|
1231
|
+
rl.question(chalk.yellow(`${question} [y/N] `), (answer: string) => {
|
|
1232
|
+
rl.close();
|
|
1233
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Append a new package ID to the `package_ids` array in dubhe.config.json.
|
|
1240
|
+
* Idempotent — does nothing if the ID is already present or the file does not exist.
|
|
1241
|
+
* Called by upgradeHandler after a successful on-chain upgrade so the indexer
|
|
1242
|
+
* can verify event.type_.address against all known package versions on next startup.
|
|
1243
|
+
*/
|
|
1244
|
+
export function appendPackageIdToConfig(configJsonPath: string, newPackageId: string): void {
|
|
1245
|
+
if (!fs.existsSync(configJsonPath)) return;
|
|
1246
|
+
const configJson = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
|
1247
|
+
const existingIds: string[] = Array.isArray(configJson.package_ids) ? configJson.package_ids : [];
|
|
1248
|
+
if (!existingIds.includes(newPackageId)) {
|
|
1249
|
+
configJson.package_ids = [...existingIds, newPackageId];
|
|
1250
|
+
fs.writeFileSync(configJsonPath, JSON.stringify(configJson, null, 2));
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Strip the [env.<network>] section from a Move.lock file before a build.
|
|
1256
|
+
*
|
|
1257
|
+
* Move.lock is owned by the Sui CLI and should never be written by our CLI.
|
|
1258
|
+
* We only remove stale [env.*] entries so the Sui CLI does not pick up an
|
|
1259
|
+
* old non-zero address and fail with PublishErrorNonZeroAddress. After the
|
|
1260
|
+
* publish/upgrade the entry is intentionally left absent — Published.toml is
|
|
1261
|
+
* the canonical source of truth for deployed addresses across our toolchain.
|
|
1262
|
+
*/
|
|
1263
|
+
export function removeEnvFromMoveLock(
|
|
1264
|
+
filePath: string,
|
|
1265
|
+
networkType: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
|
|
1266
|
+
): void {
|
|
1267
|
+
if (!fs.existsSync(filePath)) return;
|
|
1268
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1269
|
+
const regex = new RegExp(`\\[env\\.${networkType}\\][\\s\\S]*?(?=\\[|$)`, 'g');
|
|
1270
|
+
const updated = content.replace(regex, '');
|
|
1271
|
+
fs.writeFileSync(filePath, updated, 'utf-8');
|
|
1272
|
+
}
|