@effectstream/midnight-contracts 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @effectstream/midnight
2
+
3
+ Utilities for working with Midnight contracts in EffectStream.
4
+
5
+ ## read-contract
6
+
7
+ Provides a context-aware function to read Midnight contract information from JSON files.
8
+
9
+ ### Usage
10
+
11
+ ```typescript
12
+ import { readMidnightContract } from "@effectstream/midnight-contracts/read-contract";
13
+
14
+ // Read contract for the current network (defaults to contract-counter.<networkId>.json)
15
+ const contractInfo = readMidnightContract("contract-counter");
16
+
17
+ // Read contract for another network (e.g., preview)
18
+ const previewInfo = readMidnightContract(
19
+ "contract-counter",
20
+ { networkId: "preview" },
21
+ );
22
+
23
+ // With explicit base directory (still respects network-specific files)
24
+ const customDirInfo = readMidnightContract(
25
+ "contract-counter",
26
+ { baseDir: "/path/to/contracts", networkId: "undeployed" },
27
+ );
28
+ ```
29
+
30
+ ### Features
31
+
32
+ - **Context-aware**: Automatically finds contract files by recursively searching from the current working directory upward through the directory tree
33
+ - **Flexible directory structure**: Works with any directory structure - no hardcoded paths required
34
+ - **Multiple contracts**: Supports reading multiple contracts with different filenames
35
+ - **Caching**: Results are cached per contract location, name, and filename combination
36
+ - **Dynamic compiler detection**: Automatically finds the compiler subdirectory in `src/managed/`
37
+
38
+ ### Function Signature
39
+
40
+ ```typescript
41
+ function readMidnightContract(
42
+ contractName: string,
43
+ contractFileName?: string,
44
+ options?: { baseDir?: string; networkId?: string }
45
+ ): MidnightContractInfo
46
+ ```
47
+
48
+ ### Parameters
49
+
50
+ - `contractName`: The name of the contract directory (e.g., 'contract-counter', 'contract-eip-20')
51
+ - `contractFileName`: Optional override. Defaults to `${contractName}.${networkId}.json`, so you can keep per-network files.
52
+ - `options`: Optional configuration object. `baseDir` overrides the search location, and `networkId` selects the file (default: `undeployed`).
53
+
54
+ ### Returns
55
+
56
+ `MidnightContractInfo` object containing:
57
+ - `contractAddress`: The deployed contract address
58
+ - `contractInfo`: Compiler-generated contract information (circuits, certificates, contracts)
59
+ - `zkConfigPath`: Path to the zk configuration directory
60
+
61
+ ## deploy
62
+
63
+ Deploys a Midnight contract using the provided configuration. This function is context-aware and automatically finds the contract directory and zkConfigPath.
64
+
65
+ ### Usage
66
+
67
+ ```typescript
68
+ import { deployMidnightContract, type DeployConfig, type NetworkUrls } from "@effectstream/midnight-contracts/deploy";
69
+
70
+ // Deploy with default network URLs (local undeployed endpoints)
71
+ const config: DeployConfig = {
72
+ contractName: "contract-counter",
73
+ contractFileName: "contract-counter.json",
74
+ contractClass: Counter.Contract,
75
+ witnesses,
76
+ privateStateId: "counterPrivateState",
77
+ initialPrivateState: { privateCounter: 0 },
78
+ };
79
+
80
+ const contractAddress = await deployMidnightContract(config);
81
+
82
+ // Deploy with custom network URLs
83
+ const networkUrls: NetworkUrls = {
84
+ indexer: "http://localhost:8088/api/v3/graphql",
85
+ indexerWS: "ws://localhost:8088/api/v3/graphql/ws",
86
+ node: "http://localhost:9944",
87
+ proofServer: "http://localhost:6300",
88
+ };
89
+
90
+ const contractAddress = await deployMidnightContract(config, networkUrls);
91
+ ```
92
+
93
+ ### Function Signature
94
+
95
+ ```typescript
96
+ function deployMidnightContract(
97
+ config: DeployConfig,
98
+ networkUrls?: NetworkUrls
99
+ ): Promise<string>
100
+ ```
101
+
102
+ ### Parameters
103
+
104
+ - `config`: Deployment configuration object containing:
105
+ - `contractName`: Name of the contract directory (e.g., "contract-counter", "contract-eip-20")
106
+ - `contractFileName`: Base filename for the contract address. The deployed file will append the network id (e.g., `contract-counter.undeployed.json`).
107
+ - `contractClass`: The Contract class to deploy
108
+ - `witnesses`: Witness definitions
109
+ - `privateStateId`: On-chain private state ID
110
+ - `initialPrivateState`: Initial private state object
111
+ - `deployArgs`: Optional deployment arguments array
112
+ - `privateStateStoreName`: Optional private state store name (defaults to contractName-based value)
113
+ - `baseDir`: Optional base directory override for finding contracts
114
+ - `logDir`: Optional log directory path
115
+ - `extractWalletAddress`: Optional flag to extract wallet address info (for contracts that need initialOwner)
116
+
117
+ - `networkUrls`: Optional network endpoint URLs. If not provided, defaults to local undeployed endpoints:
118
+ - `indexer`: GraphQL indexer HTTP endpoint (default: "http://127.0.0.1:8088/api/v3/graphql")
119
+ - `indexerWS`: GraphQL indexer WebSocket endpoint (default: "ws://127.0.0.1:8088/api/v3/graphql/ws")
120
+ - `node`: Midnight node RPC endpoint (default: "http://127.0.0.1:9944")
121
+ - `proofServer`: Proof server HTTP endpoint (default: "http://127.0.0.1:6300")
122
+
123
+ ### Returns
124
+
125
+ A Promise that resolves to the deployed contract address as a string.
126
+
127
+ ### Features
128
+
129
+ - **Context-aware**: Automatically finds contract directory by recursively searching from the current working directory
130
+ - **Flexible network configuration**: Use default local endpoints or provide custom network URLs
131
+ - **Automatic wallet setup**: Creates and funds a wallet automatically using the genesis mint seed
132
+ - **Dynamic compiler detection**: Automatically finds the compiler subdirectory in `src/managed/`
133
+ - **Contract address persistence**: Saves the deployed contract address to a JSON file
134
+
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@effectstream/midnight-contracts",
3
+ "version": "0.100.0",
4
+ "license": "MIT",
5
+ "exports": {
6
+ ".": "./src/mod.ts",
7
+ "./read-contract": "./src/read-contract.ts",
8
+ "./deploy": "./src/deploy.ts",
9
+ "./wallet-info": "./src/get-wallet-info.ts",
10
+ "./midnight-env": "./src/midnight-env.ts",
11
+ "./types": "./src/types.ts"
12
+ },
13
+ "dependencies": {
14
+ "@effectstream/utils": "0.100.0",
15
+ "@scure/bip39": "^2.0.1",
16
+ "dotenv": "^16.4.7",
17
+ "@midnight-ntwrk/wallet": "5.0.0",
18
+ "@midnight-ntwrk/wallet-api": "5.0.0",
19
+ "@midnight-ntwrk/zswap": "4.0.0",
20
+ "@midnight-ntwrk/ledger": "4.0.0",
21
+ "rxjs": "^7.8.2",
22
+ "@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
23
+ "@midnight-ntwrk/midnight-js-network-id": "4.0.2",
24
+ "@midnight-ntwrk/midnight-js-types": "4.0.2",
25
+ "@midnight-ntwrk/midnight-js-utils": "4.0.2",
26
+ "@midnight-ntwrk/onchain-runtime-v3": "3.0.0",
27
+ "@midnight-ntwrk/midnight-js-contracts": "4.0.2",
28
+ "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
29
+ "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
30
+ "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
31
+ "@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
32
+ "@midnight-ntwrk/wallet-sdk-abstractions": "2.0.0",
33
+ "@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
34
+ "@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
35
+ "@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
36
+ "@midnight-ntwrk/wallet-sdk-dust-wallet": "3.0.0",
37
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
38
+ "@midnight-ntwrk/ledger-v8": "8.0.3",
39
+ "@midnight-ntwrk/compact-js": "2.5.0",
40
+ "@midnight-ntwrk/wallet-sdk-capabilities": "3.2.0"
41
+ }
42
+ }
@@ -0,0 +1,101 @@
1
+ const log = console;
2
+ import { Buffer } from "node:buffer";
3
+
4
+ import type { NetworkId } from "@midnight-ntwrk/wallet-sdk-abstractions";
5
+ import { shieldedToken } from "@midnight-ntwrk/ledger-v8";
6
+ import type { WalletFacade } from "@midnight-ntwrk/wallet-sdk-facade";
7
+
8
+ import {
9
+ buildWalletFacade,
10
+ getInitialShieldedState,
11
+ resolveWalletSyncTimeoutMs,
12
+ syncAndWaitForFunds,
13
+ safeStringifyProgress
14
+ } from "./get-wallet-info.ts";
15
+
16
+ import type { WalletResult, NetworkUrls } from "./types.ts";
17
+ import type { InitialOwner } from "./types.ts";
18
+ import { getEnv } from "@effectstream/utils/runtime";
19
+
20
+ // ============================================================================
21
+ // Wallet Facade
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Build wallet and wait for funds
26
+ */
27
+ export async function buildWalletAndWaitForFunds(
28
+ networkUrls: Required<NetworkUrls>,
29
+ seed: string,
30
+ networkId: NetworkId.NetworkId
31
+ ): Promise<WalletResult> {
32
+ log.info("Building wallet using modular SDK");
33
+ const result = await buildWalletFacade(networkUrls, seed, networkId);
34
+
35
+ const initialState = await getInitialShieldedState(result.wallet.shielded);
36
+ const address = initialState.address.coinPublicKeyString();
37
+ log.info(`Wallet seed: ${seed}`);
38
+ log.info(`Wallet address: ${address}`);
39
+ log.info(`Dust address: ${result.dustAddress}`);
40
+
41
+ let balance = initialState.balances[shieldedToken().tag] ?? 0n;
42
+ console.log("initialState", safeStringifyProgress(initialState));
43
+ const syncTimeoutMs = resolveWalletSyncTimeoutMs();
44
+ if (balance === 0n) {
45
+ const skipWait =
46
+ getEnv("MIDNIGHT_SKIP_WAIT_FOR_FUNDS")?.toLowerCase() === "true";
47
+ log.info("Wallet shielded balance: 0");
48
+ log.info(
49
+ `Waiting to receive tokens... (timeout ${syncTimeoutMs}ms${skipWait ? ", skip on timeout enabled" : ""})`
50
+ );
51
+ try {
52
+ const { shieldedBalance, unshieldedBalance } = await syncAndWaitForFunds(
53
+ result.wallet
54
+ );
55
+ balance = shieldedBalance;
56
+ if (unshieldedBalance > 0n) {
57
+ log.info(`Unshielded balance available: ${unshieldedBalance}`);
58
+ }
59
+ } catch (e) {
60
+ if (skipWait) {
61
+ log.warn(
62
+ `Skipping wait for shielded funds after timeout: ${(e as Error).message}`
63
+ );
64
+ } else {
65
+ throw e;
66
+ }
67
+ }
68
+ }
69
+ log.info(`Wallet balance: ${balance}`);
70
+
71
+ // Dust syncing is handled by syncAndWaitForFunds above
72
+ return result;
73
+ }
74
+
75
+
76
+
77
+ // ============================================================================
78
+ // Contract Deployment Helpers
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Extract initial owner from wallet for contracts that need it (e.g., EIP-20)
83
+ */
84
+ export async function extractInitialOwnerFromWallet(
85
+ wallet: WalletFacade
86
+ ): Promise<InitialOwner> {
87
+ const initialState = await getInitialShieldedState(wallet.shielded);
88
+ const coinPubHex = initialState.address.coinPublicKeyString();
89
+ const encPubHex = initialState.address.encryptionPublicKeyString();
90
+ log.info(`Extracting initial owner from wallet keys (hex): coin=${coinPubHex}`);
91
+ log.info(`Encryption key (hex): ${encPubHex}`);
92
+
93
+ const coinBytes = Buffer.from(coinPubHex, "hex");
94
+ const encBytes = Buffer.from(encPubHex, "hex");
95
+
96
+ return {
97
+ is_left: true,
98
+ left: { bytes: coinBytes },
99
+ right: { bytes: encBytes.subarray(0, 32) },
100
+ };
101
+ }
@@ -0,0 +1,20 @@
1
+ // ============================================================================
2
+ // Constants
3
+ // ============================================================================
4
+
5
+ export const CONSTANTS = {
6
+ /** Transaction TTL duration in milliseconds (1 hour) */
7
+ TTL_DURATION_MS: 60 * 60 * 1000,
8
+
9
+ /** Wallet sync progress logging throttle interval */
10
+ WALLET_SYNC_THROTTLE_MS: 10_000,
11
+
12
+ /** Wallet sync timeout (10 minutes) */
13
+ WALLET_SYNC_TIMEOUT_MS: 600_000,
14
+
15
+ /** Additional fee overhead for dust transactions (in smallest unit) */
16
+ DUST_FEE_OVERHEAD: 300_000_000_000_000n,
17
+
18
+ /** Fee blocks margin for dust wallet */
19
+ DUST_FEE_BLOCKS_MARGIN: 5,
20
+ } as const;
package/src/deploy.ts ADDED
@@ -0,0 +1,296 @@
1
+ // TODO Remove references to "src/managed" as this is not standard.
2
+
3
+ import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
4
+ import { Buffer } from "node:buffer";
5
+ import { statSync, readdirSync } from "node:fs";
6
+ import { writeFile } from "node:fs/promises";
7
+ import { getEnv, cwd } from "@effectstream/utils/runtime";
8
+ import { deployContract } from "@midnight-ntwrk/midnight-js-contracts";
9
+ import type { PrivateStateId } from "@midnight-ntwrk/midnight-js-types";
10
+ import { CompiledContract, type Witnesses, type Contract } from "@midnight-ntwrk/compact-js";
11
+ import type { SigningKey } from "@midnight-ntwrk/ledger-v8";
12
+ import type { NetworkId } from "@midnight-ntwrk/wallet-sdk-abstractions";
13
+ import * as path from "node:path";
14
+
15
+ const log = console;
16
+
17
+ import { findContractDirectoryForDeploy } from "./read-contract.ts";
18
+ import { mnemonicToSeed } from "./mnemonicToSeed.ts";
19
+ import type { NetworkUrls, DeployConfig, WalletResult } from "./types.ts";
20
+ import { buildWalletAndWaitForFunds, extractInitialOwnerFromWallet } from "./build-wallet.ts";
21
+ import { configureMidnightNodeProviders } from "./providers.ts";
22
+ import { midnightNetworkConfig } from "./midnight-env.ts";
23
+
24
+ function checkEnvVariables(): void {
25
+ if (!getEnv("MIDNIGHT_STORAGE_PASSWORD")) {
26
+ throw new Error(
27
+ "MIDNIGHT_STORAGE_PASSWORD is not set (Use a 16 char string)",
28
+ );
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Find the compiler subdirectory in the managed directory
34
+ */
35
+ function hasManagedArtifacts(dir: string): boolean {
36
+ const requiredDirs = ["contract", "compiler"];
37
+ try {
38
+ return requiredDirs.every((name) => {
39
+ const stats = statSync(path.join(dir, name));
40
+ return stats.isDirectory();
41
+ });
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function findCompilerSubdirectory(managedDir: string): string {
48
+ // Check the managed directory itself first before looking at subdirectories.
49
+ // This prevents accidentally picking a nested sub-contract (e.g. counter/)
50
+ // that only contains a subset of the circuit keys.
51
+ if (hasManagedArtifacts(managedDir)) {
52
+ return "";
53
+ }
54
+
55
+ try {
56
+ for (const entry of readdirSync(managedDir, { withFileTypes: true })) {
57
+ if (!entry.isDirectory()) continue;
58
+ const candidate = path.join(managedDir, entry.name);
59
+ if (hasManagedArtifacts(candidate)) {
60
+ return entry.name;
61
+ }
62
+ }
63
+ } catch (_error) {
64
+ throw new Error(`Managed directory not found: ${managedDir}`);
65
+ }
66
+
67
+ throw new Error(
68
+ `No compiler artifacts found in managed directory: ${managedDir}. ` +
69
+ `Ensure the directory contains compiler, contract, keys, and zkir assets.`,
70
+ );
71
+ }
72
+
73
+ // ============================================================================
74
+ // Main Deployment Function
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Deploys a Midnight contract using the provided configuration.
79
+ *
80
+ * This function is context-aware and will find the contract directory
81
+ * and zkConfigPath automatically using readMidnightContract.
82
+ *
83
+ * @param config - Deployment configuration
84
+ * @param networkUrls - Optional network endpoint URLs (defaults to local undeployed endpoints)
85
+ * @returns The deployed contract address
86
+ */
87
+ export async function deployMidnightContract(
88
+ config: DeployConfig,
89
+ networkUrls?: NetworkUrls,
90
+ seedOrMnemonic?: { seed: string; mnemonic: string },
91
+ ): Promise<string> {
92
+ checkEnvVariables();
93
+
94
+ // Find the contract directory
95
+ const contractDir = findContractDirectoryForDeploy(
96
+ config.contractName,
97
+ config.baseDir,
98
+ );
99
+
100
+ if (!contractDir) {
101
+ throw new Error(
102
+ `Could not find Midnight contract directory for "${config.contractName}". ` +
103
+ `Searched starting from ${config.baseDir || cwd()}. ` +
104
+ `Please ensure you're running from a directory that contains or is a parent of the Midnight contract directory, ` +
105
+ `or provide an explicit baseDir parameter.`,
106
+ );
107
+ }
108
+
109
+ // Find the compiler subdirectory to determine zkConfigPath
110
+ const managedDir = path.join(contractDir, config.contractName, "src/managed");
111
+ const compilerSubdir = findCompilerSubdirectory(managedDir);
112
+
113
+ const zkConfigPath = path.resolve(
114
+ path.join(contractDir, config.contractName, "src/managed", compilerSubdir),
115
+ );
116
+
117
+ // Default private state store name if not provided
118
+ const privateStateStoreName =
119
+ config.privateStateStoreName ??
120
+ `${config.contractName.replace("contract-", "")}-private-state`;
121
+
122
+ // Merge network URLs with defaults
123
+ const { id: networkIdOverride, ...endpoints } = networkUrls ?? {};
124
+ const resolvedNetworkUrls: Required<NetworkUrls> = {
125
+ id: 'placeholder-value',
126
+ indexer: endpoints.indexer ?? midnightNetworkConfig.indexer,
127
+ indexerWS: endpoints.indexerWS ?? midnightNetworkConfig.indexerWS,
128
+ node: endpoints.node ?? midnightNetworkConfig.node,
129
+ proofServer: endpoints.proofServer ?? midnightNetworkConfig.proofServer,
130
+ };
131
+ const resolvedNetworkId = (networkIdOverride ??
132
+ midnightNetworkConfig.id) as NetworkId.NetworkId;
133
+
134
+ log.info(
135
+ `Preflight resolved endpoints -> indexerHttp=${resolvedNetworkUrls.indexer}, indexerWs=${resolvedNetworkUrls.indexerWS}, node=${resolvedNetworkUrls.node}, proofServer=${resolvedNetworkUrls.proofServer}, networkId=${resolvedNetworkId}`,
136
+ );
137
+
138
+ setNetworkId(resolvedNetworkId);
139
+
140
+ let walletResult: WalletResult | null = null;
141
+ let providers: ReturnType<typeof configureMidnightNodeProviders> | null = null;
142
+
143
+ try {
144
+ log.info("Building wallet...");
145
+ let seed;
146
+ if (seedOrMnemonic?.seed) {
147
+ seed = seedOrMnemonic.seed;
148
+ } else if (seedOrMnemonic?.mnemonic) {
149
+ seed = Buffer.from(
150
+ await mnemonicToSeed(seedOrMnemonic.mnemonic),
151
+ ).toString("hex");
152
+ } else {
153
+ seed = midnightNetworkConfig.walletSeed;
154
+ }
155
+ if (!seed) {
156
+ throw new Error("No seed or mnemonic provided");
157
+ }
158
+
159
+ walletResult = await buildWalletAndWaitForFunds(
160
+ resolvedNetworkUrls,
161
+ seed,
162
+ resolvedNetworkId,
163
+ );
164
+
165
+ const {
166
+ wallet,
167
+ zswapSecretKeys,
168
+ walletZswapSecretKeys,
169
+ dustSecretKey,
170
+ walletDustSecretKey,
171
+ dustAddress,
172
+ unshieldedKeystore,
173
+ } = walletResult;
174
+ const resolvedDustReceiverAddress =
175
+ getEnv("MIDNIGHT_DUST_RECEIVER_ADDRESS") ?? dustAddress;
176
+ if (resolvedDustReceiverAddress === dustAddress) {
177
+ log.info(`Using derived dust address: ${resolvedDustReceiverAddress}`);
178
+ } else {
179
+ log.info(
180
+ `Using dust receiver address from MIDNIGHT_DUST_RECEIVER_ADDRESS: ${resolvedDustReceiverAddress}`,
181
+ );
182
+ }
183
+
184
+ // Extract wallet address info if needed (for contracts like EIP-20)
185
+ let deployArgs = config.deployArgs;
186
+ if (config.extractWalletAddress && deployArgs && deployArgs.length > 0) {
187
+ const initialOwner = await extractInitialOwnerFromWallet(wallet);
188
+ deployArgs = [...deployArgs.slice(0, -1), initialOwner];
189
+ }
190
+
191
+ log.info("Wallet built successfully.");
192
+
193
+ log.info("Configuring providers...");
194
+ // Use a separate LevelDB directory for deployment to avoid lock conflicts with batcher
195
+ const deployPrivateStateStoreName = `${privateStateStoreName}-deploy`;
196
+
197
+ providers = configureMidnightNodeProviders(
198
+ wallet,
199
+ zswapSecretKeys,
200
+ walletZswapSecretKeys,
201
+ dustSecretKey,
202
+ walletDustSecretKey,
203
+ resolvedNetworkUrls,
204
+ deployPrivateStateStoreName,
205
+ zkConfigPath,
206
+ unshieldedKeystore,
207
+ );
208
+ log.info("Providers configured.");
209
+
210
+ log.info("Deploying contract...");
211
+
212
+ // First, create the compiled contract
213
+ const MyCompiledContract = CompiledContract.make(
214
+ config.contractName,
215
+ config.contractClass,
216
+ ).pipe(
217
+ CompiledContract.withWitnesses(config.witnesses as never),
218
+ CompiledContract.withCompiledFileAssets(managedDir),
219
+ );
220
+
221
+ const deployOptions: {
222
+ compiledContract: CompiledContract.CompiledContract<
223
+ Contract<undefined, Witnesses<undefined>>,
224
+ undefined,
225
+ never
226
+ >;
227
+ privateStateId: PrivateStateId;
228
+ initialPrivateState: Contract.PrivateState<any>;
229
+ signingKey?: SigningKey;
230
+ args: Contract.InitializeParameters<any>;
231
+ } = {
232
+ compiledContract: MyCompiledContract as any,
233
+ privateStateId: config.privateStateId as PrivateStateId,
234
+ initialPrivateState:
235
+ config.initialPrivateState as Contract.PrivateState<any>,
236
+ args: (deployArgs && deployArgs.length > 0
237
+ ? deployArgs
238
+ : []) as Contract.InitializeParameters<any>,
239
+ signingKey: undefined,
240
+ };
241
+
242
+ const deployedContract = await deployContract(providers, deployOptions);
243
+ log.info("Contract deployed.");
244
+
245
+ const contractAddress =
246
+ deployedContract.deployTxData.public.contractAddress;
247
+ log.info(`Contract address: ${contractAddress}`);
248
+
249
+ const baseContractFileName =
250
+ config.contractFileName ?? `${config.contractName}.json`;
251
+ const {
252
+ dir: contractFileDir,
253
+ name: contractFileBaseName,
254
+ ext: contractFileExt,
255
+ } = path.parse(baseContractFileName);
256
+ const normalizedExt = contractFileExt || ".json";
257
+ const networkSuffix = `.${resolvedNetworkId}`;
258
+ const fileBaseWithNetwork = contractFileBaseName.endsWith(networkSuffix)
259
+ ? contractFileBaseName
260
+ : `${contractFileBaseName}${networkSuffix}`;
261
+ const outputFileName = `${fileBaseWithNetwork}${normalizedExt}`;
262
+ const outputPath = path.join(contractDir, contractFileDir, outputFileName);
263
+
264
+ await writeFile(outputPath, JSON.stringify({ contractAddress }, null, 2));
265
+ log.info(
266
+ `Contract address saved to ${outputPath} (network: ${resolvedNetworkId})`,
267
+ );
268
+
269
+ return contractAddress;
270
+ } catch (e) {
271
+ if (e instanceof Error) {
272
+ log.error(`Deployment failed: ${e.message}`);
273
+ log.error(e.stack);
274
+ } else {
275
+ log.error("An unknown error occurred during deployment.");
276
+ }
277
+ throw e;
278
+ } finally {
279
+ // Close wallet first
280
+ if (walletResult) {
281
+ log.info("Closing wallet...");
282
+ try {
283
+ await walletResult.wallet.stop();
284
+ } catch (_closeError) {
285
+ // Ignore close errors
286
+ }
287
+ log.info("Wallet closed.");
288
+ }
289
+
290
+ // Wait a moment for Level DB to finish any async close operations
291
+ // The levelPrivateStateProvider opens/closes DB for each operation in withSubLevel
292
+ // But there might be pending async operations
293
+ log.info("Waiting for Level DB cleanup...");
294
+ await new Promise((resolve) => setTimeout(resolve, 500));
295
+ }
296
+ }