@twin.org/move-to-json 0.0.2-next.9 → 0.0.3-next.10

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 CHANGED
@@ -1,387 +1,21 @@
1
1
  # TWIN Move to JSON
2
2
 
3
- Tool to convert Move source files to JSON with network-specific deployment support.
4
-
5
- ## Prerequisites
6
-
7
- ### IOTA CLI Installation
8
-
9
- Before using this tool, you need to install the IOTA CLI. The simplest way is using Homebrew:
10
-
11
- ```bash
12
- # Install IOTA CLI using Homebrew (macOS/Linux/WSL)
13
- brew install iotaledger/tap/iota
14
- ```
15
-
16
- **Alternative installation methods:**
17
-
18
- For other platforms or installation from source, see the [official IOTA installation guide](https://docs.iota.org/developer/getting-started/install-iota).
19
-
20
- **Verify installation:**
21
-
22
- ```bash
23
- iota --version
24
- ```
3
+ This app provides a command line workflow for compiling Move contracts and producing deployment-ready JSON for IOTA networks. It is intended for development teams that want predictable build and deployment inputs across test and production environments.
25
4
 
26
5
  ## Installation
27
6
 
28
7
  ```shell
29
- npm install -g @twin.org/move-to-json
30
- ```
31
-
32
- ## Quick Start
33
-
34
- ```bash
35
- # Build contracts for mainnet using environment variables (recommended)
36
- npx move-to-json build "src/**/*.move" --load-env configs/mainnet.env --output smart-contract-deployments.json
37
-
38
- # Deploy to mainnet using environment variables (recommended)
39
- npx move-to-json deploy --load-env configs/mainnet.env --contracts smart-contract-deployments.json
40
- ```
41
-
42
- ## Environment Setup for Production Deployments
43
-
44
- ### Required Environment Variables
45
-
46
- For production deployments, you need to set up environment variables containing your deployment credentials. The tool uses network-specific configuration files located in the `configs/` directory.
47
-
48
- #### 1. Create Environment Files
49
-
50
- Copy the environment templates for each network you plan to use:
51
-
52
- ```bash
53
- # Copy environment templates for each network
54
- cp configs/testnet.env.example configs/testnet.env
55
- cp configs/devnet.env.example configs/devnet.env
56
- cp configs/mainnet.env.example configs/mainnet.env
57
- ```
58
-
59
- **Note:** Only copy the environment files for the networks you actually plan to deploy to. For development, you typically only need `testnet.env` and `devnet.env`.
60
-
61
- #### 2. Generate Deployment Mnemonics
62
-
63
- Use the wallet CLI to generate secure mnemonics and then manually update the appropriate network configuration files with the generated mnemonic.
64
-
65
- ### Environment Variable Reference
66
-
67
- Each network has its own configuration file in the `configs/` directory with the required deployment mnemonic:
68
-
69
- | Network | File | Required Variable |
70
- | ------- | --------------------- | ------------------- |
71
- | Testnet | `configs/testnet.env` | `DEPLOYER_MNEMONIC` |
72
- | Devnet | `configs/devnet.env` | `DEPLOYER_MNEMONIC` |
73
- | Mainnet | `configs/mainnet.env` | `DEPLOYER_MNEMONIC` |
74
-
75
- ### Security Best Practices
76
-
77
- #### Local Development
78
-
79
- - Store credentials in `configs/*.env` files (never commit to git - only commit `.example` files)
80
- - Use test mnemonics with faucet funds for testnet/devnet
81
- - Use dedicated deployment wallets separate from personal wallets
82
-
83
- #### CI/CD and Production
84
-
85
- - Store credentials in GitHub Secrets or your CI/CD secrets manager
86
- - Use hardware wallets or HSM for mainnet deployment keys
87
- - Implement approval workflows for mainnet deployments
88
- - Monitor wallet balances and deployment costs
89
-
90
- ### Generating Credentials
91
-
92
- #### 1. Generate Mnemonic and Seed
93
-
94
- ```bash
95
- # Generate a new 24-word mnemonic and save to wallet.env
96
- npx "@twin.org/wallet-cli" mnemonic --env wallet.env
97
- ```
98
-
99
- #### 2. Generate Addresses from Seed
100
-
101
- ```bash
102
- # Generate 5 addresses and save to address.env
103
- npx "@twin.org/wallet-cli" address --load-env wallet.env --seed '!SEED' --count 5 --env address.env
104
- ```
105
-
106
- ## Commands
107
-
108
- ### build
109
-
110
- Compile Move contracts for a specific network:
111
-
112
- ```bash
113
- # Using environment variables (recommended)
114
- npx move-to-json build "src/**/*.move" --load-env configs/<network>.env [--output <file>]
115
-
116
- # Using explicit network flag
117
- npx move-to-json build "src/**/*.move" --network <network> [--output <file>]
118
-
119
- # If installed globally
120
- move-to-json build "src/**/*.move" --load-env configs/<network>.env [--output <file>]
121
- ```
122
-
123
- **Options:**
124
-
125
- - `--network <network>` - Target network (testnet/devnet/mainnet) **[Optional if using --load-env]**
126
- - `--load-env <file>` - Load environment variables from file (must contain NETWORK variable)
127
- - `--output <file>` - Output JSON file (default: smart-contract-deployments.json)
128
-
129
- **What it does:**
130
-
131
- 1. Reads network from `--network` flag or `NETWORK` environment variable
132
- 2. Validates environment variables for the target network
133
- 3. Cleans build artifacts and Move.lock files
134
- 4. Compiles contracts using unified Move.toml (same bytecode for all networks)
135
- 5. Generates network-aware JSON with package IDs and base64 modules
136
-
137
- **Key Changes:**
138
-
139
- - **Unified Move.toml**: Uses single Move.toml file with `framework/testnet` for consistent builds
140
- - **Network-agnostic compilation**: Same bytecode works across all networks
141
- - **Environment-aware**: Network targeting handled by IOTA CLI environments, not Move.toml
142
-
143
- **Example:**
144
-
145
- ```bash
146
- # Build for testnet using environment variables (recommended)
147
- npx move-to-json build "tests/fixtures/sources/**/*.move" --load-env configs/testnet.env --output tests/fixtures/smart-contract-deployments/smart-contract-deployments.json
148
-
149
- # Build for mainnet using environment variables (recommended)
150
- npx move-to-json build "src/contracts/**/*.move" --load-env configs/mainnet.env --output smart-contract-deployments.json
151
-
152
- # Alternative: using explicit network flag
153
- npx move-to-json build "tests/fixtures/sources/**/*.move" --network testnet --output tests/fixtures/smart-contract-deployments/smart-contract-deployments.json
154
- ```
155
-
156
- ### deploy
157
-
158
- Deploy compiled contracts to the specified network:
159
-
160
- ```bash
161
- # Using environment variables (recommended)
162
- npx move-to-json deploy --load-env configs/<network>.env [--contracts <file>] [options]
163
-
164
- # Using explicit network flag
165
- npx move-to-json deploy --network <network> [--load-env configs/<network>.env] [options]
166
-
167
- # If installed globally
168
- move-to-json deploy --load-env configs/<network>.env [--contracts <file>] [options]
8
+ npm install -D @twin.org/move-to-json
169
9
  ```
170
10
 
171
- **Options:**
11
+ ## Usage
172
12
 
173
- - `--network <network>` - Network identifier (testnet/devnet/mainnet) **[Optional if using --load-env]**
174
- - `--load-env <file>` - Load environment variables from file (must contain NETWORK variable)
175
- - `--contracts <file>` - Compiled modules JSON file (default: smart-contract-deployments.json)
176
- - `--dry-run` - Simulate deployment without executing
177
- - `--force` - Force redeployment of existing packages
13
+ Usage of the CLI is shown in the examples [docs/usage.md](docs/usage.md)
178
14
 
179
- **What it does:**
15
+ ## Reference
180
16
 
181
- 1. Switches IOTA CLI to target network environment
182
- 2. Loads environment variables from the file specified via `--load-env` flag
183
- 3. Validates deployment credentials are available
184
- 4. Checks wallet balance against gas requirements
185
- 5. Loads compiled contracts from JSON
186
- 6. Deploys using IOTA CLI with active network environment
187
- 7. Extracts and saves both Package ID and UpgradeCap ID
188
- 8. Updates JSON with deployed package information
17
+ Detailed reference documentation for the API can be found in [docs/reference/index.md](docs/reference/index.md)
189
18
 
190
- **Key Changes:**
19
+ ## Changelog
191
20
 
192
- - **Environment switching**: Automatically switches IOTA CLI to target network
193
- - **Network targeting**: Uses IOTA CLI environments instead of Move.toml configuration
194
- - **Consistent deployment**: Same compiled bytecode deployed to all networks
195
- - **UpgradeCap tracking**: Captures and stores UpgradeCap object ID for future package upgrades
196
-
197
- **Prerequisites:**
198
- Ensure IOTA CLI environments are configured:
199
-
200
- ```bash
201
- # Check available environments
202
- iota client envs
203
-
204
- # Configure networks if missing
205
- iota client new-env --alias testnet --rpc https://fullnode.testnet.iota.cafe:443
206
- iota client new-env --alias mainnet --rpc https://api.mainnet.iota.cafe
207
- iota client new-env --alias devnet --rpc https://api.devnet.iota.cafe
208
- ```
209
-
210
- **Example:**
211
-
212
- ```bash
213
- # Deploy to testnet using environment variables (recommended)
214
- npx move-to-json deploy --load-env configs/testnet.env --contracts tests/fixtures/smart-contract-deployments/smart-contract-deployments.json
215
-
216
- # Deploy to mainnet using environment variables (recommended)
217
- npx move-to-json deploy --load-env configs/mainnet.env --contracts tests/fixtures/smart-contract-deployments/smart-contract-deployments.json
218
-
219
- # Dry run (simulation)
220
- npx move-to-json deploy --load-env configs/testnet.env --contracts tests/fixtures/smart-contract-deployments/smart-contract-deployments.json --dry-run
221
- ```
222
-
223
- **Output:**
224
- The deployment saves both Package ID and UpgradeCap ID:
225
-
226
- ```json
227
- {
228
- "testnet": {
229
- "packageId": "0x...",
230
- "package": "base64data...",
231
- "deployedPackageId": "0x239ad3ea39f0910a4dc4c98161bcde948fb5ed0d7d7ae6d9a593239c43af748e",
232
- "upgradeCap": "0xfd6269c28e3931e41aa9d9e08ffabb8162cf1fd0baaef14094b4442e6c743edf"
233
- }
234
- }
235
- ```
236
-
237
- **Important Note on UpgradeCap:**
238
- The UpgradeCap ID is crucial for package upgrades. Keep this secure - it's required to upgrade your deployed packages in the future.
239
-
240
- ## Unified Move.toml Approach
241
-
242
- The tool now uses a **unified Move.toml approach** that eliminates the need for network-specific Move.toml files. This provides better consistency and reduces complexity.
243
-
244
- ### Single Move.toml Configuration
245
-
246
- Instead of multiple `Move.toml.{network}` files, use a single `Move.toml` file:
247
-
248
- ```toml
249
- [package]
250
- name = "my_contract"
251
- version = "0.0.1"
252
- edition = "2024.beta"
253
-
254
- [dependencies]
255
- Iota = {
256
- git = "https://github.com/iotaledger/iota.git",
257
- subdir = "crates/iota-framework/packages/iota-framework",
258
- rev = "framework/testnet"
259
- }
260
-
261
- [addresses]
262
- my_contract = "0x0"
263
-
264
- [dev-dependencies]
265
- # Optional: Override dependencies for testing
266
- # Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "framework/devnet", override = true }
267
-
268
- [dev-addresses]
269
- # Optional: Override addresses for development
270
- # my_contract = "0x1234"
271
- ```
272
-
273
- ### Framework Version Strategy
274
-
275
- - **Development/Testing**: Use `framework/testnet` for all networks
276
- - **Production**: Same `framework/testnet` works for mainnet deployment
277
- - **Consistency**: Same bytecode compiles for all target networks
278
-
279
- ### Network Targeting
280
-
281
- Network targeting is handled through **IOTA CLI environments**, not Move.toml:
282
-
283
- ```bash
284
- # Build once - same bytecode for all networks
285
- iota move build
286
-
287
- # Deploy to different networks by switching environments
288
- iota client switch --env testnet && iota client publish
289
- iota client switch --env mainnet && iota client publish
290
- ```
291
-
292
- ### Benefits
293
-
294
- - ✅ **Consistent builds** across all networks
295
- - ✅ **Reduced complexity** - no file copying
296
- - ✅ **Industry standard** approach
297
- - ✅ **Better maintainability** - single source of truth
298
- - ✅ **Eliminated build conflicts** between networks
299
-
300
- ## Package Upgrades and UpgradeCap Management
301
-
302
- When you deploy a Move package on IOTA, the network creates an **UpgradeCap** (Upgrade Capability) object that controls the ability to upgrade that package. This tool automatically captures and stores the UpgradeCap ID for future use.
303
-
304
- ### What is an UpgradeCap?
305
-
306
- The UpgradeCap is a special object that:
307
-
308
- - **Controls package upgrades**: Only the holder can upgrade the package
309
- - **Is created once**: Generated during initial package deployment
310
- - **Must be preserved**: Lost UpgradeCap = no future upgrades possible
311
- - **Is network-specific**: Each network deployment gets its own UpgradeCap
312
-
313
- ### UpgradeCap Storage
314
-
315
- The tool stores UpgradeCap IDs in the smart-contract-deployments.json file:
316
-
317
- ```json
318
- {
319
- "testnet": {
320
- "packageId": "0xabc123...",
321
- "package": "base64data...",
322
- "deployedPackageId": "0x239ad3ea39f0910a4dc4c98161bcde948fb5ed0d7d7ae6d9a593239c43af748e",
323
- "upgradeCap": "0xfd6269c28e3931e41aa9d9e08ffabb8162cf1fd0baaef14094b4442e6c743edf"
324
- }
325
- }
326
- ```
327
-
328
- ### Using UpgradeCap for Package Upgrades
329
-
330
- To upgrade a deployed package, you'll need the UpgradeCap ID:
331
-
332
- ```bash
333
- # Example upgrade command (using IOTA CLI)
334
- iota client call --package 0x2 --module package --function upgrade \
335
- --args 0xfd6269c28e3931e41aa9d9e08ffabb8162cf1fd0baaef14094b4442e6c743edf \
336
- --gas-budget 50000000
337
- ```
338
-
339
- ## Known Issues
340
-
341
- ### Windows
342
-
343
- #### Long Path Limitation Error
344
-
345
- **Error Symptoms:**
346
-
347
- - Build commands fail with `Failed to reset to latest Git state 'mainnet'`
348
- - Tests timeout during Move compilation
349
- - Git dependency resolution failures
350
-
351
- **Root Cause:**
352
- Windows has a default file path length limitation of 260 characters. The IOTA framework repository contains files with very long paths (especially in test directories) that exceed this limit, causing Git operations to fail during dependency resolution.
353
-
354
- **Solution:**
355
- Enable long path support in both Git and Windows:
356
-
357
- 1. **Enable Git Long Paths:**
358
-
359
- ```bash
360
- git config --global core.longpaths true
361
- ```
362
-
363
- 2. **Enable Windows Long Path Support (requires Administrator privileges):**
364
-
365
- ```powershell
366
- # Run PowerShell as Administrator
367
- Set-ItemProperty -Path 'HKLM:SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
368
- ```
369
-
370
- 3. **Restart your computer** for Windows registry changes to take effect.
371
-
372
- **Verification:**
373
- After applying the fix, you should see successful Git dependency updates:
374
-
375
- ```bash
376
- UPDATING GIT DEPENDENCY https://github.com/iotaledger/iota.git
377
- INCLUDING DEPENDENCY Iota
378
- INCLUDING DEPENDENCY MoveStdlib
379
- BUILDING nft
380
- ```
381
-
382
- **Alternative Workaround:**
383
- If you cannot enable long path support system-wide, you can use the `--skip-fetch-latest-git-deps` flag as a workaround, though this will use cached dependencies instead of the latest versions:
384
-
385
- ```bash
386
- iota move build --skip-fetch-latest-git-deps
387
- ```
21
+ The changes between each version can be found in [docs/changelog.md](docs/changelog.md)
package/bin/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright 2024 IOTA Stiftung.
3
3
  // SPDX-License-Identifier: Apache-2.0.
4
- import { CLI } from '../dist/esm/index.mjs';
4
+ import { CLI } from '../dist/es/index.js';
5
5
 
6
6
  const cli = new CLI();
7
7
  const result = await cli.run(process.argv);
package/dist/es/cli.js ADDED
@@ -0,0 +1,39 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CLIBase } from "@twin.org/cli-core";
6
+ import { buildCommandBuild } from "./commands/build.js";
7
+ import { buildCommandDeploy } from "./commands/deploy.js";
8
+ /**
9
+ * The main entry point for the Move to JSON CLI.
10
+ */
11
+ export class CLI extends CLIBase {
12
+ /**
13
+ * Run the app.
14
+ * @param argv The process arguments.
15
+ * @param localesDirectory The directory for the locales, default to relative to the script.
16
+ * @param options Additional options.
17
+ * @param options.overrideOutputWidth Override the output width.
18
+ * @returns The exit code.
19
+ */
20
+ async run(argv, localesDirectory, options) {
21
+ return this.execute({
22
+ title: "TWIN Move to JSON",
23
+ appName: "move-to-json",
24
+ version: "0.0.3-next.10", // x-release-please-version
25
+ icon: "⚙️ ",
26
+ supportsEnvFiles: true,
27
+ overrideOutputWidth: options?.overrideOutputWidth
28
+ }, localesDirectory ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "../locales"), argv);
29
+ }
30
+ /**
31
+ * Configure any options or actions at the root program level.
32
+ * @param program The root program command.
33
+ */
34
+ configureRoot(program) {
35
+ buildCommandBuild(program);
36
+ buildCommandDeploy(program);
37
+ }
38
+ }
39
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;GAEG;AACH,MAAM,OAAO,GAAI,SAAQ,OAAO;IAC/B;;;;;;;OAOG;IACI,KAAK,CAAC,GAAG,CACf,IAAc,EACd,gBAAyB,EACzB,OAEC;QAED,OAAO,IAAI,CAAC,OAAO,CAClB;YACC,KAAK,EAAE,mBAAmB;YAC1B,OAAO,EAAE,cAAc;YACvB,OAAO,EAAE,eAAe,EAAE,2BAA2B;YACrD,IAAI,EAAE,KAAK;YACX,gBAAgB,EAAE,IAAI;YACtB,mBAAmB,EAAE,OAAO,EAAE,mBAAmB;SACjD,EACD,gBAAgB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,EACzF,IAAI,CACJ,CAAC;IACH,CAAC;IAED;;;OAGG;IACO,aAAa,CAAC,OAAgB;QACvC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC3B,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;CACD","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { CLIBase } from \"@twin.org/cli-core\";\nimport type { Command } from \"commander\";\nimport { buildCommandBuild } from \"./commands/build.js\";\nimport { buildCommandDeploy } from \"./commands/deploy.js\";\n\n/**\n * The main entry point for the Move to JSON CLI.\n */\nexport class CLI extends CLIBase {\n\t/**\n\t * Run the app.\n\t * @param argv The process arguments.\n\t * @param localesDirectory The directory for the locales, default to relative to the script.\n\t * @param options Additional options.\n\t * @param options.overrideOutputWidth Override the output width.\n\t * @returns The exit code.\n\t */\n\tpublic async run(\n\t\targv: string[],\n\t\tlocalesDirectory?: string,\n\t\toptions?: {\n\t\t\toverrideOutputWidth?: number;\n\t\t}\n\t): Promise<number> {\n\t\treturn this.execute(\n\t\t\t{\n\t\t\t\ttitle: \"TWIN Move to JSON\",\n\t\t\t\tappName: \"move-to-json\",\n\t\t\t\tversion: \"0.0.3-next.10\", // x-release-please-version\n\t\t\t\ticon: \"⚙️ \",\n\t\t\t\tsupportsEnvFiles: true,\n\t\t\t\toverrideOutputWidth: options?.overrideOutputWidth\n\t\t\t},\n\t\t\tlocalesDirectory ?? path.join(path.dirname(fileURLToPath(import.meta.url)), \"../locales\"),\n\t\t\targv\n\t\t);\n\t}\n\n\t/**\n\t * Configure any options or actions at the root program level.\n\t * @param program The root program command.\n\t */\n\tprotected configureRoot(program: Command): void {\n\t\tbuildCommandBuild(program);\n\t\tbuildCommandDeploy(program);\n\t}\n}\n"]}
@@ -0,0 +1,220 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { promises as fsPromises } from "node:fs";
4
+ import path from "node:path";
5
+ import { CLIDisplay, CLIParam, CLIUtils } from "@twin.org/cli-core";
6
+ import { Converter, GeneralError, Guards, I18n, Is, StringHelper } from "@twin.org/core";
7
+ import { Sha3 } from "@twin.org/crypto";
8
+ import { NetworkTypes } from "@twin.org/dlt-iota";
9
+ import FastGlob from "fast-glob";
10
+ import { verifyIotaSDK } from "../utils/iotaUtils.js";
11
+ import { searchDirectoryForMoveToml } from "../utils/moveToJsonUtils.js";
12
+ /**
13
+ * Build the build command to be consumed by the CLI.
14
+ * @param program The command to build on.
15
+ */
16
+ export function buildCommandBuild(program) {
17
+ program
18
+ .command("build")
19
+ .description(I18n.formatMessage("commands.build.description"))
20
+ .argument("<inputGlob>", I18n.formatMessage("commands.build.options.inputGlob.description"))
21
+ .option(I18n.formatMessage("commands.build.options.network.param"), I18n.formatMessage("commands.build.options.network.description"), "!NETWORK")
22
+ .option(I18n.formatMessage("commands.build.options.output.param"), I18n.formatMessage("commands.build.options.output.description"), "smart-contract-deployments.json")
23
+ .action(actionCommandBuild);
24
+ }
25
+ /**
26
+ * Action for the build command.
27
+ * @param inputGlob A glob pattern that matches one or more Move files
28
+ * @param opts Additional options.
29
+ * @param opts.network Target network (testnet/devnet/mainnet).
30
+ * @param opts.output Where we store the final compiled modules.
31
+ */
32
+ export async function actionCommandBuild(inputGlob, opts) {
33
+ try {
34
+ const networkRaw = CLIParam.stringValue("network", opts.network);
35
+ const network = networkRaw;
36
+ Guards.arrayOneOf("commands", "network", network, Object.values(NetworkTypes));
37
+ // Verify the IOTA SDK before we do anything else
38
+ await verifyIotaSDK();
39
+ const { normalizedGlob, normalizedOutput, executionDir } = normalizePathsAndWorkingDir(inputGlob, opts.output ?? "smart-contract-deployments.json");
40
+ CLIDisplay.section(I18n.formatMessage("commands.build.section.buildingMoveContracts", {
41
+ network
42
+ }));
43
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.inputGlob"), inputGlob);
44
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.outputJson"), normalizedOutput);
45
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.network"), network);
46
+ CLIDisplay.break();
47
+ // Find matching .move files
48
+ CLIDisplay.task(I18n.formatMessage("commands.build.progress.searchingFiles"));
49
+ const matchedFiles = await FastGlob([normalizedGlob, "!**/build/**/*.move", "!**/dependencies/**/*.move"], {
50
+ cwd: executionDir,
51
+ absolute: true,
52
+ dot: true,
53
+ followSymbolicLinks: false,
54
+ caseSensitiveMatch: false, // Important for Windows
55
+ onlyFiles: true,
56
+ stats: false
57
+ });
58
+ if (matchedFiles.length === 0) {
59
+ CLIDisplay.value(I18n.formatMessage("commands.build.warnings.noMoveFilesFound", { inputGlob }), "", 2);
60
+ }
61
+ else if (matchedFiles.length > 1) {
62
+ throw new GeneralError("commands", "commands.build.multipleFilesNotSupported", {
63
+ fileCount: matchedFiles.length,
64
+ fileList: matchedFiles.map(f => path.basename(f)).join(", ")
65
+ });
66
+ }
67
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.matchedFilesCount"), matchedFiles.length.toString());
68
+ CLIDisplay.break();
69
+ // Prepare build environment
70
+ CLIDisplay.task(I18n.formatMessage("commands.build.progress.preparingBuildEnvironment"));
71
+ // Find all Move projects in the directory tree
72
+ const moveProjects = [];
73
+ await searchDirectoryForMoveToml(executionDir, moveProjects);
74
+ for (const projectRoot of moveProjects) {
75
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.preparedProject"), projectRoot, 1);
76
+ }
77
+ const existingJson = await CLIUtils.readJsonFile(normalizedOutput);
78
+ const finalJson = existingJson ?? {};
79
+ if (existingJson) {
80
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.mergingWithExistingJson"), normalizedOutput);
81
+ }
82
+ else {
83
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.noExistingJsonFound"), I18n.formatMessage("commands.build.labels.creatingNewJsonStructure"), 1);
84
+ }
85
+ CLIDisplay.break();
86
+ // Process the single Move file (validation ensures exactly 0 or 1 file)
87
+ if (matchedFiles.length === 1) {
88
+ const moveFile = matchedFiles[0];
89
+ CLIDisplay.task(I18n.formatMessage("commands.build.progress.processingMoveFile"), moveFile);
90
+ try {
91
+ const compiled = await processMoveFile(moveFile);
92
+ if (compiled) {
93
+ const { contractName, packageId, packageBytecode } = compiled;
94
+ const contractData = finalJson[network] ?? {
95
+ packageId: "",
96
+ packageBytecode: ""
97
+ };
98
+ // If the packageId changed that means the bytecode changed
99
+ // since the packageId is a hash of the bytecode.
100
+ if (packageId !== contractData.packageId) {
101
+ contractData.packageId = packageId; // Update packageId to new value
102
+ contractData.packageBytecode = packageBytecode; // Update bytecode
103
+ // If deployedPackageId exists, it means this contract was previously deployed.
104
+ // so we need to clear it to signal that a redeployment (upgrade) is needed.
105
+ // but also capture it so that we can perform upgrades
106
+ if (Is.stringValue(contractData.deployedPackageId)) {
107
+ contractData.lastDeployedPackageId = contractData.deployedPackageId;
108
+ // Clear deployedPackageId when bytecode changed, preserve upgrade metadata
109
+ delete contractData.deployedPackageId; // Clear to signal upgrade needed
110
+ }
111
+ finalJson[network] = contractData;
112
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.bytecodeChanged"), I18n.formatMessage("commands.build.labels.deployedPackageIdCleared"), 2);
113
+ }
114
+ else {
115
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.bytecodeUnchanged"), I18n.formatMessage("commands.build.labels.deployedPackageIdPreserved"), 2);
116
+ }
117
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.updatedNetworkPackage", { network }), contractName, 2);
118
+ }
119
+ }
120
+ catch (err) {
121
+ throw new GeneralError("commands", "commands.build.contractProcessingFailed", { file: moveFile }, err);
122
+ }
123
+ CLIDisplay.break();
124
+ }
125
+ CLIDisplay.task(I18n.formatMessage("commands.build.progress.writingJsonFile"));
126
+ await CLIUtils.writeJsonFile(normalizedOutput, finalJson, false);
127
+ CLIDisplay.break();
128
+ CLIDisplay.done();
129
+ }
130
+ catch (err) {
131
+ CLIDisplay.error(err);
132
+ throw err;
133
+ }
134
+ }
135
+ /**
136
+ * Process a single Move file by compiling it, computing the packageId, and base64-encoding the .mv modules.
137
+ * @param moveFile The path to a single Move source file.
138
+ * @returns The compiled results or undefined if no modules found.
139
+ */
140
+ async function processMoveFile(moveFile) {
141
+ // The contract name is based on the .move file's base name in kebab-case
142
+ const { name: baseName } = path.parse(moveFile);
143
+ const contractName = StringHelper.kebabCase(baseName);
144
+ // Find the "project root" (the directory containing Move.toml).
145
+ const projectRoot = getProjectRoot(moveFile);
146
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.contractName"), contractName, 1);
147
+ // Compile the contract
148
+ try {
149
+ const cliArgs = ["move", "build"];
150
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.compileCommand"), `iota ${cliArgs.join(" ")}`, 1);
151
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.workingDirectory"), projectRoot, 1);
152
+ await CLIUtils.runShellApp("iota", cliArgs, projectRoot);
153
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.compileResult"), I18n.formatMessage("commands.build.labels.buildCompleted"), 1);
154
+ }
155
+ catch (error) {
156
+ throw new GeneralError("commands", "commands.build.buildFailed", { file: moveFile }, error);
157
+ }
158
+ // Get the bytecode modules
159
+ const buildFolderName = StringHelper.snakeCase(baseName);
160
+ const bytecodeModulesPath = path.join(projectRoot, "build", buildFolderName, "bytecode_modules");
161
+ try {
162
+ await fsPromises.access(bytecodeModulesPath);
163
+ }
164
+ catch {
165
+ CLIDisplay.value(I18n.formatMessage("commands.build.warnings.noBytecodeModulesFolder", { contractName }), "", 2);
166
+ return;
167
+ }
168
+ const moduleFiles = await fsPromises.readdir(bytecodeModulesPath);
169
+ const mvFiles = moduleFiles.filter(f => f.endsWith(".mv"));
170
+ if (mvFiles.length === 0) {
171
+ CLIDisplay.value(I18n.formatMessage("commands.build.warnings.noMvFilesFound", { contractName }), "", 2);
172
+ return;
173
+ }
174
+ // Compute the package ID
175
+ const modulesBytesForHash = [];
176
+ const modulesBase64 = [];
177
+ for (const file of mvFiles) {
178
+ const modulePath = path.join(bytecodeModulesPath, file);
179
+ const moduleBytes = await fsPromises.readFile(modulePath);
180
+ modulesBytesForHash.push(moduleBytes);
181
+ modulesBase64.push(Converter.bytesToBase64(moduleBytes));
182
+ }
183
+ const concatenated = Buffer.concat(modulesBytesForHash);
184
+ const computedPackageIdBytes = Sha3.sum256(concatenated);
185
+ const computedPackageId = `${Converter.bytesToHex(computedPackageIdBytes, true)}`;
186
+ CLIDisplay.value(I18n.formatMessage("commands.build.labels.computedPackageId"), computedPackageId, 2);
187
+ // If multiple modules, store them as an array
188
+ const packageData = modulesBase64.length === 1 ? modulesBase64[0] : modulesBase64;
189
+ return {
190
+ contractName,
191
+ packageId: computedPackageId,
192
+ packageBytecode: packageData
193
+ };
194
+ }
195
+ /**
196
+ * Get the project root directory from a Move file path.
197
+ * @param moveFilePath The path to a Move source file.
198
+ * @returns The project root directory.
199
+ */
200
+ function getProjectRoot(moveFilePath) {
201
+ return path.resolve(path.parse(moveFilePath).dir, "..");
202
+ }
203
+ /**
204
+ * Normalize paths and resolve working directory for cross-platform compatibility.
205
+ * @param inputGlob The input glob pattern
206
+ * @param outputJson The output JSON file path
207
+ * @returns Normalized paths and working directory
208
+ */
209
+ function normalizePathsAndWorkingDir(inputGlob, outputJson) {
210
+ const executionDir = process.cwd();
211
+ // Improved path normalization with StringHelper
212
+ const normalizedGlob = StringHelper.trimTrailingSlashes(path.resolve(inputGlob).replace(/\\/g, "/"));
213
+ const normalizedOutput = path.resolve(outputJson);
214
+ return {
215
+ normalizedGlob,
216
+ normalizedOutput,
217
+ executionDir
218
+ };
219
+ }
220
+ //# sourceMappingURL=build.js.map