@0xobelisk/sui-cli 1.2.0-pre.1 → 1.2.0-pre.100
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/README.md +3 -3
- package/dist/dubhe.js +125 -66
- package/dist/dubhe.js.map +1 -1
- package/package.json +31 -19
- package/src/commands/build.ts +47 -16
- package/src/commands/call.ts +83 -83
- package/src/commands/checkBalance.ts +12 -5
- package/src/commands/configStore.ts +12 -4
- package/src/commands/convertJson.ts +70 -0
- package/src/commands/doctor.ts +1515 -0
- package/src/commands/faucet.ts +11 -7
- package/src/commands/generateKey.ts +3 -2
- package/src/commands/index.ts +16 -7
- package/src/commands/info.ts +55 -0
- package/src/commands/loadMetadata.ts +57 -0
- package/src/commands/localnode.ts +22 -12
- package/src/commands/publish.ts +21 -7
- package/src/commands/query.ts +101 -101
- package/src/commands/schemagen.ts +15 -4
- package/src/commands/shell.ts +198 -0
- package/src/commands/switchEnv.ts +26 -0
- package/src/commands/test.ts +54 -11
- package/src/commands/upgrade.ts +11 -4
- package/src/commands/wait.ts +333 -22
- package/src/commands/watch.ts +2 -1
- package/src/dubhe.ts +12 -4
- package/src/utils/axios-downloader.ts +116 -0
- package/src/utils/callHandler.ts +118 -118
- package/src/utils/constants.ts +5 -0
- package/src/utils/generateAccount.ts +1 -1
- package/src/utils/index.ts +4 -3
- package/src/utils/metadataHandler.ts +16 -0
- package/src/utils/publishHandler.ts +295 -290
- package/src/utils/queryStorage.ts +141 -141
- package/src/utils/startNode.ts +165 -108
- package/src/utils/storeConfig.ts +6 -12
- package/src/utils/upgradeHandler.ts +147 -86
- package/src/utils/utils.ts +771 -54
package/src/utils/utils.ts
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import * as fsAsync from 'fs/promises';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import { dirname } from 'path';
|
|
3
|
+
import { dirname, join as pathJoin } from 'path';
|
|
4
4
|
import { SUI_PRIVATE_KEY_PREFIX } from '@mysten/sui/cryptography';
|
|
5
5
|
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 } from '@0xobelisk/sui-client';
|
|
9
|
+
import { Dubhe, NetworkType, SuiMoveNormalizedModules, loadMetadata } from '@0xobelisk/sui-client';
|
|
10
10
|
import { DubheCliError } from './errors';
|
|
11
|
+
import { Component, MoveType, EmptyComponent, DubheConfig } from '@0xobelisk/sui-common';
|
|
12
|
+
import { TESTNET_DUBHE_HUB_OBJECT_ID, TESTNET_ORIGINAL_DUBHE_PACKAGE_ID } from './constants';
|
|
11
13
|
|
|
12
14
|
export type DeploymentJsonType = {
|
|
13
15
|
projectName: string;
|
|
14
16
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
|
|
17
|
+
startCheckpoint: string;
|
|
15
18
|
packageId: string;
|
|
16
|
-
|
|
19
|
+
dappHub: string;
|
|
17
20
|
upgradeCap: string;
|
|
18
21
|
version: number;
|
|
19
|
-
|
|
22
|
+
components: Record<string, Component | MoveType | EmptyComponent>;
|
|
23
|
+
resources: Record<string, Component | MoveType>;
|
|
24
|
+
enums?: Record<string, string[]>;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
export function validatePrivateKey(privateKey: string): false | string {
|
|
@@ -75,43 +80,68 @@ export async function getDeploymentJson(
|
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
export async function
|
|
83
|
+
export async function getDeploymentDappHub(projectPath: string, network: string): Promise<string> {
|
|
79
84
|
try {
|
|
80
85
|
const data = await fsAsync.readFile(
|
|
81
86
|
`${projectPath}/.history/sui_${network}/latest.json`,
|
|
82
87
|
'utf8'
|
|
83
88
|
);
|
|
84
89
|
const deployment = JSON.parse(data) as DeploymentJsonType;
|
|
85
|
-
return deployment.
|
|
86
|
-
} catch (
|
|
90
|
+
return deployment.dappHub;
|
|
91
|
+
} catch (_error) {
|
|
87
92
|
return '';
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
export async function
|
|
96
|
+
export async function getDubheDappHub(network: string) {
|
|
97
|
+
const path = process.cwd();
|
|
98
|
+
const contractPath = `${path}/src/dubhe`;
|
|
99
|
+
|
|
100
|
+
switch (network) {
|
|
101
|
+
case 'mainnet':
|
|
102
|
+
return TESTNET_DUBHE_HUB_OBJECT_ID;
|
|
103
|
+
case 'testnet':
|
|
104
|
+
return TESTNET_DUBHE_HUB_OBJECT_ID;
|
|
105
|
+
case 'devnet':
|
|
106
|
+
return TESTNET_DUBHE_HUB_OBJECT_ID;
|
|
107
|
+
case 'localnet':
|
|
108
|
+
return await getDeploymentDappHub(contractPath, 'localnet');
|
|
109
|
+
default:
|
|
110
|
+
throw new Error(`Invalid network: ${network}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getOriginalDubhePackageId(network: string) {
|
|
92
115
|
const path = process.cwd();
|
|
93
|
-
const contractPath = `${path}/
|
|
116
|
+
const contractPath = `${path}/src/dubhe`;
|
|
94
117
|
|
|
95
118
|
switch (network) {
|
|
96
119
|
case 'mainnet':
|
|
97
|
-
return
|
|
120
|
+
return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
|
|
98
121
|
case 'testnet':
|
|
99
|
-
return
|
|
122
|
+
return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
|
|
100
123
|
case 'devnet':
|
|
101
|
-
return
|
|
124
|
+
return TESTNET_ORIGINAL_DUBHE_PACKAGE_ID;
|
|
102
125
|
case 'localnet':
|
|
103
|
-
return await
|
|
126
|
+
return await getOldPackageId(contractPath, network);
|
|
104
127
|
default:
|
|
105
128
|
throw new Error(`Invalid network: ${network}`);
|
|
106
129
|
}
|
|
107
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
|
+
}
|
|
108
138
|
|
|
109
|
-
export async function
|
|
139
|
+
export async function getOnchainResources(
|
|
110
140
|
projectPath: string,
|
|
111
141
|
network: string
|
|
112
|
-
): Promise<Record<string,
|
|
142
|
+
): Promise<Record<string, Component | MoveType>> {
|
|
113
143
|
const deployment = await getDeploymentJson(projectPath, network);
|
|
114
|
-
return deployment.
|
|
144
|
+
return deployment.resources;
|
|
115
145
|
}
|
|
116
146
|
|
|
117
147
|
export async function getVersion(projectPath: string, network: string): Promise<number> {
|
|
@@ -132,9 +162,9 @@ export async function getOldPackageId(projectPath: string, network: string): Pro
|
|
|
132
162
|
return deployment.packageId;
|
|
133
163
|
}
|
|
134
164
|
|
|
135
|
-
export async function
|
|
165
|
+
export async function getDappHub(projectPath: string, network: string): Promise<string> {
|
|
136
166
|
const deployment = await getDeploymentJson(projectPath, network);
|
|
137
|
-
return deployment.
|
|
167
|
+
return deployment.dappHub;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
export async function getUpgradeCap(projectPath: string, network: string): Promise<string> {
|
|
@@ -142,34 +172,73 @@ export async function getUpgradeCap(projectPath: string, network: string): Promi
|
|
|
142
172
|
return deployment.upgradeCap;
|
|
143
173
|
}
|
|
144
174
|
|
|
145
|
-
export function
|
|
175
|
+
export async function getStartCheckpoint(projectPath: string, network: string): Promise<string> {
|
|
176
|
+
const deployment = await getDeploymentJson(projectPath, network);
|
|
177
|
+
return deployment.startCheckpoint;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function saveContractData(
|
|
146
181
|
projectName: string,
|
|
147
182
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
183
|
+
startCheckpoint: string,
|
|
148
184
|
packageId: string,
|
|
149
|
-
|
|
185
|
+
dappHub: string,
|
|
150
186
|
upgradeCap: string,
|
|
151
187
|
version: number,
|
|
152
|
-
|
|
188
|
+
components: Record<string, Component | MoveType | EmptyComponent>,
|
|
189
|
+
resources: Record<string, Component | MoveType>,
|
|
190
|
+
enums?: Record<string, string[]>
|
|
153
191
|
) {
|
|
154
192
|
const DeploymentData: DeploymentJsonType = {
|
|
155
193
|
projectName,
|
|
156
194
|
network,
|
|
195
|
+
startCheckpoint,
|
|
157
196
|
packageId,
|
|
158
|
-
|
|
159
|
-
schemas,
|
|
197
|
+
dappHub,
|
|
160
198
|
upgradeCap,
|
|
161
|
-
version
|
|
199
|
+
version,
|
|
200
|
+
components,
|
|
201
|
+
resources,
|
|
202
|
+
enums
|
|
162
203
|
};
|
|
163
204
|
|
|
164
205
|
const path = process.cwd();
|
|
165
206
|
const storeDeploymentData = JSON.stringify(DeploymentData, null, 2);
|
|
166
|
-
writeOutput(
|
|
207
|
+
await writeOutput(
|
|
167
208
|
storeDeploymentData,
|
|
168
|
-
`${path}/
|
|
209
|
+
`${path}/src/${projectName}/.history/sui_${network}/latest.json`,
|
|
169
210
|
'Update deploy log'
|
|
170
211
|
);
|
|
171
212
|
}
|
|
172
213
|
|
|
214
|
+
export async function saveMetadata(
|
|
215
|
+
projectName: string,
|
|
216
|
+
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
217
|
+
packageId: string
|
|
218
|
+
) {
|
|
219
|
+
const path = process.cwd();
|
|
220
|
+
|
|
221
|
+
// Save metadata files
|
|
222
|
+
try {
|
|
223
|
+
const metadata = await loadMetadata(network, packageId);
|
|
224
|
+
if (metadata) {
|
|
225
|
+
const metadataJson = JSON.stringify(metadata, null, 2);
|
|
226
|
+
|
|
227
|
+
// Save packageId-specific metadata file
|
|
228
|
+
await writeOutput(
|
|
229
|
+
metadataJson,
|
|
230
|
+
`${path}/src/${projectName}/.history/sui_${network}/${packageId}.json`,
|
|
231
|
+
'Save package metadata'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Save latest metadata.json
|
|
235
|
+
await writeOutput(metadataJson, `${path}/metadata.json`, 'Save latest metadata');
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.warn(chalk.yellow(`Warning: Failed to save metadata: ${error}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
173
242
|
export async function writeOutput(
|
|
174
243
|
output: string,
|
|
175
244
|
fullOutputPath: string,
|
|
@@ -183,55 +252,486 @@ export async function writeOutput(
|
|
|
183
252
|
}
|
|
184
253
|
}
|
|
185
254
|
|
|
186
|
-
function getDubheDependency(network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'): string {
|
|
187
|
-
switch (network) {
|
|
188
|
-
case 'localnet':
|
|
189
|
-
return 'Dubhe = { local = "../dubhe-framework" }';
|
|
190
|
-
case 'testnet':
|
|
191
|
-
return 'Dubhe = { git = "https://github.com/0xobelisk/dubhe-framework.git", subdir = "contracts/dubhe", rev = "develop" }';
|
|
192
|
-
case 'mainnet':
|
|
193
|
-
return 'Dubhe = { git = "https://github.com/0xobelisk/dubhe-framework.git", subdir = "contracts/dubhe", rev = "develop" }';
|
|
194
|
-
default:
|
|
195
|
-
throw new Error(`Unsupported network: ${network}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
255
|
export async function updateDubheDependency(
|
|
200
256
|
filePath: string,
|
|
201
257
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
|
|
202
258
|
) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
259
|
+
// With the new --build-env mechanism, we keep Dubhe as local dependency for all networks.
|
|
260
|
+
// The Published.toml in ../dubhe resolves the correct on-chain address per environment.
|
|
261
|
+
// This function is kept for backward compatibility but is a no-op for non-localnet.
|
|
262
|
+
if (network === 'localnet') {
|
|
263
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
264
|
+
const localDependency = 'Dubhe = { local = "../dubhe" }';
|
|
265
|
+
if (!fileContent.includes(localDependency)) {
|
|
266
|
+
const updatedContent = fileContent.replace(/Dubhe = \{[^}]*\}/, localDependency);
|
|
267
|
+
fs.writeFileSync(filePath, updatedContent, 'utf-8');
|
|
268
|
+
console.log(`Ensured local Dubhe dependency in ${filePath} for localnet.`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Published.toml management for the new Sui CLI (v1.44+) publishing mechanism.
|
|
274
|
+
// Published.toml tracks on-chain package addresses per environment.
|
|
275
|
+
// It SHOULD be committed to source control.
|
|
276
|
+
|
|
277
|
+
interface PublishedEntry {
|
|
278
|
+
chainId: string;
|
|
279
|
+
publishedAt: string;
|
|
280
|
+
originalId: string;
|
|
281
|
+
version: number;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function readPublishedToml(packagePath: string): Record<string, PublishedEntry> {
|
|
285
|
+
const filePath = pathJoin(packagePath, 'Published.toml');
|
|
286
|
+
if (!fs.existsSync(filePath)) {
|
|
287
|
+
return {};
|
|
288
|
+
}
|
|
289
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
290
|
+
const result: Record<string, PublishedEntry> = {};
|
|
291
|
+
|
|
292
|
+
const sectionRegex = /\[published\.(\w+)\]([\s\S]*?)(?=\[published\.|$)/g;
|
|
293
|
+
let match;
|
|
294
|
+
while ((match = sectionRegex.exec(content)) !== null) {
|
|
295
|
+
const env = match[1];
|
|
296
|
+
const body = match[2];
|
|
297
|
+
const getValue = (key: string) => {
|
|
298
|
+
const m = body.match(new RegExp(`${key}\\s*=\\s*"([^"]*)"`));
|
|
299
|
+
return m ? m[1] : '';
|
|
300
|
+
};
|
|
301
|
+
const versionMatch = body.match(/version\s*=\s*(\d+)/);
|
|
302
|
+
result[env] = {
|
|
303
|
+
chainId: getValue('chain-id'),
|
|
304
|
+
publishedAt: getValue('published-at'),
|
|
305
|
+
originalId: getValue('original-id'),
|
|
306
|
+
version: versionMatch ? parseInt(versionMatch[1], 10) : 1
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function writePublishedToml(
|
|
313
|
+
packagePath: string,
|
|
314
|
+
entries: Record<string, PublishedEntry>
|
|
315
|
+
): void {
|
|
316
|
+
const filePath = pathJoin(packagePath, 'Published.toml');
|
|
317
|
+
let content =
|
|
318
|
+
'# Generated by Move\n' +
|
|
319
|
+
'# This file contains metadata about published versions of this package in different environments\n' +
|
|
320
|
+
'# This file SHOULD be committed to source control\n';
|
|
321
|
+
|
|
322
|
+
for (const [env, entry] of Object.entries(entries)) {
|
|
323
|
+
content += `\n[published.${env}]\n`;
|
|
324
|
+
content += `chain-id = "${entry.chainId}"\n`;
|
|
325
|
+
content += `published-at = "${entry.publishedAt}"\n`;
|
|
326
|
+
content += `original-id = "${entry.originalId}"\n`;
|
|
327
|
+
content += `version = ${entry.version}\n`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function updatePublishedToml(
|
|
334
|
+
packagePath: string,
|
|
335
|
+
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
336
|
+
chainId: string,
|
|
337
|
+
packageId: string,
|
|
338
|
+
originalId?: string,
|
|
339
|
+
version?: number
|
|
340
|
+
): void {
|
|
341
|
+
const entries = readPublishedToml(packagePath);
|
|
342
|
+
const existing = entries[network];
|
|
343
|
+
|
|
344
|
+
entries[network] = {
|
|
345
|
+
chainId,
|
|
346
|
+
publishedAt: packageId,
|
|
347
|
+
originalId: originalId ?? existing?.originalId ?? packageId,
|
|
348
|
+
version: version ?? (existing ? existing.version + 1 : 1)
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
writePublishedToml(packagePath, entries);
|
|
352
|
+
console.log(`Updated Published.toml in ${packagePath} for ${network}.`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function getPublishedTomlEntry(
|
|
356
|
+
packagePath: string,
|
|
357
|
+
network: string
|
|
358
|
+
): PublishedEntry | undefined {
|
|
359
|
+
const entries = readPublishedToml(packagePath);
|
|
360
|
+
return entries[network];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function clearPublishedTomlEntry(
|
|
364
|
+
packagePath: string,
|
|
365
|
+
network: string
|
|
366
|
+
): PublishedEntry | undefined {
|
|
367
|
+
const entries = readPublishedToml(packagePath);
|
|
368
|
+
const existing = entries[network];
|
|
369
|
+
if (!existing) return undefined;
|
|
370
|
+
|
|
371
|
+
entries[network] = {
|
|
372
|
+
...existing,
|
|
373
|
+
publishedAt: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
374
|
+
originalId: '0x0000000000000000000000000000000000000000000000000000000000000000'
|
|
375
|
+
};
|
|
376
|
+
writePublishedToml(packagePath, entries);
|
|
377
|
+
return existing;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function restorePublishedTomlEntry(
|
|
381
|
+
packagePath: string,
|
|
382
|
+
network: string,
|
|
383
|
+
entry: PublishedEntry
|
|
384
|
+
): void {
|
|
385
|
+
const entries = readPublishedToml(packagePath);
|
|
386
|
+
entries[network] = entry;
|
|
387
|
+
writePublishedToml(packagePath, entries);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
391
|
+
// Ephemeral publication file (Pub.<env>.toml)
|
|
392
|
+
//
|
|
393
|
+
// Per the Sui package management docs (v1.63+), localnet / devnet deployments
|
|
394
|
+
// should use ephemeral publication files rather than the shared Published.toml.
|
|
395
|
+
// The ephemeral file holds the localnet addresses so that subsequent builds
|
|
396
|
+
// (e.g. for upgrades) can resolve local dependencies correctly.
|
|
397
|
+
//
|
|
398
|
+
// Reference: https://docs.sui.io/guides/developer/packages/move-package-management
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
export interface EphemeralPubEntry {
|
|
402
|
+
/** Absolute path to the package source directory */
|
|
403
|
+
source: string;
|
|
404
|
+
/** Current on-chain address of the package */
|
|
405
|
+
publishedAt: string;
|
|
406
|
+
/** Address of the first published version (same as publishedAt for v1) */
|
|
407
|
+
originalId: string;
|
|
408
|
+
/** Object ID of the upgrade capability */
|
|
409
|
+
upgradeCap: string;
|
|
410
|
+
/** Package version (required by Sui CLI parser) */
|
|
411
|
+
version?: number;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Return the canonical path for the ephemeral publication file.
|
|
416
|
+
* For localnet this is <contractsDir>/Pub.localnet.toml.
|
|
417
|
+
*/
|
|
418
|
+
export function getEphemeralPubFilePath(contractsDir: string, network: string): string {
|
|
419
|
+
return pathJoin(contractsDir, `Pub.${network}.toml`);
|
|
208
420
|
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Update (or create) an entry in the ephemeral publication file.
|
|
424
|
+
* Preserves existing entries for other packages.
|
|
425
|
+
*/
|
|
426
|
+
export function updateEphemeralPubFile(
|
|
427
|
+
pubfilePath: string,
|
|
428
|
+
chainId: string,
|
|
429
|
+
buildEnv: string,
|
|
430
|
+
entry: EphemeralPubEntry
|
|
431
|
+
): void {
|
|
432
|
+
const existing: EphemeralPubEntry[] = [];
|
|
433
|
+
// Always use the provided buildEnv and chainId parameters.
|
|
434
|
+
// The chainId passed in comes from the live network and is authoritative.
|
|
435
|
+
const currentBuildEnv = buildEnv;
|
|
436
|
+
const currentChainId = chainId;
|
|
437
|
+
|
|
438
|
+
if (fs.existsSync(pubfilePath)) {
|
|
439
|
+
const content = fs.readFileSync(pubfilePath, 'utf-8');
|
|
440
|
+
|
|
441
|
+
// Check if the file was written for a different chain (e.g. previous localnet run).
|
|
442
|
+
// If chain-id changed, discard all existing entries — they belong to a dead chain.
|
|
443
|
+
const chainIdMatch = content.match(/^chain-id\s*=\s*"([^"]*)"/m);
|
|
444
|
+
const fileChainId = chainIdMatch ? chainIdMatch[1] : '';
|
|
445
|
+
const chainChanged = fileChainId !== '' && fileChainId !== chainId;
|
|
446
|
+
|
|
447
|
+
if (!chainChanged) {
|
|
448
|
+
// Same chain: parse existing [[published]] blocks and preserve them.
|
|
449
|
+
// source field is an inline table: source = { local = "..." }
|
|
450
|
+
const blockRegex = /\[\[published\]\]([\s\S]*?)(?=\[\[published\]\]|$)/g;
|
|
451
|
+
let blockMatch;
|
|
452
|
+
while ((blockMatch = blockRegex.exec(content)) !== null) {
|
|
453
|
+
const block = blockMatch[1];
|
|
454
|
+
const get = (key: string) => {
|
|
455
|
+
const m = block.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
|
|
456
|
+
return m ? m[1] : '';
|
|
457
|
+
};
|
|
458
|
+
// source = { local = "/path/to/package" }
|
|
459
|
+
const srcMatch = block.match(/^source\s*=\s*\{\s*local\s*=\s*"([^"]*)"\s*\}/m);
|
|
460
|
+
const src = srcMatch ? srcMatch[1] : '';
|
|
461
|
+
if (src) {
|
|
462
|
+
existing.push({
|
|
463
|
+
source: src,
|
|
464
|
+
publishedAt: get('published-at'),
|
|
465
|
+
originalId: get('original-id'),
|
|
466
|
+
upgradeCap: get('upgrade-cap')
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
console.log(` Pub file chain-id changed (${fileChainId} → ${chainId}), resetting entries.`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Update override or add the entry
|
|
476
|
+
const idx = existing.findIndex((e) => e.source === entry.source);
|
|
477
|
+
if (idx >= 0) {
|
|
478
|
+
existing[idx] = entry;
|
|
479
|
+
} else {
|
|
480
|
+
existing.push(entry);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Write the file
|
|
484
|
+
let content =
|
|
485
|
+
'# generated by dubhe cli\n' +
|
|
486
|
+
'# this file contains metadata from ephemeral publications\n' +
|
|
487
|
+
'# this file should NOT be committed to source control\n\n';
|
|
488
|
+
content += `build-env = "${currentBuildEnv}"\n`;
|
|
489
|
+
content += `chain-id = "${currentChainId}"\n`;
|
|
490
|
+
|
|
491
|
+
for (const e of existing) {
|
|
492
|
+
content += '\n[[published]]\n';
|
|
493
|
+
// source must be a LocalDepInfo struct (not a plain string)
|
|
494
|
+
content += `source = { local = "${e.source}" }\n`;
|
|
495
|
+
content += `published-at = "${e.publishedAt}"\n`;
|
|
496
|
+
content += `original-id = "${e.originalId}"\n`;
|
|
497
|
+
content += `upgrade-cap = "${e.upgradeCap}"\n`;
|
|
498
|
+
// version is required by Sui CLI parser (even though docs omit it)
|
|
499
|
+
content += `version = 1\n`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fs.writeFileSync(pubfilePath, content, 'utf-8');
|
|
503
|
+
console.log(
|
|
504
|
+
` Updated ${pathJoin(pubfilePath.split('/').slice(-1)[0])} for ${
|
|
505
|
+
entry.source.split('/').slice(-1)[0]
|
|
506
|
+
}.`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function checkRpcAvailability(rpcUrl: string): Promise<boolean> {
|
|
511
|
+
try {
|
|
512
|
+
const response = await fetch(rpcUrl, {
|
|
513
|
+
method: 'POST',
|
|
514
|
+
headers: {
|
|
515
|
+
'Content-Type': 'application/json'
|
|
516
|
+
},
|
|
517
|
+
body: JSON.stringify({
|
|
518
|
+
jsonrpc: '2.0',
|
|
519
|
+
id: 1,
|
|
520
|
+
method: 'sui_getLatestCheckpointSequenceNumber',
|
|
521
|
+
params: []
|
|
522
|
+
})
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
if (!response.ok) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const data = await response.json();
|
|
530
|
+
return !data.error;
|
|
531
|
+
} catch (_error) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function addEnv(
|
|
537
|
+
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
|
|
538
|
+
): Promise<void> {
|
|
539
|
+
const rpcMap = {
|
|
540
|
+
localnet: 'http://127.0.0.1:9000',
|
|
541
|
+
devnet: 'https://fullnode.devnet.sui.io:443/',
|
|
542
|
+
testnet: 'https://fullnode.testnet.sui.io:443/',
|
|
543
|
+
mainnet: 'https://fullnode.mainnet.sui.io:443/'
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const rpcUrl = rpcMap[network];
|
|
547
|
+
|
|
548
|
+
// Check RPC availability first
|
|
549
|
+
const isRpcAvailable = await checkRpcAvailability(rpcUrl);
|
|
550
|
+
if (!isRpcAvailable) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`RPC endpoint ${rpcUrl} is not available. Please check your network connection or try again later.`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return new Promise<void>((resolve, reject) => {
|
|
557
|
+
let errorOutput = '';
|
|
558
|
+
let stdoutOutput = '';
|
|
559
|
+
|
|
560
|
+
const suiProcess = spawn(
|
|
561
|
+
'sui',
|
|
562
|
+
['client', 'new-env', '--alias', network, '--rpc', rpcMap[network]],
|
|
563
|
+
{
|
|
564
|
+
env: { ...process.env },
|
|
565
|
+
stdio: 'pipe'
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// Capture standard output
|
|
570
|
+
suiProcess.stdout.on('data', (data) => {
|
|
571
|
+
stdoutOutput += data.toString();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Capture error output
|
|
575
|
+
suiProcess.stderr.on('data', (data) => {
|
|
576
|
+
errorOutput += data.toString();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Handle process errors (e.g., command not found)
|
|
580
|
+
suiProcess.on('error', (error) => {
|
|
581
|
+
console.error(chalk.red(`\n❌ Failed to execute sui command: ${error.message}`));
|
|
582
|
+
reject(new Error(`Failed to execute sui command: ${error.message}`));
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Handle process exit
|
|
586
|
+
suiProcess.on('exit', (code, signal) => {
|
|
587
|
+
// Check if "already exists" message is present
|
|
588
|
+
if (errorOutput.includes('already exists') || stdoutOutput.includes('already exists')) {
|
|
589
|
+
console.log(chalk.yellow(`Environment ${network} already exists, proceeding...`));
|
|
590
|
+
resolve();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (code === 0) {
|
|
595
|
+
console.log(chalk.green(`Successfully added environment ${network}`));
|
|
596
|
+
resolve();
|
|
597
|
+
} else {
|
|
598
|
+
let finalError: string;
|
|
599
|
+
if (code === null) {
|
|
600
|
+
// Process was killed by a signal
|
|
601
|
+
finalError =
|
|
602
|
+
errorOutput ||
|
|
603
|
+
stdoutOutput ||
|
|
604
|
+
`Process was terminated by signal ${signal || 'unknown'}`;
|
|
605
|
+
} else {
|
|
606
|
+
finalError = errorOutput || stdoutOutput || `Process exited with code ${code}`;
|
|
607
|
+
}
|
|
608
|
+
console.error(chalk.red(`\n❌ Failed to add environment ${network}`));
|
|
609
|
+
console.error(chalk.red(` └─ ${finalError.trim()}`));
|
|
610
|
+
reject(new Error(finalError));
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export type NetworkAlias = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
|
|
617
|
+
|
|
618
|
+
export interface Endpoint {
|
|
619
|
+
alias: NetworkAlias;
|
|
620
|
+
rpc: string;
|
|
621
|
+
ws: string | null;
|
|
622
|
+
basic_auth: { username: string; password: string } | null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// mainly is a tuple of [endpoint list, current active alias]
|
|
626
|
+
export type ConfigTuple = [Endpoint[], NetworkAlias];
|
|
627
|
+
|
|
628
|
+
export async function envsJSON(): Promise<ConfigTuple> {
|
|
629
|
+
try {
|
|
630
|
+
return new Promise<ConfigTuple>((resolve, reject) => {
|
|
631
|
+
let errorOutput = '';
|
|
632
|
+
let stdoutOutput = '';
|
|
633
|
+
|
|
634
|
+
const suiProcess = spawn('sui', ['client', 'envs', '--json'], {
|
|
635
|
+
env: { ...process.env },
|
|
636
|
+
stdio: 'pipe'
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
suiProcess.stdout.on('data', (data) => {
|
|
640
|
+
stdoutOutput += data.toString();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
suiProcess.stderr.on('data', (data) => {
|
|
644
|
+
errorOutput += data.toString();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
suiProcess.on('error', (error) => {
|
|
648
|
+
console.error(chalk.red(`\n❌ Failed to execute sui command: ${error.message}`));
|
|
649
|
+
reject(new Error(`Failed to execute sui command: ${error.message}`));
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
suiProcess.on('exit', (code, signal) => {
|
|
653
|
+
if (code === 0) {
|
|
654
|
+
resolve(JSON.parse(stdoutOutput) as ConfigTuple);
|
|
655
|
+
} else {
|
|
656
|
+
let finalError: string;
|
|
657
|
+
if (code === null) {
|
|
658
|
+
// Process was killed by a signal
|
|
659
|
+
finalError =
|
|
660
|
+
errorOutput ||
|
|
661
|
+
stdoutOutput ||
|
|
662
|
+
`Process was terminated by signal ${signal || 'unknown'}`;
|
|
663
|
+
} else {
|
|
664
|
+
finalError = errorOutput || stdoutOutput || `Process exited with code ${code}`;
|
|
665
|
+
}
|
|
666
|
+
console.error(chalk.red(`\n❌ Failed to get envs`));
|
|
667
|
+
console.error(chalk.red(` └─ ${finalError.trim()}`));
|
|
668
|
+
reject(new Error(finalError));
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
} catch (error) {
|
|
673
|
+
// Re-throw the error for the caller to handle
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function getDefaultNetwork(): Promise<NetworkAlias> {
|
|
679
|
+
const [_, currentAlias] = await envsJSON();
|
|
680
|
+
return currentAlias as NetworkAlias;
|
|
681
|
+
}
|
|
682
|
+
|
|
209
683
|
export async function switchEnv(network: 'mainnet' | 'testnet' | 'devnet' | 'localnet') {
|
|
210
684
|
try {
|
|
685
|
+
// First, try to add the environment
|
|
686
|
+
await addEnv(network);
|
|
687
|
+
|
|
688
|
+
// Then switch to the specified environment
|
|
211
689
|
return new Promise<void>((resolve, reject) => {
|
|
690
|
+
let errorOutput = '';
|
|
691
|
+
let stdoutOutput = '';
|
|
692
|
+
|
|
212
693
|
const suiProcess = spawn('sui', ['client', 'switch', '--env', network], {
|
|
213
694
|
env: { ...process.env },
|
|
214
695
|
stdio: 'pipe'
|
|
215
696
|
});
|
|
216
697
|
|
|
698
|
+
suiProcess.stdout.on('data', (data) => {
|
|
699
|
+
stdoutOutput += data.toString();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
suiProcess.stderr.on('data', (data) => {
|
|
703
|
+
errorOutput += data.toString();
|
|
704
|
+
});
|
|
705
|
+
|
|
217
706
|
suiProcess.on('error', (error) => {
|
|
218
|
-
console.error(chalk.red(
|
|
219
|
-
|
|
220
|
-
reject(error); // Reject promise on error
|
|
707
|
+
console.error(chalk.red(`\n❌ Failed to execute sui command: ${error.message}`));
|
|
708
|
+
reject(new Error(`Failed to execute sui command: ${error.message}`));
|
|
221
709
|
});
|
|
222
710
|
|
|
223
|
-
suiProcess.on('exit', (code) => {
|
|
224
|
-
if (code
|
|
225
|
-
console.
|
|
226
|
-
|
|
711
|
+
suiProcess.on('exit', (code, signal) => {
|
|
712
|
+
if (code === 0) {
|
|
713
|
+
console.log(chalk.green(`Successfully switched to environment ${network}`));
|
|
714
|
+
resolve();
|
|
227
715
|
} else {
|
|
228
|
-
|
|
716
|
+
let finalError: string;
|
|
717
|
+
if (code === null) {
|
|
718
|
+
// Process was killed by a signal
|
|
719
|
+
finalError =
|
|
720
|
+
errorOutput ||
|
|
721
|
+
stdoutOutput ||
|
|
722
|
+
`Process was terminated by signal ${signal || 'unknown'}`;
|
|
723
|
+
} else {
|
|
724
|
+
finalError = errorOutput || stdoutOutput || `Process exited with code ${code}`;
|
|
725
|
+
}
|
|
726
|
+
console.error(chalk.red(`\n❌ Failed to switch to environment ${network}`));
|
|
727
|
+
console.error(chalk.red(` └─ ${finalError.trim()}`));
|
|
728
|
+
reject(new Error(finalError));
|
|
229
729
|
}
|
|
230
730
|
});
|
|
231
731
|
});
|
|
232
732
|
} catch (error) {
|
|
233
|
-
|
|
234
|
-
|
|
733
|
+
// Re-throw the error for the caller to handle
|
|
734
|
+
throw error;
|
|
235
735
|
}
|
|
236
736
|
}
|
|
237
737
|
|
|
@@ -271,3 +771,220 @@ export function initializeDubhe({
|
|
|
271
771
|
metadata
|
|
272
772
|
});
|
|
273
773
|
}
|
|
774
|
+
|
|
775
|
+
export function generateConfigJson(config: DubheConfig): string {
|
|
776
|
+
const components = Object.entries(config.components).map(([name, component]) => {
|
|
777
|
+
if (typeof component === 'string') {
|
|
778
|
+
return {
|
|
779
|
+
[name]: {
|
|
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
|
+
});
|
|
822
|
+
|
|
823
|
+
const resources = Object.entries(config.resources).map(([name, resource]) => {
|
|
824
|
+
// Simple type shorthand (e.g., counter1: 'u32') – entity-keyed by account (entity_id: String).
|
|
825
|
+
if (typeof resource === 'string') {
|
|
826
|
+
return {
|
|
827
|
+
[name]: {
|
|
828
|
+
fields: [{ entity_id: 'String' }, { value: resource }],
|
|
829
|
+
keys: ['entity_id'],
|
|
830
|
+
offchain: false
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Empty resource object – only the implicit entity key.
|
|
836
|
+
if (Object.keys(resource as object).length === 0) {
|
|
837
|
+
return {
|
|
838
|
+
[name]: {
|
|
839
|
+
fields: [{ entity_id: 'String' }],
|
|
840
|
+
keys: ['entity_id'],
|
|
841
|
+
offchain: false
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const fields = (resource as any).fields || {};
|
|
847
|
+
const keys = (resource as any).keys || [];
|
|
848
|
+
const offchain = (resource as any).offchain ?? false;
|
|
849
|
+
|
|
850
|
+
// Full Component format with no explicit keys: auto-inject 'entity_id: String'.
|
|
851
|
+
if (keys.length === 0) {
|
|
852
|
+
const fieldEntries = Object.entries(fields);
|
|
853
|
+
const orderedFields: [string, unknown][] = [['entity_id', 'String'], ...fieldEntries];
|
|
854
|
+
return {
|
|
855
|
+
[name]: {
|
|
856
|
+
fields: orderedFields.map(([fieldName, fieldType]) => ({
|
|
857
|
+
[fieldName]: fieldType
|
|
858
|
+
})),
|
|
859
|
+
keys: ['entity_id'],
|
|
860
|
+
offchain: offchain
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Full Component format with explicit custom keys: inject 'entity_id: String' as the first
|
|
866
|
+
// field and first key so that key_tuple[0] (the BCS-encoded account injected by the indexer)
|
|
867
|
+
// maps correctly, followed by the user-defined keys.
|
|
868
|
+
const fieldEntries = Object.entries(fields);
|
|
869
|
+
const orderedFields: [string, unknown][] = [['entity_id', 'String'], ...fieldEntries];
|
|
870
|
+
return {
|
|
871
|
+
[name]: {
|
|
872
|
+
fields: orderedFields.map(([fieldName, fieldType]) => ({
|
|
873
|
+
[fieldName]: fieldType
|
|
874
|
+
})),
|
|
875
|
+
keys: ['entity_id', ...keys],
|
|
876
|
+
offchain: offchain
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
|
|
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
|
+
// handle enums
|
|
901
|
+
const enums = Object.entries(config.enums || {}).map(([name, enumFields]) => {
|
|
902
|
+
// Sort enum values by first letter
|
|
903
|
+
const sortedFields = enumFields.sort((a, b) => a.localeCompare(b)).map((value) => value);
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
[name]: sortedFields
|
|
907
|
+
};
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
return JSON.stringify(
|
|
911
|
+
{
|
|
912
|
+
components,
|
|
913
|
+
resources,
|
|
914
|
+
enums
|
|
915
|
+
},
|
|
916
|
+
null,
|
|
917
|
+
2
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Updates the dubhe address and published-at in Move.toml file
|
|
923
|
+
* @param path - Directory path containing Move.toml file
|
|
924
|
+
* @param packageAddress - New dubhe package address to set
|
|
925
|
+
*
|
|
926
|
+
* Logic:
|
|
927
|
+
* - If packageAddress is "0x0": only set dubhe = "0x0", remove published-at line
|
|
928
|
+
* - Otherwise: set both dubhe and published-at to packageAddress
|
|
929
|
+
*/
|
|
930
|
+
export function updateMoveTomlAddress(path: string, packageAddress: string) {
|
|
931
|
+
const moveTomlPath = `${path}/Move.toml`;
|
|
932
|
+
const moveTomlContent = fs.readFileSync(moveTomlPath, 'utf-8');
|
|
933
|
+
|
|
934
|
+
let updatedContent = moveTomlContent;
|
|
935
|
+
|
|
936
|
+
if (packageAddress === '0x0') {
|
|
937
|
+
// Case 1: Address is "0x0" - set dubhe to "0x0" and remove published-at line
|
|
938
|
+
updatedContent = updatedContent.replace(/dubhe\s*=\s*"[^"]*"/, `dubhe = "0x0"`);
|
|
939
|
+
|
|
940
|
+
// Remove published-at line (including the line break)
|
|
941
|
+
updatedContent = updatedContent.replace(/published-at\s*=\s*"[^"]*"\r?\n?/, '');
|
|
942
|
+
} else {
|
|
943
|
+
// Case 2: Address is not "0x0" - set both dubhe and published-at
|
|
944
|
+
updatedContent = updatedContent.replace(/dubhe\s*=\s*"[^"]*"/, `dubhe = "${packageAddress}"`);
|
|
945
|
+
|
|
946
|
+
// Check if published-at already exists
|
|
947
|
+
if (/published-at\s*=\s*"[^"]*"/.test(updatedContent)) {
|
|
948
|
+
// Replace existing published-at
|
|
949
|
+
updatedContent = updatedContent.replace(
|
|
950
|
+
/published-at\s*=\s*"[^"]*"/,
|
|
951
|
+
`published-at = "${packageAddress}"`
|
|
952
|
+
);
|
|
953
|
+
} else {
|
|
954
|
+
// Add published-at after [package] line if it doesn't exist
|
|
955
|
+
updatedContent = updatedContent.replace(
|
|
956
|
+
/(\[package\][^\n]*\n)/,
|
|
957
|
+
`$1published-at = "${packageAddress}"\n`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
fs.writeFileSync(moveTomlPath, updatedContent, 'utf-8');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export function updateGenesisUpgradeFunction(path: string, tables: string[]) {
|
|
966
|
+
const genesisPath = `${path}/sources/codegen/genesis.move`;
|
|
967
|
+
const genesisContent = fs.readFileSync(genesisPath, 'utf-8');
|
|
968
|
+
|
|
969
|
+
// Match the first pair of // ========================================== lines (with any content, including empty, between them)
|
|
970
|
+
const separatorRegex =
|
|
971
|
+
/(\/\/ ==========================================)[\s\S]*?(\/\/ ==========================================)/;
|
|
972
|
+
const match = genesisContent.match(separatorRegex);
|
|
973
|
+
|
|
974
|
+
if (!match) {
|
|
975
|
+
throw new Error('Could not find separator comments in genesis.move');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Generate new table registration code
|
|
979
|
+
const registerTablesCode = tables
|
|
980
|
+
.map((table) => ` ${table}::register_table(dapp_hub, ctx);`)
|
|
981
|
+
.join('\n');
|
|
982
|
+
|
|
983
|
+
// Build new content, preserve separators, replace middle content
|
|
984
|
+
const newContent = `${match[1]}\n${registerTablesCode}\n${match[2]}`;
|
|
985
|
+
|
|
986
|
+
// Replace matched content
|
|
987
|
+
const updatedContent = genesisContent.replace(separatorRegex, newContent);
|
|
988
|
+
|
|
989
|
+
fs.writeFileSync(genesisPath, updatedContent, 'utf-8');
|
|
990
|
+
}
|