@twin.org/move-to-json 0.0.2-next.6 → 0.0.2-next.8
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/dist/cjs/index.cjs +412 -143
- package/dist/esm/index.mjs +416 -147
- package/dist/locales/en.json +54 -5
- package/dist/types/commands/build.d.ts +1 -1
- package/dist/types/commands/deploy.d.ts +18 -1
- package/dist/types/utils/envSetup.d.ts +6 -3
- package/dist/types/utils/environmentUtils.d.ts +43 -0
- package/docs/changelog.md +29 -0
- package/docs/reference/functions/actionCommandBuild.md +1 -1
- package/docs/reference/functions/actionCommandDeploy.md +48 -0
- package/docs/reference/functions/ensureCorrectDeployerKey.md +7 -1
- package/docs/reference/functions/getDeploymentMnemonic.md +7 -1
- package/docs/reference/functions/getDeploymentSeed.md +7 -1
- package/docs/reference/functions/validateDeploymentEnvironment.md +7 -1
- package/locales/en.json +54 -5
- package/package.json +3 -4
package/dist/cjs/index.cjs
CHANGED
|
@@ -67,33 +67,31 @@ function buildCommandBuild(program) {
|
|
|
67
67
|
.command("build")
|
|
68
68
|
.description(core.I18n.formatMessage("commands.build.description"))
|
|
69
69
|
.argument("<inputGlob>", core.I18n.formatMessage("commands.build.options.inputGlob.description"))
|
|
70
|
-
.option(core.I18n.formatMessage("commands.build.options.network.param"), core.I18n.formatMessage("commands.build.options.network.description"))
|
|
70
|
+
.option(core.I18n.formatMessage("commands.build.options.network.param"), core.I18n.formatMessage("commands.build.options.network.description"), "!NETWORK")
|
|
71
71
|
.option(core.I18n.formatMessage("commands.build.options.output.param"), core.I18n.formatMessage("commands.build.options.output.description"), "smart-contract-deployments.json")
|
|
72
|
-
.action(
|
|
73
|
-
await actionCommandBuild(inputGlob, opts);
|
|
74
|
-
});
|
|
72
|
+
.action(actionCommandBuild);
|
|
75
73
|
}
|
|
76
74
|
/**
|
|
77
75
|
* Action for the build command.
|
|
78
76
|
* @param inputGlob A glob pattern that matches one or more Move files
|
|
79
77
|
* @param opts Additional options.
|
|
80
|
-
* @param opts.network Target network (testnet/devnet/mainnet)
|
|
78
|
+
* @param opts.network Target network (testnet/devnet/mainnet).
|
|
81
79
|
* @param opts.output Where we store the final compiled modules.
|
|
82
80
|
*/
|
|
83
81
|
async function actionCommandBuild(inputGlob, opts) {
|
|
84
82
|
try {
|
|
85
|
-
const
|
|
83
|
+
const networkRaw = cliCore.CLIParam.stringValue("network", opts.network);
|
|
84
|
+
const network = networkRaw;
|
|
86
85
|
core.Guards.arrayOneOf("commands", "network", network, Object.values(dltIota.NetworkTypes));
|
|
87
86
|
// Verify the IOTA SDK before we do anything else
|
|
88
87
|
await verifyIotaSDK();
|
|
89
88
|
const { normalizedGlob, normalizedOutput, executionDir } = normalizePathsAndWorkingDir(inputGlob, opts.output ?? "smart-contract-deployments.json");
|
|
90
89
|
cliCore.CLIDisplay.section(core.I18n.formatMessage("commands.build.section.buildingMoveContracts", {
|
|
91
|
-
network
|
|
90
|
+
network
|
|
92
91
|
}));
|
|
93
92
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.inputGlob"), inputGlob);
|
|
94
93
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.outputJson"), normalizedOutput);
|
|
95
94
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.network"), network);
|
|
96
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.platform"), "iota");
|
|
97
95
|
cliCore.CLIDisplay.break();
|
|
98
96
|
// Find matching .move files
|
|
99
97
|
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.build.progress.searchingFiles"));
|
|
@@ -121,7 +119,6 @@ async function actionCommandBuild(inputGlob, opts) {
|
|
|
121
119
|
}
|
|
122
120
|
const existingJson = await cliCore.CLIUtils.readJsonFile(normalizedOutput);
|
|
123
121
|
const finalJson = existingJson ?? {};
|
|
124
|
-
finalJson[network] ??= {};
|
|
125
122
|
if (existingJson) {
|
|
126
123
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.mergingWithExistingJson"), normalizedOutput);
|
|
127
124
|
}
|
|
@@ -134,10 +131,16 @@ async function actionCommandBuild(inputGlob, opts) {
|
|
|
134
131
|
try {
|
|
135
132
|
const compiled = await processMoveFile(moveFile);
|
|
136
133
|
if (compiled) {
|
|
137
|
-
const { contractName, packageId,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
const { contractName, packageId, packageBytecode } = compiled;
|
|
135
|
+
// Capture the last package id before overwriting it
|
|
136
|
+
const lastPackageId = finalJson[network]?.packageId;
|
|
137
|
+
finalJson[network] ??= { packageId, packageBytecode };
|
|
138
|
+
// If the last package id is different we need to clear
|
|
139
|
+
// the deployed package id, otherwise calling deploy will
|
|
140
|
+
// not do anything as it thinks the package is already deployed.
|
|
141
|
+
if (lastPackageId !== packageId) {
|
|
142
|
+
delete finalJson[network].deployedPackageId;
|
|
143
|
+
}
|
|
141
144
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.updatedNetworkPackage", { network }), contractName, 2);
|
|
142
145
|
}
|
|
143
146
|
}
|
|
@@ -153,12 +156,13 @@ async function actionCommandBuild(inputGlob, opts) {
|
|
|
153
156
|
}
|
|
154
157
|
catch (err) {
|
|
155
158
|
cliCore.CLIDisplay.error(err);
|
|
159
|
+
throw err;
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
/**
|
|
159
163
|
* Process a single Move file by compiling it, computing the packageId, and base64-encoding the .mv modules.
|
|
160
164
|
* @param moveFile The path to a single Move source file.
|
|
161
|
-
* @returns The compiled results or
|
|
165
|
+
* @returns The compiled results or undefined if no modules found.
|
|
162
166
|
*/
|
|
163
167
|
async function processMoveFile(moveFile) {
|
|
164
168
|
// The contract name is based on the .move file's base name in kebab-case
|
|
@@ -167,7 +171,6 @@ async function processMoveFile(moveFile) {
|
|
|
167
171
|
// Find the "project root" (the directory containing Move.toml).
|
|
168
172
|
const projectRoot = getProjectRoot(moveFile);
|
|
169
173
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.contractName"), contractName, 1);
|
|
170
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.platform"), "iota", 1);
|
|
171
174
|
// Compile the contract
|
|
172
175
|
try {
|
|
173
176
|
const cliArgs = ["move", "build"];
|
|
@@ -177,7 +180,7 @@ async function processMoveFile(moveFile) {
|
|
|
177
180
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.labels.compileResult"), core.I18n.formatMessage("commands.build.labels.buildCompleted"), 1);
|
|
178
181
|
}
|
|
179
182
|
catch (error) {
|
|
180
|
-
throw new core.GeneralError("commands", "commands.build.buildFailed", {
|
|
183
|
+
throw new core.GeneralError("commands", "commands.build.buildFailed", { file: moveFile }, error);
|
|
181
184
|
}
|
|
182
185
|
// Get the bytecode modules
|
|
183
186
|
const buildFolderName = core.StringHelper.snakeCase(baseName);
|
|
@@ -187,13 +190,13 @@ async function processMoveFile(moveFile) {
|
|
|
187
190
|
}
|
|
188
191
|
catch {
|
|
189
192
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.warnings.noBytecodeModulesFolder", { contractName }), "", 2);
|
|
190
|
-
return
|
|
193
|
+
return;
|
|
191
194
|
}
|
|
192
195
|
const moduleFiles = await node_fs.promises.readdir(bytecodeModulesPath);
|
|
193
196
|
const mvFiles = moduleFiles.filter(f => f.endsWith(".mv"));
|
|
194
197
|
if (mvFiles.length === 0) {
|
|
195
198
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.build.warnings.noMvFilesFound", { contractName }), "", 2);
|
|
196
|
-
return
|
|
199
|
+
return;
|
|
197
200
|
}
|
|
198
201
|
// Compute the package ID
|
|
199
202
|
const modulesBytesForHash = [];
|
|
@@ -213,7 +216,7 @@ async function processMoveFile(moveFile) {
|
|
|
213
216
|
return {
|
|
214
217
|
contractName,
|
|
215
218
|
packageId: computedPackageId,
|
|
216
|
-
packageData
|
|
219
|
+
packageBytecode: packageData
|
|
217
220
|
};
|
|
218
221
|
}
|
|
219
222
|
/**
|
|
@@ -242,66 +245,267 @@ function normalizePathsAndWorkingDir(inputGlob, outputJson) {
|
|
|
242
245
|
};
|
|
243
246
|
}
|
|
244
247
|
|
|
248
|
+
// Copyright 2024 IOTA Stiftung.
|
|
249
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
250
|
+
/**
|
|
251
|
+
* Executes a command with a timeout.
|
|
252
|
+
* @param command The command to execute.
|
|
253
|
+
* @param timeoutMs The timeout in milliseconds.
|
|
254
|
+
* @returns The stdout and stderr of the command
|
|
255
|
+
*/
|
|
256
|
+
async function execAsyncWithTimeout(command, timeoutMs) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const timeoutId = setTimeout(() => {
|
|
259
|
+
reject(new core.GeneralError("environmentUtils", "error.environmentUtils.commandTimeout", {
|
|
260
|
+
command,
|
|
261
|
+
timeout: timeoutMs
|
|
262
|
+
}));
|
|
263
|
+
}, timeoutMs);
|
|
264
|
+
node_child_process.exec(command, { timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
265
|
+
clearTimeout(timeoutId);
|
|
266
|
+
if (error) {
|
|
267
|
+
reject(error);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
resolve({ stdout, stderr });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Execute a command with input.
|
|
277
|
+
* @param command The command to execute.
|
|
278
|
+
* @param inputs The inputs to provide to the command.
|
|
279
|
+
* @returns A promise that resolves when the command completes.
|
|
280
|
+
*/
|
|
281
|
+
async function execWithInput(command, inputs) {
|
|
282
|
+
const child = node_child_process.spawn(command, { shell: true, stdio: ["pipe", "inherit", "inherit"] });
|
|
283
|
+
for (const input of inputs) {
|
|
284
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
285
|
+
child.stdin.write(`${input}\n`);
|
|
286
|
+
}
|
|
287
|
+
child.stdin.end();
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const timeout = setTimeout(() => {
|
|
290
|
+
child.kill("SIGTERM");
|
|
291
|
+
reject(new core.GeneralError("environmentUtils", "error.environmentUtils.commandTimeout", {
|
|
292
|
+
command,
|
|
293
|
+
inputs,
|
|
294
|
+
timeout: 5000
|
|
295
|
+
}));
|
|
296
|
+
}, 5000);
|
|
297
|
+
child.on("close", code => {
|
|
298
|
+
clearTimeout(timeout);
|
|
299
|
+
if (code === 0) {
|
|
300
|
+
resolve();
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
reject(new core.GeneralError("environmentUtils", "error.environmentUtils.commandFailedWithCode", {
|
|
304
|
+
command,
|
|
305
|
+
inputs,
|
|
306
|
+
code
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
child.on("error", error => {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
reject(new core.GeneralError("environmentUtils", "error.environmentUtils.commandExecutionFailed", {
|
|
313
|
+
command,
|
|
314
|
+
inputs,
|
|
315
|
+
error
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Verify that IOTA CLI is installed and available.
|
|
322
|
+
* @throws GeneralError if IOTA CLI is not installed or version check fails.
|
|
323
|
+
*/
|
|
324
|
+
async function verifyIotaCliInstalled() {
|
|
325
|
+
try {
|
|
326
|
+
await execAsyncWithTimeout("iota --version", 5000);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error.code === 127 ||
|
|
330
|
+
error.code === "ENOENT") {
|
|
331
|
+
throw new core.GeneralError("environmentUtils", "error.environmentUtils.iotaCliNotFound", undefined, error);
|
|
332
|
+
}
|
|
333
|
+
throw new core.GeneralError("environmentUtils", "error.environmentUtils.iotaCliVerificationFailed", undefined, error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Check if a specific IOTA environment exists.
|
|
338
|
+
* @param network The network alias to check for.
|
|
339
|
+
* @returns Promise<boolean> True if environment exists, false otherwise.
|
|
340
|
+
*/
|
|
341
|
+
async function checkEnvironmentExists(network) {
|
|
342
|
+
try {
|
|
343
|
+
const { stdout } = await execAsyncWithTimeout("iota client envs --json", 5000);
|
|
344
|
+
const envData = JSON.parse(stdout);
|
|
345
|
+
// The IOTA CLI returns [environments[], currentEnv] format
|
|
346
|
+
const environments = core.Is.arrayValue(envData) ? envData[0] : envData;
|
|
347
|
+
if (core.Is.arrayValue(environments)) {
|
|
348
|
+
return environments.some(env => env.alias === network);
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Silently return false if environment check fails - let calling code handle messaging
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Create a new IOTA environment.
|
|
359
|
+
* @param network The network alias to create.
|
|
360
|
+
* @param rpcUrl The RPC URL for the network.
|
|
361
|
+
* @returns Promise<void>
|
|
362
|
+
*/
|
|
363
|
+
async function createEnvironment(network, rpcUrl) {
|
|
364
|
+
const isWindows = process.platform === "win32";
|
|
365
|
+
try {
|
|
366
|
+
const additionalInfo = isWindows ? "" : " || true";
|
|
367
|
+
await execWithInput(`iota client new-env --alias ${network} --rpc ${rpcUrl}${additionalInfo}`, [
|
|
368
|
+
"0"
|
|
369
|
+
]);
|
|
370
|
+
await execAsyncWithTimeout(`iota client switch --env ${network}${additionalInfo}`, 5000);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
throw new core.GeneralError("environmentUtils", "environmentCreationFailed", { network, rpcUrl }, error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create default IOTA environments when none exist.
|
|
378
|
+
* @returns Promise<void>
|
|
379
|
+
*/
|
|
380
|
+
async function createDefaultEnvironments() {
|
|
381
|
+
const defaultEnvironments = [
|
|
382
|
+
{ alias: "mainnet", rpc: "https://api.mainnet.iota.cafe" },
|
|
383
|
+
{ alias: "devnet", rpc: "https://api.devnet.iota.cafe" },
|
|
384
|
+
{ alias: "testnet", rpc: "https://api.testnet.iota.cafe" },
|
|
385
|
+
{ alias: "localnet", rpc: "http://127.0.0.1:9000" }
|
|
386
|
+
];
|
|
387
|
+
cliCore.CLIDisplay.value(core.I18n.formatMessage("info.environmentUtils.creatingDefaultEnvironments"), `${defaultEnvironments.length} environments`, 1);
|
|
388
|
+
for (const env of defaultEnvironments) {
|
|
389
|
+
try {
|
|
390
|
+
await createEnvironment(env.alias, env.rpc);
|
|
391
|
+
cliCore.CLIDisplay.value(core.I18n.formatMessage("info.environmentUtils.createdEnvironment"), env.alias, 2);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Continue creating other environments even if one fails
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Set testnet as active by default
|
|
398
|
+
const isWindows = process.platform === "win32";
|
|
399
|
+
const additionalInfo = isWindows ? "" : " || true";
|
|
400
|
+
try {
|
|
401
|
+
await execAsyncWithTimeout(`iota client switch --env testnet${additionalInfo}`, 5000);
|
|
402
|
+
cliCore.CLIDisplay.value(core.I18n.formatMessage("info.environmentUtils.activeEnvironment"), "testnet", 1);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Ignore switch failures - testnet might not exist yet
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if any IOTA environments exist by attempting to list them.
|
|
410
|
+
* If the command times out, it likely means no environments are configured.
|
|
411
|
+
* @returns Promise<boolean> True if any environments exist, false otherwise.
|
|
412
|
+
*/
|
|
413
|
+
async function checkAnyEnvironmentsExist() {
|
|
414
|
+
try {
|
|
415
|
+
const { stdout } = await execAsyncWithTimeout("iota client envs --json", 5000);
|
|
416
|
+
const envData = JSON.parse(stdout);
|
|
417
|
+
const environments = core.Is.arrayValue(envData) ? envData[0] : envData;
|
|
418
|
+
return core.Is.arrayValue(environments);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// Timeout or error likely means no environments exist
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Ensure that a specific IOTA environment exists, creating it if necessary.
|
|
427
|
+
* If no environments exist at all, creates default environments first.
|
|
428
|
+
* @param network The network alias to ensure exists.
|
|
429
|
+
* @param rpcUrl The RPC URL for the network.
|
|
430
|
+
* @returns Promise<void>
|
|
431
|
+
*/
|
|
432
|
+
async function ensureEnvironment(network, rpcUrl) {
|
|
433
|
+
// First verify IOTA CLI is installed
|
|
434
|
+
await verifyIotaCliInstalled();
|
|
435
|
+
// Check if any environments exist at all
|
|
436
|
+
const anyEnvironmentsExist = await checkAnyEnvironmentsExist();
|
|
437
|
+
if (!anyEnvironmentsExist) {
|
|
438
|
+
// No environments exist - create defaults
|
|
439
|
+
cliCore.CLIDisplay.value(core.I18n.formatMessage("info.environmentUtils.noEnvironments"), core.I18n.formatMessage("info.environmentUtils.creatingDefaults"), 1);
|
|
440
|
+
await createDefaultEnvironments();
|
|
441
|
+
}
|
|
442
|
+
// Now check if the specific environment exists
|
|
443
|
+
const exists = await checkEnvironmentExists(network);
|
|
444
|
+
if (!exists) {
|
|
445
|
+
// Use CLI framework for user-facing message
|
|
446
|
+
cliCore.CLIDisplay.value(core.I18n.formatMessage("info.environmentUtils.creatingEnvironment"), network, 1);
|
|
447
|
+
await createEnvironment(network, rpcUrl);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Switch to the environment to make sure it's active
|
|
451
|
+
const isWindows = process.platform === "win32";
|
|
452
|
+
const additionalInfo = isWindows ? "" : " || true";
|
|
453
|
+
await execAsyncWithTimeout(`iota client switch --env ${network}${additionalInfo}`, 5000);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
245
457
|
// Copyright 2024 IOTA Stiftung.
|
|
246
458
|
// SPDX-License-Identifier: Apache-2.0.
|
|
247
459
|
/**
|
|
248
460
|
* Validate that required environment variables are set for deployment.
|
|
249
461
|
* @param network The target network.
|
|
462
|
+
* @param deployerMnemonic The deployer mnemonic to validate.
|
|
250
463
|
* @throws GeneralError if required environment variables are missing.
|
|
251
464
|
*/
|
|
252
|
-
async function validateDeploymentEnvironment(network) {
|
|
253
|
-
await getDeploymentMnemonic(network);
|
|
465
|
+
async function validateDeploymentEnvironment(network, deployerMnemonic) {
|
|
466
|
+
await getDeploymentMnemonic(network, deployerMnemonic);
|
|
254
467
|
}
|
|
255
468
|
/**
|
|
256
469
|
* Get the deployment mnemonic for a network.
|
|
257
470
|
* @param network The target network.
|
|
471
|
+
* @param deployerMnemonic The deployer mnemonic from environment variables.
|
|
258
472
|
* @returns The mnemonic string.
|
|
259
473
|
* @throws GeneralError if mnemonic is not found or invalid.
|
|
260
474
|
*/
|
|
261
|
-
async function getDeploymentMnemonic(network) {
|
|
262
|
-
|
|
263
|
-
if (!mnemonic) {
|
|
475
|
+
async function getDeploymentMnemonic(network, deployerMnemonic) {
|
|
476
|
+
if (!core.Is.stringValue(deployerMnemonic)) {
|
|
264
477
|
throw new core.GeneralError("envSetup", "mnemonicMissing", {
|
|
265
478
|
network,
|
|
266
479
|
mnemonicVar: "DEPLOYER_MNEMONIC"
|
|
267
480
|
});
|
|
268
481
|
}
|
|
269
|
-
// Validate mnemonic format
|
|
270
|
-
|
|
271
|
-
if (words.length !== 24) {
|
|
482
|
+
// Validate mnemonic format using Bip39 validation
|
|
483
|
+
if (!crypto.Bip39.validateMnemonic(deployerMnemonic)) {
|
|
272
484
|
throw new core.GeneralError("envSetup", "mnemonicInvalidFormat", {
|
|
273
485
|
network,
|
|
274
|
-
mnemonicVar: "DEPLOYER_MNEMONIC"
|
|
275
|
-
wordCount: words.length
|
|
486
|
+
mnemonicVar: "DEPLOYER_MNEMONIC"
|
|
276
487
|
});
|
|
277
488
|
}
|
|
278
|
-
return
|
|
489
|
+
return deployerMnemonic;
|
|
279
490
|
}
|
|
280
491
|
/**
|
|
281
492
|
* Get the deployment seed for a network (if available).
|
|
282
493
|
* @param network The target network.
|
|
494
|
+
* @param deployerSeed The deployer seed from environment variables (optional).
|
|
283
495
|
* @returns The seed string or undefined if not set.
|
|
284
496
|
* @throws GeneralError if seed is not found or invalid.
|
|
285
497
|
*/
|
|
286
|
-
async function getDeploymentSeed(network) {
|
|
287
|
-
|
|
288
|
-
if (!core.Is.stringValue(seed)) {
|
|
498
|
+
async function getDeploymentSeed(network, deployerSeed) {
|
|
499
|
+
if (!core.Is.stringValue(deployerSeed)) {
|
|
289
500
|
return undefined;
|
|
290
501
|
}
|
|
291
|
-
if (!core.Is.
|
|
502
|
+
if (!core.Is.stringHexLength(deployerSeed, 64, true)) {
|
|
292
503
|
throw new core.GeneralError("envSetup", "seedInvalidFormat", {
|
|
293
504
|
network,
|
|
294
505
|
seedVar: "DEPLOYER_SEED"
|
|
295
506
|
});
|
|
296
507
|
}
|
|
297
|
-
|
|
298
|
-
// 0x + 64 hex characters
|
|
299
|
-
throw new core.GeneralError("envSetup", "seedInvalidFormat", {
|
|
300
|
-
network,
|
|
301
|
-
seedVar: "DEPLOYER_SEED"
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
return seed;
|
|
508
|
+
return deployerSeed;
|
|
305
509
|
}
|
|
306
510
|
|
|
307
511
|
// Copyright 2024 IOTA Stiftung.
|
|
@@ -316,40 +520,134 @@ function buildCommandDeploy(program) {
|
|
|
316
520
|
.command("deploy")
|
|
317
521
|
.description(core.I18n.formatMessage("commands.deploy.description"))
|
|
318
522
|
.option(core.I18n.formatMessage("commands.deploy.options.contracts.param"), core.I18n.formatMessage("commands.deploy.options.contracts.description"), "smart-contract-deployments.json")
|
|
319
|
-
.option(core.I18n.formatMessage("commands.deploy.options.network.param"), core.I18n.formatMessage("commands.deploy.options.network.description"))
|
|
523
|
+
.option(core.I18n.formatMessage("commands.deploy.options.network.param"), core.I18n.formatMessage("commands.deploy.options.network.description"), "!NETWORK")
|
|
320
524
|
.option(core.I18n.formatMessage("commands.deploy.options.dryRun.param"), core.I18n.formatMessage("commands.deploy.options.dryRun.description"))
|
|
321
525
|
.option(core.I18n.formatMessage("commands.deploy.options.force.param"), core.I18n.formatMessage("commands.deploy.options.force.description"))
|
|
526
|
+
.option(core.I18n.formatMessage("commands.deploy.options.rpcUrl.param"), core.I18n.formatMessage("commands.deploy.options.rpcUrl.description"), "!RPC_URL")
|
|
527
|
+
.option(core.I18n.formatMessage("commands.deploy.options.addressIndex.param"), core.I18n.formatMessage("commands.deploy.options.addressIndex.description"), "!ADDRESS_INDEX")
|
|
528
|
+
.option(core.I18n.formatMessage("commands.deploy.options.rpcTimeout.param"), core.I18n.formatMessage("commands.deploy.options.rpcTimeout.description"), "!RPC_TIMEOUT")
|
|
529
|
+
.option(core.I18n.formatMessage("commands.deploy.options.gasBudget.param"), core.I18n.formatMessage("commands.deploy.options.gasBudget.description"), "!GAS_BUDGET")
|
|
530
|
+
.option(core.I18n.formatMessage("commands.deploy.options.confirmationTimeout.param"), core.I18n.formatMessage("commands.deploy.options.confirmationTimeout.description"), "!CONFIRMATION_TIMEOUT")
|
|
531
|
+
.option(core.I18n.formatMessage("commands.deploy.options.faucetUrl.param"), core.I18n.formatMessage("commands.deploy.options.faucetUrl.description"), "!FAUCET_URL")
|
|
532
|
+
.option(core.I18n.formatMessage("commands.deploy.options.deployerMnemonic.param"), core.I18n.formatMessage("commands.deploy.options.deployerMnemonic.description"), "!DEPLOYER_MNEMONIC")
|
|
533
|
+
.option(core.I18n.formatMessage("commands.deploy.options.deployerSeed.param"), core.I18n.formatMessage("commands.deploy.options.deployerSeed.description"), "!DEPLOYER_SEED")
|
|
322
534
|
.action(actionCommandDeploy);
|
|
323
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Action for the deploy command.
|
|
538
|
+
* @param opts Command options.
|
|
539
|
+
* @param opts.contracts Path to compiled modules JSON.
|
|
540
|
+
* @param opts.network Network identifier - optional if NETWORK env var is set.
|
|
541
|
+
* @param opts.dryRun Simulate deployment without executing.
|
|
542
|
+
* @param opts.force Force redeployment of existing packages.
|
|
543
|
+
* @param opts.rpcUrl RPC endpoint URL for the network.
|
|
544
|
+
* @param opts.addressIndex Address index for key derivation.
|
|
545
|
+
* @param opts.rpcTimeout RPC request timeout in milliseconds.
|
|
546
|
+
* @param opts.gasBudget Gas budget for transactions.
|
|
547
|
+
* @param opts.confirmationTimeout Transaction confirmation timeout in milliseconds.
|
|
548
|
+
* @param opts.faucetUrl Faucet URL for requesting test tokens.
|
|
549
|
+
* @param opts.deployerMnemonic Deployer wallet mnemonic phrase.
|
|
550
|
+
* @param opts.deployerSeed Deployer wallet seed (alternative to mnemonic).
|
|
551
|
+
*/
|
|
552
|
+
async function actionCommandDeploy(opts) {
|
|
553
|
+
try {
|
|
554
|
+
const contractsPath = opts.contracts ?? "smart-contract-deployments.json";
|
|
555
|
+
const dryRun = opts.dryRun ?? false;
|
|
556
|
+
const force = opts.force ?? false;
|
|
557
|
+
cliCore.CLIDisplay.section(core.I18n.formatMessage("commands.deploy.section.deployContracts"));
|
|
558
|
+
cliCore.CLIDisplay.section(contractsPath);
|
|
559
|
+
const networkRaw = cliCore.CLIParam.stringValue("network", opts.network);
|
|
560
|
+
const network = networkRaw;
|
|
561
|
+
core.Guards.arrayOneOf("commands", "network", network, Object.values(dltIota.NetworkTypes));
|
|
562
|
+
// Verify the IOTA SDK before we do anything else
|
|
563
|
+
await verifyIotaSDK();
|
|
564
|
+
// Get configuration values needed for environment setup
|
|
565
|
+
const rpcUrl = cliCore.CLIParam.stringValue("rpcUrl", opts.rpcUrl);
|
|
566
|
+
const addressIndex = cliCore.CLIParam.number("addressIndex", opts.addressIndex) ?? 0;
|
|
567
|
+
const rpcTimeout = cliCore.CLIParam.number("rpcTimeout", opts.rpcTimeout);
|
|
568
|
+
const gasBudget = cliCore.CLIParam.number("gasBudget", opts.gasBudget);
|
|
569
|
+
const confirmationTimeout = cliCore.CLIParam.number("confirmationTimeout", opts.confirmationTimeout);
|
|
570
|
+
const faucetUrl = network === dltIota.NetworkTypes.Mainnet
|
|
571
|
+
? undefined
|
|
572
|
+
: cliCore.CLIParam.stringValue("faucetUrl", opts.faucetUrl);
|
|
573
|
+
let deployerMnemonic;
|
|
574
|
+
try {
|
|
575
|
+
deployerMnemonic = cliCore.CLIParam.stringValue("deployerMnemonic", opts.deployerMnemonic);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// Optional parameter, can be undefined
|
|
579
|
+
deployerMnemonic = undefined;
|
|
580
|
+
}
|
|
581
|
+
let deployerSeed;
|
|
582
|
+
try {
|
|
583
|
+
deployerSeed = cliCore.CLIParam.stringValue("deployerSeed", opts.deployerSeed);
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// Optional parameter, can be undefined
|
|
587
|
+
deployerSeed = undefined;
|
|
588
|
+
}
|
|
589
|
+
// Validate that at least one deployer credential is provided
|
|
590
|
+
const hasValidMnemonic = core.Is.stringValue(deployerMnemonic);
|
|
591
|
+
const hasValidSeed = core.Is.stringValue(deployerSeed);
|
|
592
|
+
if (!hasValidMnemonic && !hasValidSeed) {
|
|
593
|
+
throw new core.GeneralError("commands", "commands.deploy.deployerCredentialRequired", {
|
|
594
|
+
network
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
// Check/switch to target network environment BEFORE loading config
|
|
598
|
+
await setIotaEnvironment(network, rpcUrl, addressIndex, dryRun, deployerMnemonic, deployerSeed);
|
|
599
|
+
const config = await createNetworkConfig(network, rpcUrl, addressIndex, rpcTimeout, gasBudget, confirmationTimeout);
|
|
600
|
+
validateNetworkConfig(config, network);
|
|
601
|
+
const contractsData = await loadCompiledContracts(contractsPath);
|
|
602
|
+
if (network === dltIota.NetworkTypes.Mainnet) {
|
|
603
|
+
const validatedMnemonic = await getDeploymentMnemonic(network, hasValidMnemonic ? deployerMnemonic : undefined);
|
|
604
|
+
await validateDeploymentEnvironment(network, validatedMnemonic);
|
|
605
|
+
}
|
|
606
|
+
const networkContracts = contractsData[network];
|
|
607
|
+
if (!core.Is.object(networkContracts)) {
|
|
608
|
+
throw new core.GeneralError("commands", "commands.deploy.noContractsFound", {
|
|
609
|
+
network,
|
|
610
|
+
contractsPath
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
await deployContract("contract", networkContracts, config, network, dryRun, force, faucetUrl, deployerMnemonic, deployerSeed);
|
|
614
|
+
if (!dryRun) {
|
|
615
|
+
await updateContractsFile(contractsPath, contractsData);
|
|
616
|
+
}
|
|
617
|
+
cliCore.CLIDisplay.done();
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
cliCore.CLIDisplay.error(err);
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
324
624
|
/**
|
|
325
625
|
* Switch IOTA CLI to the target network environment and set the active address.
|
|
326
626
|
* @param network Target network to switch to
|
|
627
|
+
* @param rpcUrl The RPC URL for the network
|
|
628
|
+
* @param addressIndex The address index to derive the target address
|
|
327
629
|
* @param dryRun Whether this is a dry run (checks environment but doesn't switch)
|
|
630
|
+
* @param deployerMnemonic The deployer mnemonic from environment variables.
|
|
631
|
+
* @param deployerSeed The deployer seed from environment variables (optional).
|
|
328
632
|
*/
|
|
329
|
-
async function setIotaEnvironment(network, dryRun = false) {
|
|
633
|
+
async function setIotaEnvironment(network, rpcUrl, addressIndex, dryRun = false, deployerMnemonic, deployerSeed) {
|
|
330
634
|
try {
|
|
331
635
|
cliCore.CLIDisplay.task(dryRun
|
|
332
636
|
? core.I18n.formatMessage("commands.deploy.progress.checkingEnvironment")
|
|
333
637
|
: core.I18n.formatMessage("commands.deploy.progress.settingEnvironment"));
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
throw new core.GeneralError("commands", "commands.deploy.environmentNotFound", {
|
|
338
|
-
network,
|
|
339
|
-
availableEnvironments: envListOutput,
|
|
340
|
-
setupCommand: `iota client new-env --alias ${network} --rpc <RPC_URL>`
|
|
341
|
-
});
|
|
342
|
-
}
|
|
638
|
+
// Ensure environment exists, create if necessary
|
|
639
|
+
core.Guards.stringValue("setIotaEnvironment", "rpcUrl", rpcUrl);
|
|
640
|
+
await ensureEnvironment(network, rpcUrl);
|
|
343
641
|
if (dryRun) {
|
|
344
642
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.iotaEnvironmentCheck"), `✅ ${network} environment exists`, 1);
|
|
345
643
|
return;
|
|
346
644
|
}
|
|
347
645
|
// Derive the target address from existing mnemonic/seed
|
|
348
|
-
const
|
|
349
|
-
const targetAddress = await getDeploymentWalletAddress(network, addressIndex);
|
|
646
|
+
const targetAddress = await getDeploymentWalletAddress(network, addressIndex, deployerMnemonic, deployerSeed);
|
|
350
647
|
// Ensure the correct deployer key exists in the keystore
|
|
351
648
|
const aliasName = `deployer-${network}`;
|
|
352
|
-
await
|
|
649
|
+
const validatedMnemonic = await getDeploymentMnemonic(network, deployerMnemonic);
|
|
650
|
+
await ensureCorrectDeployerKey(network, aliasName, targetAddress, addressIndex, validatedMnemonic);
|
|
353
651
|
// Switch both environment and address in one command
|
|
354
652
|
await execAsync(`iota client switch --env ${network} --address ${targetAddress}`);
|
|
355
653
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.switchedIotaEnvironment"), network, 1);
|
|
@@ -377,76 +675,31 @@ async function setIotaEnvironment(network, dryRun = false) {
|
|
|
377
675
|
throw new core.GeneralError("commands", "commands.deploy.environmentOperationFailed", { network, operation: dryRun ? "check" : "switch to" }, error);
|
|
378
676
|
}
|
|
379
677
|
}
|
|
380
|
-
/**
|
|
381
|
-
* Action for the deploy command.
|
|
382
|
-
* @param opts Command options.
|
|
383
|
-
* @param opts.contracts Path to compiled modules JSON.
|
|
384
|
-
* @param opts.network Network identifier - optional if NETWORK env var is set.
|
|
385
|
-
* @param opts.dryRun Simulate deployment without executing.
|
|
386
|
-
* @param opts.force Force redeployment of existing packages.
|
|
387
|
-
*/
|
|
388
|
-
async function actionCommandDeploy(opts) {
|
|
389
|
-
cliCore.CLIDisplay.section(core.I18n.formatMessage("commands.deploy.section.deployContracts"));
|
|
390
|
-
cliCore.CLIDisplay.section(opts.contracts ?? "smart-contract-deployments.json");
|
|
391
|
-
try {
|
|
392
|
-
const contractsPath = opts.contracts ?? "smart-contract-deployments.json";
|
|
393
|
-
const dryRun = opts.dryRun ?? false;
|
|
394
|
-
const force = opts.force ?? false;
|
|
395
|
-
const network = opts.network ?? process.env.NETWORK;
|
|
396
|
-
core.Guards.arrayOneOf("commands", "network", network, Object.values(dltIota.NetworkTypes));
|
|
397
|
-
// Verify the IOTA SDK before we do anything else
|
|
398
|
-
await verifyIotaSDK();
|
|
399
|
-
// Check/switch to target network environment BEFORE loading config
|
|
400
|
-
await setIotaEnvironment(network, dryRun);
|
|
401
|
-
const config = await createNetworkConfig(network);
|
|
402
|
-
validateNetworkConfig(config, network);
|
|
403
|
-
const contractsData = await loadCompiledContracts(contractsPath);
|
|
404
|
-
if (network === dltIota.NetworkTypes.Mainnet) {
|
|
405
|
-
await validateDeploymentEnvironment(network);
|
|
406
|
-
}
|
|
407
|
-
const networkContracts = contractsData[network];
|
|
408
|
-
if (!core.Is.object(networkContracts)) {
|
|
409
|
-
throw new core.GeneralError("commands", "commands.deploy.noContractsFound", {
|
|
410
|
-
network,
|
|
411
|
-
contractsPath
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
await deployContract("contract", networkContracts, config, network, dryRun, force);
|
|
415
|
-
if (!dryRun) {
|
|
416
|
-
await updateContractsFile(contractsPath, contractsData);
|
|
417
|
-
}
|
|
418
|
-
cliCore.CLIDisplay.done();
|
|
419
|
-
}
|
|
420
|
-
catch (err) {
|
|
421
|
-
cliCore.CLIDisplay.error(err);
|
|
422
|
-
throw err;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
678
|
/**
|
|
426
679
|
* Creates the network configuration.
|
|
427
680
|
* @param network Target network to determine which env file to load.
|
|
681
|
+
* @param rpcUrl The RPC URL for the network.
|
|
682
|
+
* @param addressIndex The address index for the wallet.
|
|
683
|
+
* @param rpcTimeout The RPC timeout in milliseconds.
|
|
684
|
+
* @param gasBudget The gas budget for deployment.
|
|
685
|
+
* @param confirmationTimeout The confirmation timeout in seconds.
|
|
428
686
|
* @returns Network configuration.
|
|
429
687
|
*/
|
|
430
|
-
async function createNetworkConfig(network) {
|
|
688
|
+
async function createNetworkConfig(network, rpcUrl, addressIndex, rpcTimeout, gasBudget, confirmationTimeout) {
|
|
431
689
|
try {
|
|
432
|
-
|
|
433
|
-
if (!core.Is.stringValue(rpcUrl)) {
|
|
434
|
-
throw new core.GeneralError("commands", "commands.deploy.rpcUrlRequired", {
|
|
435
|
-
network
|
|
436
|
-
});
|
|
437
|
-
}
|
|
690
|
+
core.Guards.stringValue("createNetworkConfig", "rpcUrl", rpcUrl);
|
|
438
691
|
const config = {
|
|
439
692
|
network,
|
|
440
693
|
platform: "iota",
|
|
441
694
|
rpc: {
|
|
442
695
|
url: rpcUrl,
|
|
443
|
-
timeout:
|
|
696
|
+
timeout: rpcTimeout ?? 60000
|
|
444
697
|
},
|
|
445
698
|
deployment: {
|
|
446
|
-
gasBudget:
|
|
447
|
-
confirmationTimeout:
|
|
699
|
+
gasBudget: gasBudget ?? 50000000,
|
|
700
|
+
confirmationTimeout: confirmationTimeout ?? 60,
|
|
448
701
|
wallet: {
|
|
449
|
-
addressIndex
|
|
702
|
+
addressIndex
|
|
450
703
|
}
|
|
451
704
|
}
|
|
452
705
|
};
|
|
@@ -508,10 +761,13 @@ async function loadCompiledContracts(contractsPath) {
|
|
|
508
761
|
* @param network Target network.
|
|
509
762
|
* @param config Network configuration.
|
|
510
763
|
* @param isDryRun Whether this is a dry run.
|
|
764
|
+
* @param faucetUrl The faucet URL (optional, defaults to network-specific URL).
|
|
765
|
+
* @param deployerMnemonic The deployer mnemonic for validation.
|
|
766
|
+
* @param deployerSeed The deployer seed (optional).
|
|
511
767
|
* @returns Wallet address for the deployment.
|
|
512
768
|
*/
|
|
513
|
-
async function validateEnvironmentForNetwork(network, config, isDryRun = false) {
|
|
514
|
-
const walletAddress = await getDeploymentWalletAddress(network, config.deployment.wallet.addressIndex);
|
|
769
|
+
async function validateEnvironmentForNetwork(network, config, isDryRun = false, faucetUrl, deployerMnemonic, deployerSeed) {
|
|
770
|
+
const walletAddress = await getDeploymentWalletAddress(network, config.deployment.wallet.addressIndex, deployerMnemonic, deployerSeed);
|
|
515
771
|
if (isDryRun) {
|
|
516
772
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.dryRunWalletAddress"), walletAddress, 1);
|
|
517
773
|
}
|
|
@@ -519,11 +775,12 @@ async function validateEnvironmentForNetwork(network, config, isDryRun = false)
|
|
|
519
775
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.walletAddress"), walletAddress, 1);
|
|
520
776
|
}
|
|
521
777
|
if (network === dltIota.NetworkTypes.Mainnet) {
|
|
522
|
-
await
|
|
778
|
+
const validatedMnemonic = await getDeploymentMnemonic(network, deployerMnemonic);
|
|
779
|
+
await validateDeploymentEnvironment(network, validatedMnemonic);
|
|
523
780
|
}
|
|
524
781
|
else if ((network === dltIota.NetworkTypes.Testnet || network === dltIota.NetworkTypes.Devnet) && !isDryRun) {
|
|
525
782
|
// For testnet/devnet, check balance first and only request funds if needed
|
|
526
|
-
await checkBalanceAndRequestFaucetIfNeeded(network, config, walletAddress);
|
|
783
|
+
await checkBalanceAndRequestFaucetIfNeeded(network, config, walletAddress, faucetUrl);
|
|
527
784
|
}
|
|
528
785
|
return walletAddress;
|
|
529
786
|
}
|
|
@@ -583,13 +840,16 @@ async function checkWalletBalance(network, config, walletAddress, isDryRun = fal
|
|
|
583
840
|
* @param contractData Contract data.
|
|
584
841
|
* @param config Network configuration.
|
|
585
842
|
* @param network Target network.
|
|
843
|
+
* @param faucetUrl The faucet URL (optional, defaults to network-specific URL).
|
|
844
|
+
* @param deployerMnemonic The deployer mnemonic from environment variables.
|
|
845
|
+
* @param deployerSeed The deployer seed from environment variables (optional).
|
|
586
846
|
*/
|
|
587
|
-
async function handleDryRunValidation(contractName, contractData, config, network) {
|
|
847
|
+
async function handleDryRunValidation(contractName, contractData, config, network, faucetUrl, deployerMnemonic, deployerSeed) {
|
|
588
848
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.dryRunWouldDeploy"), `${contractName} (${network})`, 1);
|
|
589
849
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.dryRunPackageId"), contractData.packageId, 1);
|
|
590
850
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.dryRunRpcUrl"), config.rpc.url, 1);
|
|
591
851
|
try {
|
|
592
|
-
const walletAddress = await validateEnvironmentForNetwork(network, config, true);
|
|
852
|
+
const walletAddress = await validateEnvironmentForNetwork(network, config, true, faucetUrl, deployerMnemonic, deployerSeed);
|
|
593
853
|
await checkWalletBalance(network, config, walletAddress, true);
|
|
594
854
|
}
|
|
595
855
|
catch (err) {
|
|
@@ -604,10 +864,13 @@ async function handleDryRunValidation(contractName, contractData, config, networ
|
|
|
604
864
|
* @param contractData Contract data.
|
|
605
865
|
* @param config Network configuration.
|
|
606
866
|
* @param network Target network.
|
|
867
|
+
* @param faucetUrl The faucet URL (optional, defaults to network-specific URL).
|
|
868
|
+
* @param deployerMnemonic The deployer mnemonic from environment variables.
|
|
869
|
+
* @param deployerSeed The deployer seed from environment variables (optional).
|
|
607
870
|
*/
|
|
608
|
-
async function handleActualDeployment(contractName, contractData, config, network) {
|
|
871
|
+
async function handleActualDeployment(contractName, contractData, config, network, faucetUrl, deployerMnemonic, deployerSeed) {
|
|
609
872
|
try {
|
|
610
|
-
const walletAddress = await validateEnvironmentForNetwork(network, config, false);
|
|
873
|
+
const walletAddress = await validateEnvironmentForNetwork(network, config, false, faucetUrl, deployerMnemonic, deployerSeed);
|
|
611
874
|
await checkWalletBalance(network, config, walletAddress, false);
|
|
612
875
|
const deploymentResult = await deployWithIotaCli(config.deployment.gasBudget);
|
|
613
876
|
contractData.deployedPackageId = deploymentResult.packageId;
|
|
@@ -638,34 +901,39 @@ async function handleActualDeployment(contractName, contractData, config, networ
|
|
|
638
901
|
* @param network Target network
|
|
639
902
|
* @param dryRun Whether this is a dry run
|
|
640
903
|
* @param force Whether to force redeployment
|
|
904
|
+
* @param faucetUrl The faucet URL (optional, defaults to network-specific URL).
|
|
905
|
+
* @param deployerMnemonic The deployer mnemonic from environment variables.
|
|
906
|
+
* @param deployerSeed The deployer seed from environment variables (optional).
|
|
641
907
|
*/
|
|
642
|
-
async function deployContract(contractName, contractData, config, network, dryRun, force) {
|
|
908
|
+
async function deployContract(contractName, contractData, config, network, dryRun, force, faucetUrl, deployerMnemonic, deployerSeed) {
|
|
643
909
|
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.deploy.progress.deployingContract", { contractName, network }));
|
|
644
910
|
if (core.Is.stringValue(contractData.deployedPackageId) && !force) {
|
|
645
911
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.contractAlreadyDeployed"), contractData.deployedPackageId, 1);
|
|
646
912
|
return;
|
|
647
913
|
}
|
|
648
914
|
if (dryRun) {
|
|
649
|
-
await handleDryRunValidation(contractName, contractData, config, network);
|
|
915
|
+
await handleDryRunValidation(contractName, contractData, config, network, faucetUrl, deployerMnemonic, deployerSeed);
|
|
650
916
|
return;
|
|
651
917
|
}
|
|
652
|
-
await handleActualDeployment(contractName, contractData, config, network);
|
|
918
|
+
await handleActualDeployment(contractName, contractData, config, network, faucetUrl, deployerMnemonic, deployerSeed);
|
|
653
919
|
}
|
|
654
920
|
/**
|
|
655
921
|
* Get wallet address for deployment, preferring seed over mnemonic if available.
|
|
656
922
|
* @param network The target network.
|
|
657
923
|
* @param addressIndex The address index to derive.
|
|
924
|
+
* @param deployerMnemonic The deployer mnemonic from environment variables.
|
|
925
|
+
* @param deployerSeed The deployer seed from environment variables (optional).
|
|
658
926
|
* @returns The wallet address.
|
|
659
927
|
*/
|
|
660
|
-
async function getDeploymentWalletAddress(network, addressIndex) {
|
|
928
|
+
async function getDeploymentWalletAddress(network, addressIndex, deployerMnemonic, deployerSeed) {
|
|
661
929
|
// Try to use seed first if available
|
|
662
|
-
const hexSeed = await getDeploymentSeed(network);
|
|
930
|
+
const hexSeed = await getDeploymentSeed(network, deployerSeed);
|
|
663
931
|
let seed;
|
|
664
932
|
if (core.Is.stringValue(hexSeed)) {
|
|
665
933
|
seed = core.Converter.hexToBytes(hexSeed);
|
|
666
934
|
}
|
|
667
935
|
else {
|
|
668
|
-
const mnemonic = await getDeploymentMnemonic(network);
|
|
936
|
+
const mnemonic = await getDeploymentMnemonic(network, deployerMnemonic);
|
|
669
937
|
seed = crypto.Bip39.mnemonicToSeed(mnemonic);
|
|
670
938
|
}
|
|
671
939
|
const addresses = dltIota.Iota.getAddresses(seed, dltIota.Iota.DEFAULT_COIN_TYPE, 0, addressIndex, 1, false);
|
|
@@ -683,14 +951,12 @@ function nanosToIota(nanos) {
|
|
|
683
951
|
* Request funds from the faucet for testnet or devnet deployment.
|
|
684
952
|
* @param network The target network (testnet or devnet).
|
|
685
953
|
* @param walletAddress The wallet address to fund.
|
|
954
|
+
* @param rpcUrl The RPC URL for the network.
|
|
955
|
+
* @param faucetUrl The faucet URL (optional, defaults to network-specific URL).
|
|
686
956
|
* @returns Promise that resolves when funding is complete.
|
|
687
957
|
*/
|
|
688
|
-
async function requestFaucetFunds(network, walletAddress) {
|
|
689
|
-
if (network !== dltIota.NetworkTypes.Testnet && network !== dltIota.NetworkTypes.Devnet) {
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
958
|
+
async function requestFaucetFunds(network, walletAddress, rpcUrl, faucetUrl) {
|
|
692
959
|
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.deploy.progress.requestingFaucetFunds", { network }));
|
|
693
|
-
const faucetUrl = process.env.FAUCET_URL ?? `https://faucet.${network}.iota.cafe`;
|
|
694
960
|
const response = await faucet.requestIotaFromFaucetV0({
|
|
695
961
|
host: faucetUrl,
|
|
696
962
|
recipient: walletAddress
|
|
@@ -698,7 +964,7 @@ async function requestFaucetFunds(network, walletAddress) {
|
|
|
698
964
|
if (response?.error) {
|
|
699
965
|
throw new core.GeneralError("commands", "commands.deploy.fundingFailed", undefined, response.error);
|
|
700
966
|
}
|
|
701
|
-
const client$1 = new client.IotaClient({ url:
|
|
967
|
+
const client$1 = new client.IotaClient({ url: rpcUrl });
|
|
702
968
|
const balanceResponse = await client$1.getBalance({ owner: walletAddress });
|
|
703
969
|
const balanceInNanos = Number(balanceResponse.totalBalance);
|
|
704
970
|
if (balanceInNanos > 0) {
|
|
@@ -714,12 +980,10 @@ async function requestFaucetFunds(network, walletAddress) {
|
|
|
714
980
|
* @param network The target network (testnet or devnet).
|
|
715
981
|
* @param config Network configuration.
|
|
716
982
|
* @param walletAddress The wallet address to check and potentially fund.
|
|
983
|
+
* @param faucetUrl The faucet URL (optional, defaults to network-specific URL).
|
|
717
984
|
* @returns Promise that resolves when balance check and optional funding is complete.
|
|
718
985
|
*/
|
|
719
|
-
async function checkBalanceAndRequestFaucetIfNeeded(network, config, walletAddress) {
|
|
720
|
-
if (network !== dltIota.NetworkTypes.Testnet && network !== dltIota.NetworkTypes.Devnet) {
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
986
|
+
async function checkBalanceAndRequestFaucetIfNeeded(network, config, walletAddress, faucetUrl) {
|
|
723
987
|
// Check current balance
|
|
724
988
|
const client$1 = new client.IotaClient({ url: config.rpc.url });
|
|
725
989
|
const balanceResponse = await client$1.getBalance({ owner: walletAddress });
|
|
@@ -736,8 +1000,11 @@ async function checkBalanceAndRequestFaucetIfNeeded(network, config, walletAddre
|
|
|
736
1000
|
currentBalance: balanceInIota.toFixed(2),
|
|
737
1001
|
requiredBalance: requiredInIota.toFixed(2)
|
|
738
1002
|
}), 1);
|
|
1003
|
+
if (!core.Is.stringValue(faucetUrl)) {
|
|
1004
|
+
throw new core.GeneralError("commands", "error.commands.deploy.noFaucetConfigured");
|
|
1005
|
+
}
|
|
739
1006
|
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.deploy.progress.requestingAdditionalFaucetFunds"));
|
|
740
|
-
await requestFaucetFunds(network, walletAddress);
|
|
1007
|
+
await requestFaucetFunds(network, walletAddress, config.rpc.url, faucetUrl);
|
|
741
1008
|
// Check balance again after faucet request
|
|
742
1009
|
const updatedBalanceResponse = await client$1.getBalance({ owner: walletAddress });
|
|
743
1010
|
const updatedBalanceInNanos = Number(updatedBalanceResponse.totalBalance);
|
|
@@ -829,8 +1096,9 @@ function generateUniqueBackupAlias(baseAlias, existingKeys) {
|
|
|
829
1096
|
* @param aliasName The desired alias name (e.g., "deployer-testnet").
|
|
830
1097
|
* @param expectedAddress The expected address from the current mnemonic.
|
|
831
1098
|
* @param addressIndex The address index to use.
|
|
1099
|
+
* @param deployerMnemonic The deployer mnemonic.
|
|
832
1100
|
*/
|
|
833
|
-
async function ensureCorrectDeployerKey(network, aliasName, expectedAddress, addressIndex) {
|
|
1101
|
+
async function ensureCorrectDeployerKey(network, aliasName, expectedAddress, addressIndex, deployerMnemonic) {
|
|
834
1102
|
try {
|
|
835
1103
|
// Check if the alias already exists in keystore
|
|
836
1104
|
const { stdout: keysListOutput } = await execAsync("iota keytool list --json");
|
|
@@ -847,7 +1115,7 @@ async function ensureCorrectDeployerKey(network, aliasName, expectedAddress, add
|
|
|
847
1115
|
await execAsync(`iota keytool update-alias "${aliasName}" "${backupAlias}"`);
|
|
848
1116
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.renamedExistingKey"), `${aliasName} → ${backupAlias} (${existingAddress})`, 1);
|
|
849
1117
|
// Now import the correct key with the desired alias
|
|
850
|
-
await importCorrectDeployerKey(network, aliasName, addressIndex, expectedAddress);
|
|
1118
|
+
await importCorrectDeployerKey(network, aliasName, addressIndex, expectedAddress, deployerMnemonic);
|
|
851
1119
|
}
|
|
852
1120
|
else {
|
|
853
1121
|
// Existing key is correct - no action needed
|
|
@@ -856,7 +1124,7 @@ async function ensureCorrectDeployerKey(network, aliasName, expectedAddress, add
|
|
|
856
1124
|
}
|
|
857
1125
|
else {
|
|
858
1126
|
// No existing alias - import the key
|
|
859
|
-
await importCorrectDeployerKey(network, aliasName, addressIndex, expectedAddress);
|
|
1127
|
+
await importCorrectDeployerKey(network, aliasName, addressIndex, expectedAddress, deployerMnemonic);
|
|
860
1128
|
}
|
|
861
1129
|
// Verify the address exists in client addresses
|
|
862
1130
|
const { stdout: addressListOutput } = await execAsync("iota client addresses --json");
|
|
@@ -879,10 +1147,11 @@ async function ensureCorrectDeployerKey(network, aliasName, expectedAddress, add
|
|
|
879
1147
|
* @param aliasName The alias name to use.
|
|
880
1148
|
* @param addressIndex The address index.
|
|
881
1149
|
* @param targetAddress The expected target address (avoids redundant calculation).
|
|
1150
|
+
* @param deployerMnemonic The deployer mnemonic.
|
|
882
1151
|
*/
|
|
883
|
-
async function importCorrectDeployerKey(network, aliasName, addressIndex, targetAddress) {
|
|
1152
|
+
async function importCorrectDeployerKey(network, aliasName, addressIndex, targetAddress, deployerMnemonic) {
|
|
884
1153
|
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.deploy.progress.importingDeployerKey", { aliasName }));
|
|
885
|
-
const mnemonic = await getDeploymentMnemonic(network);
|
|
1154
|
+
const mnemonic = await getDeploymentMnemonic(network, deployerMnemonic);
|
|
886
1155
|
const derivationPath = crypto.Bip44.path(dltIota.Iota.DEFAULT_COIN_TYPE, 0, false, addressIndex).toString();
|
|
887
1156
|
await execAsync(`iota keytool import "${mnemonic}" ed25519 "${derivationPath}" --alias "${aliasName}"`);
|
|
888
1157
|
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.deploy.labels.importedDeployerKey"), `${aliasName} (${targetAddress})`, 1);
|
|
@@ -906,7 +1175,7 @@ class CLI extends cliCore.CLIBase {
|
|
|
906
1175
|
return this.execute({
|
|
907
1176
|
title: "TWIN Move to JSON",
|
|
908
1177
|
appName: "move-to-json",
|
|
909
|
-
version: "0.0.2-next.
|
|
1178
|
+
version: "0.0.2-next.8", // x-release-please-version
|
|
910
1179
|
icon: "⚙️ ",
|
|
911
1180
|
supportsEnvFiles: true,
|
|
912
1181
|
overrideOutputWidth: options?.overrideOutputWidth
|