@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { NetworkId } from "@midnight-ntwrk/wallet-sdk-abstractions";
|
|
2
|
+
import { mnemonicToSeed } from "@scure/bip39";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { getEnv } from "@effectstream/utils/runtime";
|
|
5
|
+
|
|
6
|
+
const getEnvValue = (key: string): string | undefined => {
|
|
7
|
+
return getEnv(key);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const env = (key: string | string[], fallback?: string): string => {
|
|
11
|
+
if (typeof key === 'string') {
|
|
12
|
+
return getEnvValue(key)?.trim() || fallback || "";
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(key)) {
|
|
15
|
+
return key.map((k) => getEnvValue(k)?.trim()).find((value) => !!value) ||
|
|
16
|
+
fallback ||
|
|
17
|
+
"";
|
|
18
|
+
}
|
|
19
|
+
throw new Error('Invalid key type');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type NetworkConfig = {
|
|
23
|
+
indexer: string;
|
|
24
|
+
indexerWS: string;
|
|
25
|
+
node: string;
|
|
26
|
+
proofServer: string;
|
|
27
|
+
networkId: NetworkId.NetworkId;
|
|
28
|
+
genesisWalletSeed: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Midnight Network default configurations
|
|
32
|
+
const undeployedNetworkConfig: NetworkConfig = {
|
|
33
|
+
indexer: "http://127.0.0.1:8088/api/v3/graphql",
|
|
34
|
+
indexerWS: "ws://127.0.0.1:8088/api/v3/graphql/ws",
|
|
35
|
+
node: "http://127.0.0.1:9944",
|
|
36
|
+
networkId: "undeployed" as NetworkId.NetworkId,
|
|
37
|
+
proofServer: "http://127.0.0.1:6300",
|
|
38
|
+
// In local mode, the Genesis Wallet Seed determines the initial funded wallet
|
|
39
|
+
genesisWalletSeed: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
const deployedNetworkConfig = (networkId: NetworkId.NetworkId): NetworkConfig => ({
|
|
43
|
+
indexer: `https://indexer.${networkId}.midnight.network/api/v3/graphql`,
|
|
44
|
+
indexerWS: `wss://indexer.${networkId}.midnight.network/api/v3/graphql/ws`,
|
|
45
|
+
node: `https://rpc.${networkId}.midnight.network`,
|
|
46
|
+
proofServer: "http://127.0.0.1:6300",
|
|
47
|
+
networkId,
|
|
48
|
+
genesisWalletSeed: '',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const networkId = env("MIDNIGHT_NETWORK_ID") || "undeployed";
|
|
52
|
+
let selectedNetworkConfig: NetworkConfig;
|
|
53
|
+
switch (networkId) {
|
|
54
|
+
case "undeployed":
|
|
55
|
+
selectedNetworkConfig = undeployedNetworkConfig;
|
|
56
|
+
break;
|
|
57
|
+
case "mainnet":
|
|
58
|
+
case "testnet":
|
|
59
|
+
case "devnet":
|
|
60
|
+
case "qanet":
|
|
61
|
+
case "preview":
|
|
62
|
+
case "preprod":
|
|
63
|
+
default:
|
|
64
|
+
selectedNetworkConfig = deployedNetworkConfig(networkId as NetworkId.NetworkId);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let walletSeed: string;
|
|
69
|
+
if (env("MIDNIGHT_WALLET_SEED")) {
|
|
70
|
+
walletSeed = env("MIDNIGHT_WALLET_SEED");
|
|
71
|
+
} else if (env("MIDNIGHT_WALLET_MNEMONIC")) {
|
|
72
|
+
walletSeed = Buffer.from(await mnemonicToSeed(env("MIDNIGHT_WALLET_MNEMONIC"))).toString('hex');
|
|
73
|
+
} else {
|
|
74
|
+
walletSeed = selectedNetworkConfig.genesisWalletSeed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const midnightNetworkConfig = {
|
|
78
|
+
id: selectedNetworkConfig.networkId,
|
|
79
|
+
indexer: env("MIDNIGHT_INDEXER_HTTP", selectedNetworkConfig.indexer),
|
|
80
|
+
indexerWS: env("MIDNIGHT_INDEXER_WS", selectedNetworkConfig.indexerWS),
|
|
81
|
+
node: env("MIDNIGHT_NODE_HTTP", selectedNetworkConfig.node),
|
|
82
|
+
proofServer: env(["MIDNIGHT_PROOF_SERVER_URL", "MIDNIGHT_PROOF_SERVER"], selectedNetworkConfig.proofServer),
|
|
83
|
+
walletSeed,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isLocalProofServer = !!midnightNetworkConfig.proofServer.match(/(localhost|127\.0\.0\.1)/);
|
|
87
|
+
export const isExternalProofServerConfigured = !isLocalProofServer;
|
|
88
|
+
|
|
89
|
+
// Set this using MIDNIGHT_NETWORK_ID=<network-id>
|
|
90
|
+
export type MidnightNetworkConfig = typeof midnightNetworkConfig;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as bip39 from '@scure/bip39';
|
|
2
|
+
import { wordlist as english } from '@scure/bip39/wordlists/english.js';
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
|
|
5
|
+
// Use a BIP39 mnemonic to derive a seed.
|
|
6
|
+
// This is compatible with Lace Wallets.
|
|
7
|
+
export const mnemonicToSeed = async (mnemonic: string): Promise<Buffer> => {
|
|
8
|
+
const words = mnemonic.trim().split(/\s+/);
|
|
9
|
+
if (!bip39.validateMnemonic(words.join(' '), english)) {
|
|
10
|
+
throw new Error('Invalid mnemonic phrase');
|
|
11
|
+
}
|
|
12
|
+
// Use BIP39 standard seed derivation (PBKDF2)
|
|
13
|
+
// Produces 64 bytes.
|
|
14
|
+
// hashes it (mixes it up) 2048 times using SHA-512
|
|
15
|
+
const seed = await bip39.mnemonicToSeed(words.join(' '));
|
|
16
|
+
return Buffer.from(seed);
|
|
17
|
+
// As hex string seed:
|
|
18
|
+
// Buffer.from(await mnemonicToSeed(mnemonic)).toString('hex')
|
|
19
|
+
};
|
package/src/mod.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { deployMidnightContract } from "./deploy.ts";
|
|
2
|
+
export type {
|
|
3
|
+
DeployConfig,
|
|
4
|
+
NetworkUrls,
|
|
5
|
+
WalletResult,
|
|
6
|
+
InitialOwner,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
export {
|
|
9
|
+
resolveWalletSyncTimeoutMs,
|
|
10
|
+
safeStringifyProgress,
|
|
11
|
+
syncAndWaitForFunds,
|
|
12
|
+
buildWalletFacade,
|
|
13
|
+
getInitialShieldedState,
|
|
14
|
+
registerNightForDust,
|
|
15
|
+
getInitialDustState,
|
|
16
|
+
waitForDustFunds,
|
|
17
|
+
waitForDustFundsWithRetry,
|
|
18
|
+
saveDustState,
|
|
19
|
+
loadDustState,
|
|
20
|
+
} from "./get-wallet-info.ts";
|
|
21
|
+
export type { WalletSyncMode, DustSyncWithRetryOptions } from "./get-wallet-info.ts";
|
|
22
|
+
export { CONSTANTS } from "./constants.ts";
|
|
23
|
+
export {
|
|
24
|
+
buildWalletAndWaitForFunds,
|
|
25
|
+
extractInitialOwnerFromWallet,
|
|
26
|
+
} from "./build-wallet.ts";
|
|
27
|
+
export { readMidnightContract } from "./read-contract.ts";
|
|
28
|
+
export { configureMidnightNodeProviders } from "./providers.ts";
|
|
29
|
+
export { midnightNetworkConfig } from "./midnight-env.ts";
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Provider Configuration
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { Buffer } from "node:buffer";
|
|
6
|
+
import type { ZswapSecretKeys, DustSecretKey, CoinPublicKey, EncPublicKey, FinalizedTransaction, TransactionId } from "@midnight-ntwrk/ledger-v8";
|
|
7
|
+
import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider";
|
|
8
|
+
import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
|
|
9
|
+
import { levelPrivateStateProvider } from "@midnight-ntwrk/midnight-js-level-private-state-provider";
|
|
10
|
+
import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider";
|
|
11
|
+
import type { WalletProvider, MidnightProvider, MidnightProviders, UnboundTransaction } from "@midnight-ntwrk/midnight-js-types";
|
|
12
|
+
import type { WalletFacade } from "@midnight-ntwrk/wallet-sdk-facade";
|
|
13
|
+
import type { NetworkUrls } from "./types.ts";
|
|
14
|
+
import type { UnshieldedKeystore } from "@midnight-ntwrk/wallet-sdk-unshielded-wallet";
|
|
15
|
+
import { CONSTANTS } from "./constants.ts";
|
|
16
|
+
// import { Contract } from "@midnight-ntwrk/compact-js";
|
|
17
|
+
import { getEnv } from "@effectstream/utils/runtime";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a TTL date for transactions
|
|
21
|
+
*/
|
|
22
|
+
function createTtl(): Date {
|
|
23
|
+
return new Date(Date.now() + CONSTANTS.TTL_DURATION_MS);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create wallet and midnight provider adapter for WalletFacade
|
|
28
|
+
*
|
|
29
|
+
* Implements the WalletProvider and MidnightProvider interfaces
|
|
30
|
+
* as defined in @midnight-ntwrk/midnight-js-types v3.x
|
|
31
|
+
*/
|
|
32
|
+
function createWalletAndMidnightProvider(
|
|
33
|
+
wallet: WalletFacade,
|
|
34
|
+
zswapSecretKeys: ZswapSecretKeys,
|
|
35
|
+
walletZswapSecretKeys: ZswapSecretKeys,
|
|
36
|
+
dustSecretKey: DustSecretKey,
|
|
37
|
+
walletDustSecretKey: DustSecretKey,
|
|
38
|
+
unshieldedKeystore: UnshieldedKeystore
|
|
39
|
+
): WalletProvider & MidnightProvider {
|
|
40
|
+
return {
|
|
41
|
+
getCoinPublicKey(): CoinPublicKey {
|
|
42
|
+
console.log("✅ Getting coin public key", zswapSecretKeys.coinPublicKey);
|
|
43
|
+
return zswapSecretKeys.coinPublicKey;
|
|
44
|
+
},
|
|
45
|
+
getEncryptionPublicKey(): EncPublicKey {
|
|
46
|
+
console.log("✅ Getting encryption public key", zswapSecretKeys.encryptionPublicKey);
|
|
47
|
+
return zswapSecretKeys.encryptionPublicKey;
|
|
48
|
+
},
|
|
49
|
+
async balanceTx(
|
|
50
|
+
tx: UnboundTransaction,
|
|
51
|
+
ttl?: Date
|
|
52
|
+
): Promise<FinalizedTransaction> {
|
|
53
|
+
console.log("✅ Balancing transaction", tx);
|
|
54
|
+
const bound = tx.bind();
|
|
55
|
+
const finalizedTransactionRecipe = await wallet.balanceFinalizedTransaction(bound, {
|
|
56
|
+
shieldedSecretKeys: zswapSecretKeys,
|
|
57
|
+
dustSecretKey: dustSecretKey,
|
|
58
|
+
}, { ttl: ttl ?? createTtl() } );
|
|
59
|
+
const x = await wallet.signRecipe(finalizedTransactionRecipe, (payload) => unshieldedKeystore.signData(payload));
|
|
60
|
+
return wallet.finalizeRecipe(x);
|
|
61
|
+
},
|
|
62
|
+
submitTx(tx: FinalizedTransaction): Promise<TransactionId> {
|
|
63
|
+
console.log("✅ Submitting transaction", tx);
|
|
64
|
+
return wallet.submitTransaction(tx);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Configure all providers needed for contract deployment
|
|
71
|
+
*/
|
|
72
|
+
export function configureMidnightNodeProviders(
|
|
73
|
+
wallet: WalletFacade,
|
|
74
|
+
zswapSecretKeys: ZswapSecretKeys,
|
|
75
|
+
walletZswapSecretKeys: ZswapSecretKeys,
|
|
76
|
+
dustSecretKey: DustSecretKey,
|
|
77
|
+
walletDustSecretKey: DustSecretKey,
|
|
78
|
+
networkUrls: Required<Omit<NetworkUrls, "id">>,
|
|
79
|
+
privateStateStoreName: string,
|
|
80
|
+
zkConfigPath: string,
|
|
81
|
+
unshieldedKeystore: UnshieldedKeystore
|
|
82
|
+
): MidnightProviders {
|
|
83
|
+
const signingKeyStoreName = `${privateStateStoreName}-signing-keys`;
|
|
84
|
+
const walletAndMidnightProvider = createWalletAndMidnightProvider(
|
|
85
|
+
wallet,
|
|
86
|
+
zswapSecretKeys,
|
|
87
|
+
walletZswapSecretKeys,
|
|
88
|
+
dustSecretKey,
|
|
89
|
+
walletDustSecretKey,
|
|
90
|
+
unshieldedKeystore
|
|
91
|
+
);
|
|
92
|
+
const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath);
|
|
93
|
+
return {
|
|
94
|
+
// For deployment, we use full private state config because we may need to verify
|
|
95
|
+
// the deployed contract state. For batcher/transaction submission use cases,
|
|
96
|
+
// a minimal config with just walletProvider is sufficient and much faster:
|
|
97
|
+
// levelPrivateStateProvider({ walletProvider })
|
|
98
|
+
// Omitting privateStateStoreName/midnightDbName avoids historical private state sync.
|
|
99
|
+
privateStateProvider: levelPrivateStateProvider({
|
|
100
|
+
midnightDbName: "midnight-level-db-deploy", // Use separate DB for deployment to avoid lock conflicts
|
|
101
|
+
privateStateStoreName,
|
|
102
|
+
signingKeyStoreName,
|
|
103
|
+
// walletProvider: walletAndMidnightProvider, // Use wallet's encryption key for private state
|
|
104
|
+
privateStoragePasswordProvider: async () => getEnv("MIDNIGHT_STORAGE_PASSWORD") ?? "YourPasswordMy1!",
|
|
105
|
+
accountId: Buffer.from(zswapSecretKeys.coinPublicKey).toString('hex'),
|
|
106
|
+
}), // Type assertion: runtime supports walletProvider even though types don't reflect it yet
|
|
107
|
+
publicDataProvider: indexerPublicDataProvider(
|
|
108
|
+
networkUrls.indexer,
|
|
109
|
+
networkUrls.indexerWS
|
|
110
|
+
),
|
|
111
|
+
zkConfigProvider,
|
|
112
|
+
proofProvider: httpClientProofProvider(networkUrls.proofServer, zkConfigProvider),
|
|
113
|
+
walletProvider: walletAndMidnightProvider,
|
|
114
|
+
midnightProvider: walletAndMidnightProvider,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// TODO Remove references to "src/managed" as this is not standard.
|
|
2
|
+
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
5
|
+
import { getEnv, cwd, isNotFoundError, getRuntime } from "@effectstream/utils/runtime";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Information about a compiled Midnight contract
|
|
9
|
+
*/
|
|
10
|
+
export type MidnightContractCompilerInfo = {
|
|
11
|
+
/** Circuit definitions for the contract */
|
|
12
|
+
circuits: any[];
|
|
13
|
+
/** Witness definitions for the contract */
|
|
14
|
+
witnesses: any[];
|
|
15
|
+
/** Contract definitions */
|
|
16
|
+
contracts: any[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Address information for a deployed contract
|
|
21
|
+
* Supports both legacy format (string) and new format (network-keyed object)
|
|
22
|
+
*/
|
|
23
|
+
export type MidnightContractAddressInfo = {
|
|
24
|
+
/** The deployed contract address - can be a string (legacy) or network-keyed object */
|
|
25
|
+
contractAddress: string
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Complete contract information combining address and compiler info
|
|
30
|
+
*/
|
|
31
|
+
export type MidnightContractInfo = {
|
|
32
|
+
/** The deployed contract address for the specified network */
|
|
33
|
+
contractAddress: string;
|
|
34
|
+
/** Compiler-generated contract information */
|
|
35
|
+
contractInfo: MidnightContractCompilerInfo;
|
|
36
|
+
zkConfigPath: string;
|
|
37
|
+
/** Directory where the contract file is located */
|
|
38
|
+
contractDir: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const cachedContractInfo: Record<string, MidnightContractInfo> = {};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Recursively searches for all files matching a name in a directory and its subdirectories.
|
|
45
|
+
* @param dir - The directory to search in
|
|
46
|
+
* @param fileName - The file name to search for
|
|
47
|
+
* @param maxDepth - Maximum depth to search (default: 5)
|
|
48
|
+
* @param currentDepth - Current search depth (internal use)
|
|
49
|
+
* @returns Array of directories containing matching files
|
|
50
|
+
*/
|
|
51
|
+
function findAllFilesRecursive(
|
|
52
|
+
dir: string,
|
|
53
|
+
fileName: string,
|
|
54
|
+
maxDepth: number = 5,
|
|
55
|
+
currentDepth: number = 0
|
|
56
|
+
): string[] {
|
|
57
|
+
const results: string[] = [];
|
|
58
|
+
|
|
59
|
+
if (currentDepth > maxDepth) {
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
65
|
+
|
|
66
|
+
// First check if the file exists in the current directory
|
|
67
|
+
const hasFile = entries.some(entry =>
|
|
68
|
+
entry.isFile() && entry.name === fileName
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (hasFile) {
|
|
72
|
+
results.push(dir);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Then recursively search subdirectories
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
// Skip common directories that are unlikely to contain contracts
|
|
79
|
+
const skipDirs = ["node_modules", ".git", "dist", "build", ".deno"];
|
|
80
|
+
if (skipDirs.includes(entry.name)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const subDir = path.join(dir, entry.name);
|
|
85
|
+
const subResults = findAllFilesRecursive(subDir, fileName, maxDepth, currentDepth + 1);
|
|
86
|
+
results.push(...subResults);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Can't read directory, skip it
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validates that a directory contains a valid Midnight contract structure.
|
|
98
|
+
* Checks if the contractName directory with src/managed/ exists.
|
|
99
|
+
*/
|
|
100
|
+
function isValidMidnightContractDir(dir: string, contractName: string): boolean {
|
|
101
|
+
const managedDir = path.join(dir, contractName, "src/managed");
|
|
102
|
+
try {
|
|
103
|
+
const stats = statSync(managedDir);
|
|
104
|
+
return stats.isDirectory();
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Determine whether a directory contains the expected Midnight compiler artifacts.
|
|
112
|
+
*/
|
|
113
|
+
function hasManagedArtifacts(dir: string): boolean {
|
|
114
|
+
const requiredDirs = ["compiler", "contract"];
|
|
115
|
+
try {
|
|
116
|
+
return requiredDirs.every((entry) => {
|
|
117
|
+
const stats = statSync(path.join(dir, entry));
|
|
118
|
+
return stats.isDirectory();
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolves the directory that actually holds the managed compiler artifacts.
|
|
127
|
+
* Supports both nested structures (src/managed/<name>/...) and flattened ones
|
|
128
|
+
* (src/managed/...).
|
|
129
|
+
*/
|
|
130
|
+
function resolveManagedArtifactsDir(managedDir: string): string {
|
|
131
|
+
try {
|
|
132
|
+
for (const entry of readdirSync(managedDir, { withFileTypes: true })) {
|
|
133
|
+
if (!entry.isDirectory()) continue;
|
|
134
|
+
const candidate = path.join(managedDir, entry.name);
|
|
135
|
+
if (hasManagedArtifacts(candidate)) {
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// handled below
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (hasManagedArtifacts(managedDir)) {
|
|
144
|
+
return managedDir;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Managed compiler artifacts not found under ${managedDir}. ` +
|
|
149
|
+
`Expected either src/managed/<subdir>/{contract,compiler,...} or ` +
|
|
150
|
+
`src/managed/{contract,compiler,...}.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Finds the directory containing a Midnight contract by searching for the contract directory structure.
|
|
156
|
+
* This is used during deployment when the contract.json file doesn't exist yet.
|
|
157
|
+
* Searches for directories matching {contractName}/src/managed/ pattern.
|
|
158
|
+
*/
|
|
159
|
+
export function findContractDirectoryForDeploy(
|
|
160
|
+
contractName: string,
|
|
161
|
+
baseDir?: string
|
|
162
|
+
): string | null {
|
|
163
|
+
let startDir: string;
|
|
164
|
+
|
|
165
|
+
if (baseDir) {
|
|
166
|
+
startDir = path.resolve(baseDir);
|
|
167
|
+
} else {
|
|
168
|
+
startDir = cwd();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let currentDir = path.resolve(startDir);
|
|
172
|
+
const root = path.parse(currentDir).root;
|
|
173
|
+
|
|
174
|
+
while (currentDir !== root) {
|
|
175
|
+
try {
|
|
176
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
177
|
+
|
|
178
|
+
// Check if contract directory exists here
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (entry.isDirectory() && entry.name === contractName) {
|
|
181
|
+
// Validate it's a Midnight contract by checking for src/managed/
|
|
182
|
+
if (isValidMidnightContractDir(currentDir, contractName)) {
|
|
183
|
+
return currentDir;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Also search recursively in subdirectories (up to 3 levels deep)
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
const skipDirs = ["node_modules", ".git", "dist", "build", ".deno"];
|
|
192
|
+
if (skipDirs.includes(entry.name)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const subDir = path.join(currentDir, entry.name);
|
|
198
|
+
const subEntries = readdirSync(subDir, { withFileTypes: true });
|
|
199
|
+
|
|
200
|
+
// Check direct subdirectory
|
|
201
|
+
for (const subEntry of subEntries) {
|
|
202
|
+
if (subEntry.isDirectory() && subEntry.name === contractName) {
|
|
203
|
+
if (isValidMidnightContractDir(subDir, contractName)) {
|
|
204
|
+
return subDir;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check one more level deep
|
|
210
|
+
for (const subEntry of subEntries) {
|
|
211
|
+
if (subEntry.isDirectory() && !skipDirs.includes(subEntry.name)) {
|
|
212
|
+
const subSubDir = path.join(subDir, subEntry.name);
|
|
213
|
+
try {
|
|
214
|
+
const subSubEntries = readdirSync(subSubDir, { withFileTypes: true });
|
|
215
|
+
for (const subSubEntry of subSubEntries) {
|
|
216
|
+
if (subSubEntry.isDirectory() && subSubEntry.name === contractName) {
|
|
217
|
+
if (isValidMidnightContractDir(subSubDir, contractName)) {
|
|
218
|
+
return subSubDir;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Skip if can't read
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Skip if can't read
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
currentDir = path.dirname(currentDir);
|
|
234
|
+
} catch {
|
|
235
|
+
currentDir = path.dirname(currentDir);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Finds the directory containing contract files by searching from a starting directory upward.
|
|
244
|
+
* At each level, recursively searches downward for the contract file.
|
|
245
|
+
* Validates that found contract.json files belong to Midnight contracts.
|
|
246
|
+
* This prevents confusion with EVM contract.json files or other similarly named files.
|
|
247
|
+
*/
|
|
248
|
+
function findContractDirectory(
|
|
249
|
+
startDir: string,
|
|
250
|
+
contractFileName: string,
|
|
251
|
+
contractName: string
|
|
252
|
+
): string | null {
|
|
253
|
+
let currentDir = path.resolve(startDir);
|
|
254
|
+
const root = path.parse(currentDir).root;
|
|
255
|
+
|
|
256
|
+
while (currentDir !== root) {
|
|
257
|
+
// Recursively search from current directory downward for ALL matching files
|
|
258
|
+
const foundDirs = findAllFilesRecursive(currentDir, contractFileName);
|
|
259
|
+
|
|
260
|
+
// Validate each found directory to ensure it's a Midnight contract
|
|
261
|
+
for (const found of foundDirs) {
|
|
262
|
+
if (isValidMidnightContractDir(found, contractName)) {
|
|
263
|
+
return found;
|
|
264
|
+
}
|
|
265
|
+
// If not valid, continue checking other found directories
|
|
266
|
+
// This handles cases where multiple contract.json files exist
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Move up one directory
|
|
270
|
+
currentDir = path.dirname(currentDir);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Reads contract information from JSON files in a context-aware manner.
|
|
278
|
+
*
|
|
279
|
+
* The function detects the directory containing contract files by searching
|
|
280
|
+
* from the current working directory upward, or uses an explicit baseDir if provided.
|
|
281
|
+
* This makes it work regardless of where the function is imported from.
|
|
282
|
+
*
|
|
283
|
+
* @param contractName - The name of the contract directory (e.g., 'contract-eip-1155', 'contract-counter')
|
|
284
|
+
* @param contractFileName - The name of the contract address file (default: 'contract.json')
|
|
285
|
+
* @param options - Optional configuration: baseDir to override search location, networkId to select specific network
|
|
286
|
+
* @returns The complete contract information including address and compiler data
|
|
287
|
+
*/
|
|
288
|
+
export function readMidnightContract(
|
|
289
|
+
contractName: string,
|
|
290
|
+
options?: { baseDir?: string; networkId?: string; contractFileName?: string }
|
|
291
|
+
): MidnightContractInfo {
|
|
292
|
+
const baseDir = options?.baseDir;
|
|
293
|
+
const resolvedNetworkId = options?.networkId || "undeployed";
|
|
294
|
+
const networkFileName = `${contractName}.${resolvedNetworkId}.json`;
|
|
295
|
+
const candidateFileNames = [
|
|
296
|
+
networkFileName,
|
|
297
|
+
`${contractName}.json`,
|
|
298
|
+
"contract.json",
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
let moduleDir: string;
|
|
303
|
+
|
|
304
|
+
if (baseDir) {
|
|
305
|
+
moduleDir = path.resolve(baseDir);
|
|
306
|
+
} else if (getRuntime().environment === 'backend') {
|
|
307
|
+
let foundDir: string | null = null;
|
|
308
|
+
|
|
309
|
+
for (const candidate of candidateFileNames) {
|
|
310
|
+
const dir = findContractDirectory(cwd(), candidate, contractName);
|
|
311
|
+
if (dir) {
|
|
312
|
+
foundDir = dir;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!foundDir) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Could not find Midnight contract directory for "${contractName}". ` +
|
|
320
|
+
`Searched for ${candidateFileNames.join(", ")} starting from ${path.resolve(cwd())}. ` +
|
|
321
|
+
`Please ensure you're running from a directory that contains or is a parent of the Midnight contract files, ` +
|
|
322
|
+
`or provide an explicit baseDir parameter. ` +
|
|
323
|
+
`Note: This function only finds Midnight contracts (with src/managed/ structure), not EVM contracts.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
moduleDir = foundDir;
|
|
328
|
+
} else {
|
|
329
|
+
const envContractAddress = getEnv("MIDNIGHT_CONTRACT_ADDRESS");
|
|
330
|
+
return {
|
|
331
|
+
contractAddress: envContractAddress || "",
|
|
332
|
+
contractInfo: { circuits: [], witnesses: [], contracts: [] },
|
|
333
|
+
zkConfigPath: "",
|
|
334
|
+
contractDir: "",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const normalizedModuleDir = path.resolve(moduleDir);
|
|
339
|
+
for (const candidate of candidateFileNames) {
|
|
340
|
+
const cacheKey = `${normalizedModuleDir}:${contractName}:${candidate}`;
|
|
341
|
+
if (cachedContractInfo[cacheKey]) return cachedContractInfo[cacheKey];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let contractAddressJson: string | undefined;
|
|
345
|
+
let actualContractFileName: string | undefined;
|
|
346
|
+
|
|
347
|
+
for (const candidate of candidateFileNames) {
|
|
348
|
+
const candidatePath = path.join(moduleDir, candidate);
|
|
349
|
+
try {
|
|
350
|
+
contractAddressJson = readFileSync(candidatePath, "utf-8");
|
|
351
|
+
actualContractFileName = candidate;
|
|
352
|
+
break;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (isNotFoundError(err)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!contractAddressJson || !actualContractFileName) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Contract files not found for "${contractName}". Tried: ${candidateFileNames.join(
|
|
364
|
+
", ",
|
|
365
|
+
)} under ${moduleDir}. Please verify the compiler artifacts and contract JSON exist.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let managedArtifactsDir = "";
|
|
370
|
+
try {
|
|
371
|
+
// Find the first directory inside the managed directory
|
|
372
|
+
const managedDir = path.join(moduleDir, contractName, "src/managed/");
|
|
373
|
+
try {
|
|
374
|
+
managedArtifactsDir = resolveManagedArtifactsDir(managedDir);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Managed directory not found or invalid: ${managedDir}. ${(error as Error).message}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Construct the full path to the contract info file using the resolved managed directory
|
|
382
|
+
const contractInfoPath = path.join(
|
|
383
|
+
managedArtifactsDir,
|
|
384
|
+
"compiler/contract-info.json"
|
|
385
|
+
);
|
|
386
|
+
const zkConfigPath = path.resolve(managedArtifactsDir);
|
|
387
|
+
const contractInfoJson = readFileSync(contractInfoPath, "utf-8");
|
|
388
|
+
const contractAddressInfo = JSON.parse(contractAddressJson) as MidnightContractAddressInfo;
|
|
389
|
+
const contractInfo = JSON.parse(contractInfoJson) as MidnightContractCompilerInfo;
|
|
390
|
+
|
|
391
|
+
// Handle both legacy format (string) and new format (network-keyed object)
|
|
392
|
+
let contractAddress: string;
|
|
393
|
+
let contractAddresses: Record<string, string>;
|
|
394
|
+
|
|
395
|
+
if (typeof contractAddressInfo.contractAddress === "string") {
|
|
396
|
+
// Legacy format - single string address
|
|
397
|
+
contractAddress = contractAddressInfo.contractAddress;
|
|
398
|
+
contractAddresses = { [resolvedNetworkId]: contractAddress };
|
|
399
|
+
} else {
|
|
400
|
+
contractAddresses = contractAddressInfo.contractAddress;
|
|
401
|
+
contractAddress = contractAddresses[resolvedNetworkId];
|
|
402
|
+
|
|
403
|
+
if (!contractAddress) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Contract address not found for network "${resolvedNetworkId}". ` +
|
|
406
|
+
`Available networks: ${Object.keys(contractAddresses).join(", ")}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Override contract address if MIDNIGHT_CONTRACT_ADDRESS env var is set
|
|
412
|
+
const envContractAddress = getEnv("MIDNIGHT_CONTRACT_ADDRESS");
|
|
413
|
+
if (envContractAddress) {
|
|
414
|
+
contractAddress = envContractAddress;
|
|
415
|
+
contractAddresses[resolvedNetworkId] = envContractAddress;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const cacheKey = `${normalizedModuleDir}:${contractName}:${actualContractFileName}`;
|
|
419
|
+
cachedContractInfo[cacheKey] = {
|
|
420
|
+
contractAddress,
|
|
421
|
+
contractInfo,
|
|
422
|
+
zkConfigPath,
|
|
423
|
+
contractDir: moduleDir,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return cachedContractInfo[cacheKey];
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (isNotFoundError(err)) {
|
|
429
|
+
const fileList = candidateFileNames
|
|
430
|
+
.map((name) => path.join(moduleDir, name))
|
|
431
|
+
.join(", ");
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Contract files not found - expected one of [${fileList}] and compiler artifacts under ${managedArtifactsDir}.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`Failed to read contract files: ${String(err)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|