@0xobelisk/sui-cli 1.2.0-pre.11 → 1.2.0-pre.110
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 +126 -49
- 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 -6
- 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 +330 -293
- package/src/utils/queryStorage.ts +141 -141
- package/src/utils/startNode.ts +115 -16
- package/src/utils/storeConfig.ts +6 -12
- package/src/utils/upgradeHandler.ts +147 -86
- package/src/utils/utils.ts +771 -55
|
@@ -3,19 +3,49 @@ import { execSync } from 'child_process';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import {
|
|
5
5
|
saveContractData,
|
|
6
|
-
|
|
6
|
+
updateMoveTomlAddress,
|
|
7
7
|
switchEnv,
|
|
8
8
|
delay,
|
|
9
|
-
|
|
10
|
-
initializeDubhe
|
|
9
|
+
getDubheDappHub,
|
|
10
|
+
initializeDubhe,
|
|
11
|
+
saveMetadata,
|
|
12
|
+
getOriginalDubhePackageId,
|
|
13
|
+
updatePublishedToml,
|
|
14
|
+
updateEphemeralPubFile,
|
|
15
|
+
getEphemeralPubFilePath,
|
|
16
|
+
getPublishedTomlEntry,
|
|
17
|
+
clearPublishedTomlEntry,
|
|
18
|
+
restorePublishedTomlEntry
|
|
11
19
|
} from './utils';
|
|
12
20
|
import { DubheConfig } from '@0xobelisk/sui-common';
|
|
13
21
|
import * as fs from 'fs';
|
|
14
22
|
import * as path from 'path';
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Temporarily add localnet to Move.toml [environments] section before building.
|
|
26
|
+
* Sui CLI 1.40+ requires the active environment to be declared in Move.toml even
|
|
27
|
+
* when --build-env is specified. This patches the file and returns the original
|
|
28
|
+
* content so the caller can restore it in a finally block.
|
|
29
|
+
* Returns null if no changes were needed.
|
|
30
|
+
*/
|
|
31
|
+
function patchMoveTomlWithLocalnetEnv(moveTomlPath: string, chainId: string): string | null {
|
|
32
|
+
if (!fs.existsSync(moveTomlPath)) return null;
|
|
33
|
+
const content = fs.readFileSync(moveTomlPath, 'utf-8');
|
|
34
|
+
|
|
35
|
+
if (content.includes('localnet')) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let updatedContent: string;
|
|
40
|
+
if (content.includes('[environments]')) {
|
|
41
|
+
updatedContent = content.replace('[environments]', `[environments]\nlocalnet = "${chainId}"`);
|
|
42
|
+
} else {
|
|
43
|
+
updatedContent = content.trimEnd() + `\n\n[environments]\nlocalnet = "${chainId}"\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(moveTomlPath, updatedContent, 'utf-8');
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
19
49
|
|
|
20
50
|
async function removeEnvContent(
|
|
21
51
|
filePath: string,
|
|
@@ -127,21 +157,53 @@ published-version = "${config.publishedVersion}"
|
|
|
127
157
|
// return segments.length > 0 ? segments[segments.length - 1] : '';
|
|
128
158
|
// }
|
|
129
159
|
|
|
130
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Build a Move package and return [modules, dependencies] as base64 arrays.
|
|
162
|
+
*
|
|
163
|
+
* For localnet (ephemeral) networks:
|
|
164
|
+
* - Uses --build-env testnet so dependency addresses are resolved via testnet
|
|
165
|
+
* Published.toml (no need to add 'localnet' to Move.toml [environments]).
|
|
166
|
+
* - Optionally reads a Pub.localnet.toml pubfile for already-published local deps.
|
|
167
|
+
* - This matches the Sui docs approach:
|
|
168
|
+
* https://docs.sui.io/guides/developer/packages/move-package-management
|
|
169
|
+
*
|
|
170
|
+
* For persistent networks (testnet/mainnet/devnet): uses -e <network> as before.
|
|
171
|
+
*/
|
|
172
|
+
function buildContract(
|
|
173
|
+
projectPath: string,
|
|
174
|
+
network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
175
|
+
pubfilePath?: string
|
|
176
|
+
): string[][] {
|
|
131
177
|
let modules: any, dependencies: any;
|
|
132
178
|
try {
|
|
179
|
+
let buildEnvFlag: string;
|
|
180
|
+
if (network === 'localnet') {
|
|
181
|
+
// Ephemeral approach: resolve deps via testnet configuration.
|
|
182
|
+
// --pubfile-path supplies already-published local dep addresses (e.g. dubhe).
|
|
183
|
+
buildEnvFlag = ' --build-env testnet';
|
|
184
|
+
if (pubfilePath) {
|
|
185
|
+
buildEnvFlag += ` --pubfile-path ${pubfilePath}`;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
buildEnvFlag = network ? ` -e ${network}` : '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --no-tree-shaking avoids on-chain RPC calls during build.
|
|
133
192
|
const buildResult = JSON.parse(
|
|
134
|
-
execSync(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
193
|
+
execSync(
|
|
194
|
+
`sui move build --dump-bytecode-as-base64 --no-tree-shaking${buildEnvFlag} --path ${projectPath}`,
|
|
195
|
+
{
|
|
196
|
+
encoding: 'utf-8',
|
|
197
|
+
stdio: 'pipe'
|
|
198
|
+
}
|
|
199
|
+
)
|
|
138
200
|
);
|
|
139
201
|
modules = buildResult.modules;
|
|
140
202
|
dependencies = buildResult.dependencies;
|
|
141
203
|
} catch (error: any) {
|
|
142
204
|
console.error(chalk.red(' └─ Build failed'));
|
|
143
|
-
console.error(error.stdout);
|
|
144
|
-
|
|
205
|
+
console.error(error.stdout || error.stderr);
|
|
206
|
+
throw new Error(`Build failed: ${error.stdout || error.stderr || error.message}`);
|
|
145
207
|
}
|
|
146
208
|
return [modules, dependencies];
|
|
147
209
|
}
|
|
@@ -154,132 +216,28 @@ interface ObjectChange {
|
|
|
154
216
|
}
|
|
155
217
|
|
|
156
218
|
async function waitForNode(dubhe: Dubhe): Promise<string> {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const handleInterrupt = () => {
|
|
165
|
-
isInterrupted = true;
|
|
166
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
167
|
-
console.log('\n └─ Operation cancelled by user');
|
|
168
|
-
process.exit(0);
|
|
169
|
-
};
|
|
170
|
-
process.on('SIGINT', handleInterrupt);
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// 第一阶段:等待获取 chainId
|
|
174
|
-
while (retryCount < MAX_RETRIES && !isInterrupted && !chainId) {
|
|
175
|
-
try {
|
|
176
|
-
chainId = await dubhe.suiInteractor.currentClient.getChainIdentifier();
|
|
177
|
-
} catch (error) {
|
|
178
|
-
// 忽略错误,继续重试
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (isInterrupted) break;
|
|
182
|
-
|
|
183
|
-
if (!chainId) {
|
|
184
|
-
retryCount++;
|
|
185
|
-
if (retryCount === MAX_RETRIES) {
|
|
186
|
-
console.log(chalk.red(` └─ Failed to connect to node after ${MAX_RETRIES} attempts`));
|
|
187
|
-
console.log(chalk.red(' └─ Please check if the Sui node is running.'));
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
|
|
192
|
-
const spinner = SPINNER[spinnerIndex % SPINNER.length];
|
|
193
|
-
spinnerIndex++;
|
|
194
|
-
|
|
195
|
-
process.stdout.write(`\r ├─ ${spinner} Waiting for node... (${elapsedTime}s)`);
|
|
196
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 显示 chainId
|
|
201
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
202
|
-
console.log(` ├─ ChainId: ${chainId}`);
|
|
203
|
-
|
|
204
|
-
// 第二阶段:检查部署账户余额
|
|
205
|
-
retryCount = 0;
|
|
206
|
-
while (retryCount < MAX_RETRIES && !isInterrupted) {
|
|
207
|
-
try {
|
|
208
|
-
const address = dubhe.getAddress();
|
|
209
|
-
const coins = await dubhe.suiInteractor.currentClient.getCoins({
|
|
210
|
-
owner: address,
|
|
211
|
-
coinType: '0x2::sui::SUI'
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (coins.data.length > 0) {
|
|
215
|
-
const balance = coins.data.reduce((sum, coin) => sum + Number(coin.balance), 0);
|
|
216
|
-
if (balance > 0) {
|
|
217
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
218
|
-
console.log(` ├─ Deployer balance: ${balance / 10 ** 9} SUI`);
|
|
219
|
-
return chainId;
|
|
220
|
-
} else if (!hasShownBalanceWarning) {
|
|
221
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
222
|
-
console.log(
|
|
223
|
-
chalk.yellow(
|
|
224
|
-
` ├─ Deployer balance: 0 SUI, please ensure your account has sufficient SUI balance`
|
|
225
|
-
)
|
|
226
|
-
);
|
|
227
|
-
hasShownBalanceWarning = true;
|
|
228
|
-
}
|
|
229
|
-
} else if (!hasShownBalanceWarning) {
|
|
230
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
231
|
-
console.log(
|
|
232
|
-
chalk.yellow(
|
|
233
|
-
` ├─ No SUI coins found in deployer account, please ensure your account has sufficient SUI balance`
|
|
234
|
-
)
|
|
235
|
-
);
|
|
236
|
-
hasShownBalanceWarning = true;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
retryCount++;
|
|
240
|
-
if (retryCount === MAX_RETRIES) {
|
|
241
|
-
console.log(
|
|
242
|
-
chalk.red(` └─ Deployer account has no SUI balance after ${MAX_RETRIES} attempts`)
|
|
243
|
-
);
|
|
244
|
-
console.log(chalk.red(' └─ Please ensure your account has sufficient SUI balance.'));
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
|
|
249
|
-
const spinner = SPINNER[spinnerIndex % SPINNER.length];
|
|
250
|
-
spinnerIndex++;
|
|
251
|
-
|
|
252
|
-
process.stdout.write(`\r ├─ ${spinner} Checking deployer balance... (${elapsedTime}s)`);
|
|
253
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (isInterrupted) break;
|
|
256
|
-
|
|
257
|
-
retryCount++;
|
|
258
|
-
if (retryCount === MAX_RETRIES) {
|
|
259
|
-
console.log(
|
|
260
|
-
chalk.red(` └─ Failed to check deployer balance after ${MAX_RETRIES} attempts`)
|
|
261
|
-
);
|
|
262
|
-
console.log(chalk.red(' └─ Please check your account and network connection.'));
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
|
|
267
|
-
const spinner = SPINNER[spinnerIndex % SPINNER.length];
|
|
268
|
-
spinnerIndex++;
|
|
219
|
+
const chainId = await dubhe.suiInteractor.currentClient.getChainIdentifier();
|
|
220
|
+
console.log(` ├─ ChainId: ${chainId}`);
|
|
221
|
+
const address = dubhe.getAddress();
|
|
222
|
+
const coins = await dubhe.suiInteractor.currentClient.getCoins({
|
|
223
|
+
owner: address,
|
|
224
|
+
coinType: '0x2::sui::SUI'
|
|
225
|
+
});
|
|
269
226
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
227
|
+
if (coins.data.length > 0) {
|
|
228
|
+
const balance = coins.data.reduce((sum, coin) => sum + Number(coin.balance), 0);
|
|
229
|
+
if (balance > 0) {
|
|
230
|
+
console.log(` ├─ Deployer balance: ${balance / 10 ** 9} SUI`);
|
|
231
|
+
return chainId;
|
|
232
|
+
} else {
|
|
233
|
+
console.log(
|
|
234
|
+
chalk.yellow(
|
|
235
|
+
` ├─ Deployer balance: 0 SUI, please ensure your account has sufficient SUI balance`
|
|
236
|
+
)
|
|
237
|
+
);
|
|
273
238
|
}
|
|
274
|
-
} finally {
|
|
275
|
-
process.removeListener('SIGINT', handleInterrupt);
|
|
276
239
|
}
|
|
277
|
-
|
|
278
|
-
if (isInterrupted) {
|
|
279
|
-
process.exit(0);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
throw new Error('Failed to connect to node');
|
|
240
|
+
return chainId;
|
|
283
241
|
}
|
|
284
242
|
|
|
285
243
|
async function publishContract(
|
|
@@ -287,7 +245,8 @@ async function publishContract(
|
|
|
287
245
|
dubheConfig: DubheConfig,
|
|
288
246
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
289
247
|
projectPath: string,
|
|
290
|
-
gasBudget?: number
|
|
248
|
+
gasBudget?: number,
|
|
249
|
+
force?: boolean
|
|
291
250
|
) {
|
|
292
251
|
console.log('\n🚀 Starting Contract Publication...');
|
|
293
252
|
console.log(` ├─ Project: ${projectPath}`);
|
|
@@ -301,7 +260,79 @@ async function publishContract(
|
|
|
301
260
|
console.log(` └─ Account: ${dubhe.getAddress()}`);
|
|
302
261
|
|
|
303
262
|
console.log('\n📦 Building Contract...');
|
|
304
|
-
|
|
263
|
+
// For localnet: pass the ephemeral pubfile so the build system can resolve
|
|
264
|
+
// the dubhe dependency that was just published in publishDubheFramework().
|
|
265
|
+
const pubfilePath =
|
|
266
|
+
network === 'localnet' ? getEphemeralPubFilePath(process.cwd(), network) : undefined;
|
|
267
|
+
|
|
268
|
+
// So the build uses package address 0x0: for localnet always remove the contract's
|
|
269
|
+
// Published.toml; for testnet/mainnet/devnet only when --force (clear current network entry).
|
|
270
|
+
// Otherwise Sui CLI bakes the existing [published.<network>] address into the bytecode and
|
|
271
|
+
// the chain rejects with PublishErrorNonZeroAddress.
|
|
272
|
+
const contractPublishedTomlPath = `${projectPath}/Published.toml`;
|
|
273
|
+
let savedContractPublishedToml: string | null = null;
|
|
274
|
+
let savedContractPublishedEntry: {
|
|
275
|
+
network: string;
|
|
276
|
+
entry: Exclude<ReturnType<typeof getPublishedTomlEntry>, undefined>;
|
|
277
|
+
} | null = null;
|
|
278
|
+
if (network === 'localnet' && fs.existsSync(contractPublishedTomlPath)) {
|
|
279
|
+
savedContractPublishedToml = fs.readFileSync(contractPublishedTomlPath, 'utf-8');
|
|
280
|
+
fs.unlinkSync(contractPublishedTomlPath);
|
|
281
|
+
} else if (force && (network === 'testnet' || network === 'mainnet' || network === 'devnet')) {
|
|
282
|
+
const entry = getPublishedTomlEntry(projectPath, network);
|
|
283
|
+
if (entry) {
|
|
284
|
+
savedContractPublishedEntry = { network, entry };
|
|
285
|
+
clearPublishedTomlEntry(projectPath, network);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// For localnet: also temporarily remove dubhe's Published.toml when building the
|
|
290
|
+
// contract package. After publishDubheFramework restores dubhe/Published.toml, its
|
|
291
|
+
// testnet entry's original-id may be "0x0", which collides with the contract package's
|
|
292
|
+
// own address (also 0x0 before publish), triggering a spurious cyclic-dependency error.
|
|
293
|
+
const dubhePublishedTomlPath = path.join(path.dirname(projectPath), 'dubhe', 'Published.toml');
|
|
294
|
+
let savedDubhePublishedToml: string | null = null;
|
|
295
|
+
if (network === 'localnet' && fs.existsSync(dubhePublishedTomlPath)) {
|
|
296
|
+
savedDubhePublishedToml = fs.readFileSync(dubhePublishedTomlPath, 'utf-8');
|
|
297
|
+
fs.unlinkSync(dubhePublishedTomlPath);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Sui CLI 1.40+ checks that the active environment is declared in Move.toml
|
|
301
|
+
// even when --build-env is specified. Temporarily inject localnet into [environments]
|
|
302
|
+
// 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
|
+
if (network === 'localnet') {
|
|
308
|
+
savedContractMoveToml = patchMoveTomlWithLocalnetEnv(contractMoveTomlPath, chainId);
|
|
309
|
+
savedDubheMoveToml = patchMoveTomlWithLocalnetEnv(dubheMoveTomlPath, chainId);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let modules: any, dependencies: any;
|
|
313
|
+
try {
|
|
314
|
+
[modules, dependencies] = buildContract(projectPath, network, pubfilePath);
|
|
315
|
+
} finally {
|
|
316
|
+
if (savedContractPublishedToml !== null) {
|
|
317
|
+
fs.writeFileSync(contractPublishedTomlPath, savedContractPublishedToml, 'utf-8');
|
|
318
|
+
}
|
|
319
|
+
if (savedContractPublishedEntry !== null) {
|
|
320
|
+
restorePublishedTomlEntry(
|
|
321
|
+
projectPath,
|
|
322
|
+
savedContractPublishedEntry.network,
|
|
323
|
+
savedContractPublishedEntry.entry
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (savedDubhePublishedToml !== null) {
|
|
327
|
+
fs.writeFileSync(dubhePublishedTomlPath, savedDubhePublishedToml, 'utf-8');
|
|
328
|
+
}
|
|
329
|
+
if (savedContractMoveToml !== null) {
|
|
330
|
+
fs.writeFileSync(contractMoveTomlPath, savedContractMoveToml, 'utf-8');
|
|
331
|
+
}
|
|
332
|
+
if (savedDubheMoveToml !== null) {
|
|
333
|
+
fs.writeFileSync(dubheMoveTomlPath, savedDubheMoveToml, 'utf-8');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
305
336
|
|
|
306
337
|
console.log('\n🔄 Publishing Contract...');
|
|
307
338
|
const tx = new Transaction();
|
|
@@ -311,63 +342,39 @@ async function publishContract(
|
|
|
311
342
|
const [upgradeCap] = tx.publish({ modules, dependencies });
|
|
312
343
|
tx.transferObjects([upgradeCap], dubhe.getAddress());
|
|
313
344
|
|
|
314
|
-
let result
|
|
315
|
-
let retryCount = 0;
|
|
316
|
-
let spinnerIndex = 0;
|
|
317
|
-
const startTime = Date.now();
|
|
318
|
-
let isInterrupted = false;
|
|
319
|
-
|
|
320
|
-
const handleInterrupt = () => {
|
|
321
|
-
isInterrupted = true;
|
|
322
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
323
|
-
console.log('\n └─ Operation cancelled by user');
|
|
324
|
-
process.exit(0);
|
|
325
|
-
};
|
|
326
|
-
process.on('SIGINT', handleInterrupt);
|
|
327
|
-
|
|
345
|
+
let result;
|
|
328
346
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const spinner = SPINNER[spinnerIndex % SPINNER.length];
|
|
344
|
-
spinnerIndex++;
|
|
345
|
-
|
|
346
|
-
process.stdout.write(`\r ├─ ${spinner} Retrying... (${elapsedTime}s)`);
|
|
347
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
|
|
348
|
-
}
|
|
347
|
+
result = await dubhe.signAndSendTxn({ tx });
|
|
348
|
+
} catch (error: any) {
|
|
349
|
+
console.error(chalk.red(' └─ Publication failed'));
|
|
350
|
+
console.error(error.message);
|
|
351
|
+
if (
|
|
352
|
+
!force &&
|
|
353
|
+
(network === 'testnet' || network === 'mainnet' || network === 'devnet') &&
|
|
354
|
+
/PublishErrorNonZeroAddress/i.test(String(error?.message))
|
|
355
|
+
) {
|
|
356
|
+
console.error(
|
|
357
|
+
chalk.yellow(
|
|
358
|
+
' Tip: This package may already be published on this network. Use --force to clear the stored address and publish as new, or use "dubhe upgrade" to update the existing package.'
|
|
359
|
+
)
|
|
360
|
+
);
|
|
349
361
|
}
|
|
350
|
-
|
|
351
|
-
process.removeListener('SIGINT', handleInterrupt);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (isInterrupted) {
|
|
355
|
-
process.exit(0);
|
|
362
|
+
throw new Error(`Contract publication failed: ${error.message}`);
|
|
356
363
|
}
|
|
357
364
|
|
|
358
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
359
|
-
|
|
360
365
|
if (!result || result.effects?.status.status === 'failure') {
|
|
361
|
-
|
|
362
|
-
process.exit(1);
|
|
366
|
+
throw new Error('Contract publication transaction failed');
|
|
363
367
|
}
|
|
364
368
|
|
|
365
369
|
console.log(' ├─ Processing publication results...');
|
|
366
370
|
let version = 1;
|
|
367
371
|
let packageId = '';
|
|
368
|
-
let
|
|
369
|
-
let
|
|
372
|
+
let dappHub = '';
|
|
373
|
+
let components = dubheConfig.components;
|
|
374
|
+
let resources = dubheConfig.resources;
|
|
375
|
+
let enums = dubheConfig.enums;
|
|
370
376
|
let upgradeCapId = '';
|
|
377
|
+
let startCheckpoint = '';
|
|
371
378
|
|
|
372
379
|
let printObjects: any[] = [];
|
|
373
380
|
|
|
@@ -384,6 +391,13 @@ async function publishContract(
|
|
|
384
391
|
console.log(` ├─ Upgrade Cap: ${object.objectId}`);
|
|
385
392
|
upgradeCapId = object.objectId || '';
|
|
386
393
|
}
|
|
394
|
+
if (
|
|
395
|
+
object.type === 'created' &&
|
|
396
|
+
object.objectType &&
|
|
397
|
+
object.objectType.includes('dapp_service::DappHub')
|
|
398
|
+
) {
|
|
399
|
+
dappHub = object.objectId || '';
|
|
400
|
+
}
|
|
387
401
|
if (object.type === 'created') {
|
|
388
402
|
printObjects.push(object);
|
|
389
403
|
}
|
|
@@ -392,19 +406,20 @@ async function publishContract(
|
|
|
392
406
|
console.log(` └─ Transaction: ${result.digest}`);
|
|
393
407
|
|
|
394
408
|
updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
|
|
409
|
+
updatePublishedToml(projectPath, network, chainId, packageId, packageId, 1);
|
|
395
410
|
|
|
396
411
|
console.log('\n⚡ Executing Deploy Hook...');
|
|
397
412
|
await delay(5000);
|
|
398
413
|
|
|
414
|
+
startCheckpoint = await dubhe.suiInteractor.currentClient.getLatestCheckpointSequenceNumber();
|
|
415
|
+
|
|
399
416
|
const deployHookTx = new Transaction();
|
|
400
417
|
let args = [];
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
args.push(deployHookTx.object(dubheSchemaId));
|
|
404
|
-
}
|
|
418
|
+
let dubheDappHub = dubheConfig.name === 'dubhe' ? dappHub : await getDubheDappHub(network);
|
|
419
|
+
args.push(deployHookTx.object(dubheDappHub));
|
|
405
420
|
args.push(deployHookTx.object('0x6'));
|
|
406
421
|
deployHookTx.moveCall({
|
|
407
|
-
target: `${packageId}
|
|
422
|
+
target: `${packageId}::genesis::run`,
|
|
408
423
|
arguments: args
|
|
409
424
|
});
|
|
410
425
|
|
|
@@ -414,7 +429,7 @@ async function publishContract(
|
|
|
414
429
|
} catch (error: any) {
|
|
415
430
|
console.error(chalk.red(' └─ Deploy hook execution failed'));
|
|
416
431
|
console.error(error.message);
|
|
417
|
-
|
|
432
|
+
throw new Error(`genesis::run failed: ${error.message}`);
|
|
418
433
|
}
|
|
419
434
|
|
|
420
435
|
if (deployHookResult.effects?.status.status === 'success') {
|
|
@@ -422,45 +437,38 @@ async function publishContract(
|
|
|
422
437
|
console.log(` ├─ Transaction: ${deployHookResult.digest}`);
|
|
423
438
|
|
|
424
439
|
console.log('\n📋 Created Objects:');
|
|
425
|
-
deployHookResult.objectChanges?.map((object: ObjectChange) => {
|
|
426
|
-
if (
|
|
427
|
-
object.type === 'created' &&
|
|
428
|
-
object.objectType &&
|
|
429
|
-
object.objectType.includes('schema::Schema')
|
|
430
|
-
) {
|
|
431
|
-
schemaId = object.objectId || '';
|
|
432
|
-
}
|
|
433
|
-
if (
|
|
434
|
-
object.type === 'created' &&
|
|
435
|
-
object.objectType &&
|
|
436
|
-
object.objectType.includes('schema') &&
|
|
437
|
-
!object.objectType.includes('dynamic_field')
|
|
438
|
-
) {
|
|
439
|
-
printObjects.push(object);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
|
|
443
440
|
printObjects.map((object: ObjectChange) => {
|
|
444
|
-
console.log(` ├─
|
|
445
|
-
console.log(` └─
|
|
441
|
+
console.log(` ├─ ID: ${object.objectId}`);
|
|
442
|
+
console.log(` └─ Type: ${object.objectType}`);
|
|
446
443
|
});
|
|
447
444
|
|
|
448
|
-
saveContractData(
|
|
445
|
+
await saveContractData(
|
|
449
446
|
dubheConfig.name,
|
|
450
447
|
network,
|
|
448
|
+
startCheckpoint,
|
|
451
449
|
packageId,
|
|
452
|
-
|
|
450
|
+
dubheDappHub,
|
|
453
451
|
upgradeCapId,
|
|
454
452
|
version,
|
|
455
|
-
|
|
453
|
+
components,
|
|
454
|
+
resources,
|
|
455
|
+
enums
|
|
456
456
|
);
|
|
457
|
+
|
|
458
|
+
await saveMetadata(dubheConfig.name, network, packageId);
|
|
459
|
+
|
|
460
|
+
// Insert package id to dubhe config
|
|
461
|
+
let config = JSON.parse(fs.readFileSync(`${process.cwd()}/dubhe.config.json`, 'utf-8'));
|
|
462
|
+
config.original_package_id = packageId;
|
|
463
|
+
config.dubhe_object_id = dubheDappHub;
|
|
464
|
+
config.original_dubhe_package_id = await getOriginalDubhePackageId(network);
|
|
465
|
+
config.start_checkpoint = startCheckpoint;
|
|
466
|
+
|
|
467
|
+
fs.writeFileSync(`${process.cwd()}/dubhe.config.json`, JSON.stringify(config, null, 2));
|
|
468
|
+
|
|
457
469
|
console.log('\n✅ Contract Publication Complete\n');
|
|
458
470
|
} else {
|
|
459
|
-
|
|
460
|
-
console.log(chalk.yellow(' Please republish or manually call deploy_hook::run'));
|
|
461
|
-
console.log(chalk.yellow(' Please check the transaction digest:'));
|
|
462
|
-
console.log(chalk.yellow(` ${deployHookResult.digest}`));
|
|
463
|
-
process.exit(1);
|
|
471
|
+
throw new Error(`genesis::run transaction failed. Digest: ${deployHookResult.digest}`);
|
|
464
472
|
}
|
|
465
473
|
}
|
|
466
474
|
|
|
@@ -469,15 +477,13 @@ async function checkDubheFramework(projectPath: string): Promise<boolean> {
|
|
|
469
477
|
console.log(chalk.yellow('\nℹ️ Dubhe Framework Files Not Found'));
|
|
470
478
|
console.log(chalk.yellow(' ├─ Expected Path:'), projectPath);
|
|
471
479
|
console.log(chalk.yellow(' ├─ To set up Dubhe Framework:'));
|
|
472
|
-
console.log(chalk.yellow(' │ 1. Create directory: mkdir -p contracts/dubhe
|
|
480
|
+
console.log(chalk.yellow(' │ 1. Create directory: mkdir -p contracts/dubhe'));
|
|
473
481
|
console.log(
|
|
474
482
|
chalk.yellow(
|
|
475
|
-
' │ 2. Clone repository: git clone https://github.com/0xobelisk/dubhe
|
|
483
|
+
' │ 2. Clone repository: git clone https://github.com/0xobelisk/dubhe contracts/dubhe'
|
|
476
484
|
)
|
|
477
485
|
);
|
|
478
|
-
console.log(
|
|
479
|
-
chalk.yellow(' │ 3. Or download from: https://github.com/0xobelisk/dubhe-framework')
|
|
480
|
-
);
|
|
486
|
+
console.log(chalk.yellow(' │ 3. Or download from: https://github.com/0xobelisk/dubhe'));
|
|
481
487
|
console.log(chalk.yellow(' └─ After setup, restart the local node'));
|
|
482
488
|
return false;
|
|
483
489
|
}
|
|
@@ -488,8 +494,8 @@ export async function publishDubheFramework(
|
|
|
488
494
|
dubhe: Dubhe,
|
|
489
495
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet'
|
|
490
496
|
) {
|
|
491
|
-
const
|
|
492
|
-
const projectPath = `${
|
|
497
|
+
const cwd = process.cwd();
|
|
498
|
+
const projectPath = `${cwd}/src/dubhe`;
|
|
493
499
|
|
|
494
500
|
if (!(await checkDubheFramework(projectPath))) {
|
|
495
501
|
return;
|
|
@@ -501,66 +507,74 @@ export async function publishDubheFramework(
|
|
|
501
507
|
const chainId = await waitForNode(dubhe);
|
|
502
508
|
|
|
503
509
|
await removeEnvContent(`${projectPath}/Move.lock`, network);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
510
|
+
if (network === 'localnet') {
|
|
511
|
+
// When building with --build-env testnet, Sui CLI reads Move.lock's [env.testnet] section
|
|
512
|
+
// and bakes its original-published-id (non-zero for a previously published dubhe) into the
|
|
513
|
+
// bytecode as the package self-address. Publishing then fails with PublishErrorNonZeroAddress
|
|
514
|
+
// because Sui requires the self-address to be 0x0 for a first-time publish.
|
|
515
|
+
// Fix: clear the testnet env section before building so the CLI uses 0x0 from Move.toml.
|
|
516
|
+
await removeEnvContent(`${projectPath}/Move.lock`, 'testnet');
|
|
517
|
+
}
|
|
518
|
+
await updateMoveTomlAddress(projectPath, '0x0');
|
|
519
|
+
|
|
520
|
+
const startCheckpoint =
|
|
521
|
+
await dubhe.suiInteractor.currentClient.getLatestCheckpointSequenceNumber();
|
|
522
|
+
|
|
523
|
+
// For localnet: --build-env testnet is used to resolve git dependencies, but the
|
|
524
|
+
// Move CLI will also read Published.toml and use any existing testnet address for
|
|
525
|
+
// dubhe — causing PublishErrorNonZeroAddress if a testnet entry already exists.
|
|
526
|
+
// Fix: temporarily remove Published.toml before the build, then restore it.
|
|
527
|
+
// This ensures the dubhe package compiles with address 0x0 (from Move.toml).
|
|
528
|
+
const publishedTomlPath = `${projectPath}/Published.toml`;
|
|
529
|
+
let savedPublishedTomlContent: string | null = null;
|
|
530
|
+
if (network === 'localnet' && fs.existsSync(publishedTomlPath)) {
|
|
531
|
+
savedPublishedTomlContent = fs.readFileSync(publishedTomlPath, 'utf-8');
|
|
532
|
+
fs.unlinkSync(publishedTomlPath);
|
|
533
|
+
}
|
|
508
534
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
isInterrupted = true;
|
|
517
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
518
|
-
console.log('\n └─ Operation cancelled by user');
|
|
519
|
-
process.exit(0);
|
|
520
|
-
};
|
|
521
|
-
process.on('SIGINT', handleInterrupt);
|
|
535
|
+
// Sui CLI 1.40+ checks that the active environment is declared in Move.toml
|
|
536
|
+
// even when --build-env is specified. Temporarily inject localnet into [environments].
|
|
537
|
+
const moveTomlPath = `${projectPath}/Move.toml`;
|
|
538
|
+
let savedMoveTomlContent: string | null = null;
|
|
539
|
+
if (network === 'localnet') {
|
|
540
|
+
savedMoveTomlContent = patchMoveTomlWithLocalnetEnv(moveTomlPath, chainId);
|
|
541
|
+
}
|
|
522
542
|
|
|
543
|
+
let modules: any, dependencies: any;
|
|
523
544
|
try {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
} catch (error) {
|
|
528
|
-
if (isInterrupted) break;
|
|
529
|
-
|
|
530
|
-
retryCount++;
|
|
531
|
-
if (retryCount === MAX_RETRIES) {
|
|
532
|
-
console.log(chalk.red(` └─ Publication failed after ${MAX_RETRIES} attempts`));
|
|
533
|
-
console.log(chalk.red(' └─ Please check your network connection and try again later.'));
|
|
534
|
-
process.exit(1);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
|
|
538
|
-
const spinner = SPINNER[spinnerIndex % SPINNER.length];
|
|
539
|
-
spinnerIndex++;
|
|
540
|
-
|
|
541
|
-
process.stdout.write(`\r ├─ ${spinner} Retrying... (${elapsedTime}s)`);
|
|
542
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
+
// For localnet: use --build-env testnet (no pubfile needed — dubhe has no local deps).
|
|
546
|
+
// For testnet/mainnet: use -e <network> as usual.
|
|
547
|
+
[modules, dependencies] = buildContract(projectPath, network);
|
|
545
548
|
} finally {
|
|
546
|
-
|
|
549
|
+
// Always restore Published.toml and Move.toml (successful build or error)
|
|
550
|
+
if (savedPublishedTomlContent !== null) {
|
|
551
|
+
fs.writeFileSync(publishedTomlPath, savedPublishedTomlContent, 'utf-8');
|
|
552
|
+
}
|
|
553
|
+
if (savedMoveTomlContent !== null) {
|
|
554
|
+
fs.writeFileSync(moveTomlPath, savedMoveTomlContent, 'utf-8');
|
|
555
|
+
}
|
|
547
556
|
}
|
|
548
557
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
558
|
+
const tx = new Transaction();
|
|
559
|
+
const [upgradeCap] = tx.publish({ modules, dependencies });
|
|
560
|
+
tx.transferObjects([upgradeCap], dubhe.getAddress());
|
|
552
561
|
|
|
553
|
-
|
|
562
|
+
let result;
|
|
563
|
+
try {
|
|
564
|
+
result = await dubhe.signAndSendTxn({ tx });
|
|
565
|
+
} catch (error: any) {
|
|
566
|
+
console.error(chalk.red(' └─ Publication failed'));
|
|
567
|
+
console.error(error.message);
|
|
568
|
+
throw new Error(`Dubhe framework publication failed: ${error.message}`);
|
|
569
|
+
}
|
|
554
570
|
|
|
555
571
|
if (!result || result.effects?.status.status === 'failure') {
|
|
556
|
-
|
|
557
|
-
process.exit(1);
|
|
572
|
+
throw new Error('Dubhe framework publication transaction failed');
|
|
558
573
|
}
|
|
559
574
|
|
|
560
575
|
let version = 1;
|
|
561
576
|
let packageId = '';
|
|
562
|
-
let
|
|
563
|
-
let schemas: Record<string, string> = {};
|
|
577
|
+
let dappHub = '';
|
|
564
578
|
let upgradeCapId = '';
|
|
565
579
|
|
|
566
580
|
result.objectChanges!.map((object: ObjectChange) => {
|
|
@@ -574,14 +588,20 @@ export async function publishDubheFramework(
|
|
|
574
588
|
) {
|
|
575
589
|
upgradeCapId = object.objectId || '';
|
|
576
590
|
}
|
|
591
|
+
if (
|
|
592
|
+
object.type === 'created' &&
|
|
593
|
+
object.objectType &&
|
|
594
|
+
object.objectType.includes('dapp_service::DappHub')
|
|
595
|
+
) {
|
|
596
|
+
dappHub = object.objectId || '';
|
|
597
|
+
}
|
|
577
598
|
});
|
|
578
599
|
|
|
579
600
|
await delay(3000);
|
|
580
|
-
|
|
581
601
|
const deployHookTx = new Transaction();
|
|
582
602
|
deployHookTx.moveCall({
|
|
583
|
-
target: `${packageId}::
|
|
584
|
-
arguments: [deployHookTx.object('0x6')]
|
|
603
|
+
target: `${packageId}::genesis::run`,
|
|
604
|
+
arguments: [deployHookTx.object(dappHub), deployHookTx.object('0x6')]
|
|
585
605
|
});
|
|
586
606
|
|
|
587
607
|
let deployHookResult;
|
|
@@ -590,30 +610,47 @@ export async function publishDubheFramework(
|
|
|
590
610
|
} catch (error: any) {
|
|
591
611
|
console.error(chalk.red(' └─ Deploy hook execution failed'));
|
|
592
612
|
console.error(error.message);
|
|
593
|
-
|
|
613
|
+
throw new Error(`Dubhe genesis::run failed: ${error.message}`);
|
|
594
614
|
}
|
|
595
615
|
|
|
596
|
-
if (deployHookResult.effects?.status.status
|
|
597
|
-
|
|
598
|
-
if (
|
|
599
|
-
object.type === 'created' &&
|
|
600
|
-
object.objectType &&
|
|
601
|
-
object.objectType.includes('dubhe_schema::Schema')
|
|
602
|
-
) {
|
|
603
|
-
schemaId = object.objectId || '';
|
|
604
|
-
}
|
|
605
|
-
});
|
|
616
|
+
if (deployHookResult.effects?.status.status !== 'success') {
|
|
617
|
+
throw new Error('Deploy hook execution failed');
|
|
606
618
|
}
|
|
607
619
|
|
|
608
|
-
|
|
620
|
+
await updateMoveTomlAddress(projectPath, packageId);
|
|
621
|
+
await saveContractData(
|
|
622
|
+
'dubhe',
|
|
623
|
+
network,
|
|
624
|
+
startCheckpoint,
|
|
625
|
+
packageId,
|
|
626
|
+
dappHub,
|
|
627
|
+
upgradeCapId,
|
|
628
|
+
version,
|
|
629
|
+
{},
|
|
630
|
+
{},
|
|
631
|
+
{}
|
|
632
|
+
);
|
|
609
633
|
|
|
610
634
|
updateEnvFile(`${projectPath}/Move.lock`, network, 'publish', chainId, packageId);
|
|
611
|
-
|
|
635
|
+
updatePublishedToml(projectPath, network, chainId, packageId, packageId, 1);
|
|
636
|
+
|
|
637
|
+
// For localnet: write dubhe's published address to Pub.localnet.toml so that
|
|
638
|
+
// the counter package build (next step) can resolve the dubhe dependency.
|
|
639
|
+
if (network === 'localnet') {
|
|
640
|
+
const pubfilePath = getEphemeralPubFilePath(cwd, network);
|
|
641
|
+
updateEphemeralPubFile(pubfilePath, chainId, 'testnet', {
|
|
642
|
+
source: projectPath,
|
|
643
|
+
publishedAt: packageId,
|
|
644
|
+
originalId: packageId,
|
|
645
|
+
upgradeCap: upgradeCapId
|
|
646
|
+
});
|
|
647
|
+
}
|
|
612
648
|
}
|
|
613
649
|
|
|
614
650
|
export async function publishHandler(
|
|
615
651
|
dubheConfig: DubheConfig,
|
|
616
652
|
network: 'mainnet' | 'testnet' | 'devnet' | 'localnet',
|
|
653
|
+
force: boolean,
|
|
617
654
|
gasBudget?: number
|
|
618
655
|
) {
|
|
619
656
|
await switchEnv(network);
|
|
@@ -622,12 +659,12 @@ export async function publishHandler(
|
|
|
622
659
|
network
|
|
623
660
|
});
|
|
624
661
|
|
|
625
|
-
|
|
662
|
+
const path = process.cwd();
|
|
663
|
+
const projectPath = `${path}/src/${dubheConfig.name}`;
|
|
664
|
+
|
|
665
|
+
if (network === 'localnet' && dubheConfig.name !== 'dubhe') {
|
|
626
666
|
await publishDubheFramework(dubhe, network);
|
|
627
667
|
}
|
|
628
668
|
|
|
629
|
-
|
|
630
|
-
const projectPath = `${path}/contracts/${dubheConfig.name}`;
|
|
631
|
-
await updateDubheDependency(`${projectPath}/Move.toml`, network);
|
|
632
|
-
await publishContract(dubhe, dubheConfig, network, projectPath, gasBudget);
|
|
669
|
+
await publishContract(dubhe, dubheConfig, network, projectPath, gasBudget, force);
|
|
633
670
|
}
|