@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 +134 -0
- package/package.json +42 -0
- package/src/build-wallet.ts +101 -0
- package/src/constants.ts +20 -0
- package/src/deploy.ts +296 -0
- package/src/get-wallet-info.ts +995 -0
- package/src/midnight-env.ts +90 -0
- package/src/mnemonicToSeed.ts +19 -0
- package/src/mod.ts +29 -0
- package/src/providers.ts +116 -0
- package/src/read-contract.ts +439 -0
- package/src/types.ts +75 -0
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
}
|