@effectstream/midnight-contracts 0.100.17 → 0.100.19
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 +38 -5
- package/package.json +2 -2
- package/src/deploy-phased.ts +284 -0
- package/src/deploy.ts +57 -30
- package/src/mod.ts +1 -0
- package/src/types.ts +29 -10
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ Returns `{ contractAddress, contractInfo, zkConfigPath }`. Results are cached pe
|
|
|
41
41
|
|
|
42
42
|
### Deploy a contract
|
|
43
43
|
|
|
44
|
+
Only `contractName` and `contractClass` are required — everything else has a sensible default, so a minimal deploy is just:
|
|
45
|
+
|
|
44
46
|
```typescript
|
|
45
47
|
import {
|
|
46
48
|
deployMidnightContract,
|
|
@@ -48,13 +50,24 @@ import {
|
|
|
48
50
|
type NetworkUrls,
|
|
49
51
|
} from "@effectstream/midnight-contracts/deploy";
|
|
50
52
|
|
|
53
|
+
// contractName must match the directory that holds `src/managed`;
|
|
54
|
+
// contractClass is the compiled Contract (e.g. `export * as Counter from "./managed/contract"`).
|
|
55
|
+
const address = await deployMidnightContract({
|
|
56
|
+
contractName: "contract-counter",
|
|
57
|
+
contractClass: Counter.Contract,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Supply the rest only when your contract needs it:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
51
64
|
const config: DeployConfig = {
|
|
52
65
|
contractName: "contract-counter",
|
|
53
|
-
contractFileName: "contract-counter.json",
|
|
54
66
|
contractClass: Counter.Contract,
|
|
55
|
-
witnesses,
|
|
56
|
-
privateStateId: "counterPrivateState",
|
|
57
|
-
initialPrivateState: { privateCounter: 0 },
|
|
67
|
+
witnesses, // default: {} (no witnesses)
|
|
68
|
+
privateStateId: "counterPrivateState", // default: "privateState"
|
|
69
|
+
initialPrivateState: { privateCounter: 0 }, // default: {} (no private state)
|
|
70
|
+
contractFileName: "contract-counter.json", // default: `${contractName}.json`
|
|
58
71
|
};
|
|
59
72
|
|
|
60
73
|
const address = await deployMidnightContract(config);
|
|
@@ -75,6 +88,25 @@ const address = await deployMidnightContract(config, network);
|
|
|
75
88
|
|
|
76
89
|
The deploy helper creates and funds a wallet from the genesis mint seed, runs the deployment, and writes the resulting address to `${contractName}.${networkId}.json` so the next `readMidnightContract` call picks it up.
|
|
77
90
|
|
|
91
|
+
### Contracts with many circuits (phased deployment)
|
|
92
|
+
|
|
93
|
+
A standard deploy carries every circuit's verifier key in a single transaction. For contracts with enough circuits, that transaction exceeds the node's per-block limits and fails with `Transaction would exhaust block limits`. Set `phasedVerifierKeys: true` to instead deploy the contract with no verifier keys and then insert each circuit's key in its own transaction:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const address = await deployMidnightContract({
|
|
97
|
+
...config,
|
|
98
|
+
phasedVerifierKeys: true, // opt-in; default is a single-transaction deploy
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Phased mode writes progress to a resume-state file (`deployment-state.json` by default) and removes it on success, so an interrupted run can be re-run to continue from the last inserted circuit instead of redeploying. Tunables:
|
|
103
|
+
|
|
104
|
+
- `phasedVerifierKeys?: boolean` — enable phased deployment (default `false`).
|
|
105
|
+
- `vkInsertRetries?: number` — per-circuit retry count for key insertion (default `3`).
|
|
106
|
+
- `phasedStateFile?: string` — resume-state file path (default `deployment-state.json` in the CWD).
|
|
107
|
+
|
|
108
|
+
The circuit list is enumerated automatically from the contract's compiled `keys/` directory, so no per-contract configuration is required.
|
|
109
|
+
|
|
78
110
|
## Inside Effectstream
|
|
79
111
|
|
|
80
112
|
`@effectstream/midnight-contracts` is the seam between Midnight contract artifacts on disk and the code that reads or deploys them. Templates that target Midnight call `readMidnightContract` from their node startup to resolve the on-chain address; the orchestrator's deploy step calls `deployMidnightContract` to put one there in the first place. On the sync side, `@effectstream/sync`'s `MidnightFetcher` consumes the node behind both calls.
|
|
@@ -87,7 +119,8 @@ The deploy helper creates and funds a wallet from the genesis mint seed, runs th
|
|
|
87
119
|
|
|
88
120
|
`@effectstream/midnight-contracts/deploy`:
|
|
89
121
|
|
|
90
|
-
- `deployMidnightContract(config, networkUrls?)`: deploys and returns the address. Persists the address to a JSON file.
|
|
122
|
+
- `deployMidnightContract(config, networkUrls?)`: deploys and returns the address. Persists the address to a JSON file. Set `config.phasedVerifierKeys` for contracts whose circuits don't fit in a single deploy transaction.
|
|
123
|
+
- `deployMidnightContractPhased(...)`: the phased deploy routine `deployMidnightContract` delegates to when `phasedVerifierKeys` is set. Exported for advanced callers that already have built providers and a wallet.
|
|
91
124
|
- `DeployConfig`, `NetworkUrls`: input types.
|
|
92
125
|
|
|
93
126
|
## Examples
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/midnight-contracts",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"./types": "./src/types.ts"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@effectstream/utils": "0.100.
|
|
23
|
+
"@effectstream/utils": "0.100.19",
|
|
24
24
|
"@scure/bip39": "^2.0.1",
|
|
25
25
|
"dotenv": "^16.4.7",
|
|
26
26
|
"@midnight-ntwrk/wallet": "5.0.0",
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Phased Midnight contract deployment
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Some contracts define so many circuits that a single deploy transaction —
|
|
6
|
+
// which carries every circuit's verifier key (VK) — exceeds the node's
|
|
7
|
+
// per-block transaction limits ("Transaction would exhaust block limits").
|
|
8
|
+
//
|
|
9
|
+
// This module deploys such contracts in phases:
|
|
10
|
+
// 1. Deploy the contract with NO verifier keys (a "stripped" contract state
|
|
11
|
+
// that keeps only the initial data and maintenance authority).
|
|
12
|
+
// 2. Insert each circuit's verifier key in its own transaction, using the
|
|
13
|
+
// official `submitInsertVerifierKeyTx` maintenance helper.
|
|
14
|
+
//
|
|
15
|
+
// Progress is tracked in a small resume-state file so an interrupted run can be
|
|
16
|
+
// continued without redeploying or re-inserting already-applied keys.
|
|
17
|
+
//
|
|
18
|
+
// This path is opt-in via `DeployConfig.phasedVerifierKeys`; the default
|
|
19
|
+
// single-transaction deploy in `deploy.ts` is unaffected.
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { getNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
|
|
24
|
+
import { ContractExecutable } from "@midnight-ntwrk/compact-js";
|
|
25
|
+
import {
|
|
26
|
+
asContractAddress,
|
|
27
|
+
exitResultOrError,
|
|
28
|
+
makeContractExecutableRuntime,
|
|
29
|
+
SucceedEntirely,
|
|
30
|
+
type MidnightProviders,
|
|
31
|
+
} from "@midnight-ntwrk/midnight-js-types";
|
|
32
|
+
import { submitInsertVerifierKeyTx } from "@midnight-ntwrk/midnight-js-contracts";
|
|
33
|
+
import {
|
|
34
|
+
ContractDeploy,
|
|
35
|
+
ContractState,
|
|
36
|
+
Intent,
|
|
37
|
+
sampleSigningKey,
|
|
38
|
+
Transaction,
|
|
39
|
+
} from "@midnight-ntwrk/ledger-v8";
|
|
40
|
+
|
|
41
|
+
import { CONSTANTS } from "./constants.ts";
|
|
42
|
+
import type { DeployConfig, WalletResult } from "./types.ts";
|
|
43
|
+
|
|
44
|
+
const log = console;
|
|
45
|
+
|
|
46
|
+
const DEFAULT_VK_INSERT_RETRIES = 3;
|
|
47
|
+
const DEFAULT_STATE_FILE = "deployment-state.json";
|
|
48
|
+
|
|
49
|
+
interface PhasedDeploymentState {
|
|
50
|
+
contractAddress: string;
|
|
51
|
+
deployedCircuits: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createTtl(): Date {
|
|
55
|
+
return new Date(Date.now() + CONSTANTS.TTL_DURATION_MS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enumerate circuit IDs generically from the compiled `keys/` directory.
|
|
60
|
+
* Each circuit has a `<circuitId>.verifier` artifact; this avoids hardcoding a
|
|
61
|
+
* per-contract circuit list.
|
|
62
|
+
*/
|
|
63
|
+
function listCircuitIds(zkConfigPath: string): string[] {
|
|
64
|
+
const keysDir = path.join(zkConfigPath, "keys");
|
|
65
|
+
let entries: string[];
|
|
66
|
+
try {
|
|
67
|
+
entries = readdirSync(keysDir);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Could not read verifier keys directory '${keysDir}': ${
|
|
71
|
+
error instanceof Error ? error.message : String(error)
|
|
72
|
+
}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const circuitIds = entries
|
|
76
|
+
.filter((name) => name.endsWith(".verifier"))
|
|
77
|
+
.map((name) => name.slice(0, -".verifier".length))
|
|
78
|
+
.sort();
|
|
79
|
+
if (circuitIds.length === 0) {
|
|
80
|
+
throw new Error(`No '*.verifier' artifacts found in '${keysDir}'.`);
|
|
81
|
+
}
|
|
82
|
+
return circuitIds;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function loadState(stateFilePath: string): PhasedDeploymentState {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(
|
|
88
|
+
readFileSync(stateFilePath, "utf8"),
|
|
89
|
+
) as PhasedDeploymentState;
|
|
90
|
+
log.info(
|
|
91
|
+
`Found existing deployment state. Resuming deployment for contract: ${parsed.contractAddress}`,
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
contractAddress: parsed.contractAddress ?? "",
|
|
95
|
+
deployedCircuits: parsed.deployedCircuits ?? [],
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return { contractAddress: "", deployedCircuits: [] };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function saveState(stateFilePath: string, state: PhasedDeploymentState): void {
|
|
103
|
+
writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Deploy a Midnight contract in phases: empty deploy, then per-circuit verifier
|
|
108
|
+
* key insertion. Returns the deployed contract address.
|
|
109
|
+
*/
|
|
110
|
+
export async function deployMidnightContractPhased(
|
|
111
|
+
providers: MidnightProviders,
|
|
112
|
+
// deno-lint-ignore no-explicit-any
|
|
113
|
+
compiledContract: any,
|
|
114
|
+
config: DeployConfig,
|
|
115
|
+
// deno-lint-ignore no-explicit-any
|
|
116
|
+
deployArgs: any[] | undefined,
|
|
117
|
+
walletResult: WalletResult,
|
|
118
|
+
zkConfigPath: string,
|
|
119
|
+
): Promise<string> {
|
|
120
|
+
const stateFilePath = path.resolve(
|
|
121
|
+
config.phasedStateFile ?? path.join(process.cwd(), DEFAULT_STATE_FILE),
|
|
122
|
+
);
|
|
123
|
+
const maxRetries = config.vkInsertRetries ?? DEFAULT_VK_INSERT_RETRIES;
|
|
124
|
+
|
|
125
|
+
const state = loadState(stateFilePath);
|
|
126
|
+
let contractAddress = state.contractAddress;
|
|
127
|
+
|
|
128
|
+
log.info(
|
|
129
|
+
`Phased deploy: contract=${config.contractName} resuming=${Boolean(
|
|
130
|
+
contractAddress,
|
|
131
|
+
)}`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// ── Phase 1: deploy the contract with no verifier keys ────────────────────
|
|
135
|
+
if (!contractAddress) {
|
|
136
|
+
const signingKey = sampleSigningKey();
|
|
137
|
+
const coinPublicKey = providers.walletProvider.getCoinPublicKey().toString();
|
|
138
|
+
|
|
139
|
+
const contractExec = ContractExecutable.make(compiledContract);
|
|
140
|
+
const contractRuntime = makeContractExecutableRuntime(
|
|
141
|
+
providers.zkConfigProvider,
|
|
142
|
+
{ coinPublicKey, signingKey },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
log.info("Running contract initialization to derive initial state...");
|
|
146
|
+
const exitResult = await contractRuntime.runPromiseExit(
|
|
147
|
+
(contractExec as any).initialize(
|
|
148
|
+
config.initialPrivateState ?? undefined,
|
|
149
|
+
...(deployArgs ?? []),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
const initResult = exitResultOrError(exitResult as any) as any;
|
|
153
|
+
|
|
154
|
+
const privateState = initResult.private.privateState;
|
|
155
|
+
const derivedSigningKey = initResult.private.signingKey;
|
|
156
|
+
const fullContractState = initResult.public.contractState;
|
|
157
|
+
|
|
158
|
+
// Convert the compact-runtime contract state to a ledger-v8 contract state,
|
|
159
|
+
// then build a stripped copy that keeps the initial data + maintenance
|
|
160
|
+
// authority but drops every circuit operation (i.e. no verifier keys).
|
|
161
|
+
const fullLedgerState = ContractState.deserialize(
|
|
162
|
+
fullContractState.serialize(),
|
|
163
|
+
);
|
|
164
|
+
const strippedState = new ContractState();
|
|
165
|
+
strippedState.data = fullLedgerState.data;
|
|
166
|
+
strippedState.maintenanceAuthority = fullLedgerState.maintenanceAuthority;
|
|
167
|
+
log.info("Created stripped contract state (no verifier keys).");
|
|
168
|
+
|
|
169
|
+
const contractDeploy = new ContractDeploy(strippedState);
|
|
170
|
+
contractAddress = contractDeploy.address;
|
|
171
|
+
|
|
172
|
+
const unprovenTx = Transaction.fromParts(
|
|
173
|
+
getNetworkId(),
|
|
174
|
+
undefined,
|
|
175
|
+
undefined,
|
|
176
|
+
Intent.new(createTtl()).addDeploy(contractDeploy),
|
|
177
|
+
);
|
|
178
|
+
log.info(`Deploying empty contract at address: ${contractAddress}`);
|
|
179
|
+
|
|
180
|
+
const recipe = await walletResult.wallet.balanceUnprovenTransaction(
|
|
181
|
+
unprovenTx as any,
|
|
182
|
+
{
|
|
183
|
+
shieldedSecretKeys: walletResult.walletZswapSecretKeys as any,
|
|
184
|
+
dustSecretKey: walletResult.walletDustSecretKey as any,
|
|
185
|
+
},
|
|
186
|
+
{ ttl: createTtl() },
|
|
187
|
+
);
|
|
188
|
+
const signedRecipe = await walletResult.wallet.signRecipe(
|
|
189
|
+
recipe,
|
|
190
|
+
(payload) => walletResult.unshieldedKeystore.signData(payload),
|
|
191
|
+
);
|
|
192
|
+
const finalizedTx = await walletResult.wallet.finalizeRecipe(signedRecipe);
|
|
193
|
+
const txId = await walletResult.wallet.submitTransaction(finalizedTx);
|
|
194
|
+
|
|
195
|
+
const txData = await providers.publicDataProvider.watchForTxData(txId);
|
|
196
|
+
if (txData.status !== SucceedEntirely) {
|
|
197
|
+
throw new Error(`Empty deploy failed with status ${txData.status}`);
|
|
198
|
+
}
|
|
199
|
+
log.info(
|
|
200
|
+
`Empty deploy succeeded: address=${contractAddress} (no verifier keys).`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Persist the contract address, private state and signing key so the VK
|
|
204
|
+
// insertion phase (and any resumed run) can find the maintenance authority.
|
|
205
|
+
state.contractAddress = contractAddress;
|
|
206
|
+
saveState(stateFilePath, state);
|
|
207
|
+
|
|
208
|
+
(providers.privateStateProvider as any).setContractAddress?.(contractAddress);
|
|
209
|
+
if (config.privateStateId) {
|
|
210
|
+
await providers.privateStateProvider.set(
|
|
211
|
+
config.privateStateId as any,
|
|
212
|
+
privateState,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
await providers.privateStateProvider.setSigningKey(
|
|
216
|
+
contractAddress,
|
|
217
|
+
derivedSigningKey,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Phase 2: insert each circuit's verifier key individually ──────────────
|
|
222
|
+
const circuitIds = listCircuitIds(zkConfigPath);
|
|
223
|
+
const verifierKeys = await providers.zkConfigProvider.getVerifierKeys(
|
|
224
|
+
circuitIds,
|
|
225
|
+
);
|
|
226
|
+
log.info(
|
|
227
|
+
`Inserting ${verifierKeys.length} verifier keys one-by-one: [${circuitIds.join(
|
|
228
|
+
", ",
|
|
229
|
+
)}]`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
for (const [circuitId, verifierKey] of verifierKeys) {
|
|
233
|
+
if (state.deployedCircuits.includes(circuitId)) {
|
|
234
|
+
log.info(`Skipping already deployed circuit: ${circuitId}`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log.info(`Inserting verifier key for circuit: ${circuitId}`);
|
|
239
|
+
for (let attempt = 1; ; attempt++) {
|
|
240
|
+
try {
|
|
241
|
+
const result = await submitInsertVerifierKeyTx(
|
|
242
|
+
providers,
|
|
243
|
+
compiledContract,
|
|
244
|
+
asContractAddress(contractAddress),
|
|
245
|
+
circuitId as any,
|
|
246
|
+
verifierKey,
|
|
247
|
+
);
|
|
248
|
+
if (result.status !== SucceedEntirely) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Insert verifier key for ${circuitId} returned status ${result.status}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (attempt >= maxRetries) throw error;
|
|
256
|
+
log.warn(
|
|
257
|
+
`Retry inserting ${circuitId} (${attempt}/${maxRetries}) due to: ${
|
|
258
|
+
error instanceof Error ? error.message : String(error)
|
|
259
|
+
}`,
|
|
260
|
+
);
|
|
261
|
+
await new Promise((r) => setTimeout(r, 2000 * attempt));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
state.deployedCircuits.push(circuitId);
|
|
266
|
+
saveState(stateFilePath, state);
|
|
267
|
+
log.info(
|
|
268
|
+
`Verifier key inserted: circuit=${circuitId} (${state.deployedCircuits.length}/${verifierKeys.length})`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
log.info(
|
|
273
|
+
`Phased deploy complete: address=${contractAddress} circuits=[${state.deployedCircuits.join(
|
|
274
|
+
", ",
|
|
275
|
+
)}]`,
|
|
276
|
+
);
|
|
277
|
+
try {
|
|
278
|
+
rmSync(stateFilePath);
|
|
279
|
+
} catch {
|
|
280
|
+
// Resume-state file already gone — nothing to clean up.
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return contractAddress;
|
|
284
|
+
}
|
package/src/deploy.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { mnemonicToSeed } from "./mnemonicToSeed.ts";
|
|
|
19
19
|
import type { NetworkUrls, DeployConfig, WalletResult } from "./types.ts";
|
|
20
20
|
import { buildWalletAndWaitForFunds, extractInitialOwnerFromWallet } from "./build-wallet.ts";
|
|
21
21
|
import { configureMidnightNodeProviders } from "./providers.ts";
|
|
22
|
+
import { deployMidnightContractPhased } from "./deploy-phased.ts";
|
|
22
23
|
import { midnightNetworkConfig } from "./midnight-env.ts";
|
|
23
24
|
|
|
24
25
|
function checkEnvVariables(): void {
|
|
@@ -209,45 +210,71 @@ export async function deployMidnightContract(
|
|
|
209
210
|
|
|
210
211
|
log.info("Deploying contract...");
|
|
211
212
|
|
|
213
|
+
// Apply defaults so callers only pass what their contract needs. Only
|
|
214
|
+
// contractName and contractClass are truly required; everything else has a
|
|
215
|
+
// sensible default.
|
|
216
|
+
const resolvedConfig: DeployConfig = {
|
|
217
|
+
...config,
|
|
218
|
+
contractFileName: config.contractFileName ?? `${config.contractName}.json`,
|
|
219
|
+
witnesses: config.witnesses ?? {},
|
|
220
|
+
privateStateId: config.privateStateId ?? "privateState",
|
|
221
|
+
initialPrivateState: config.initialPrivateState ?? {},
|
|
222
|
+
};
|
|
223
|
+
|
|
212
224
|
// First, create the compiled contract
|
|
213
225
|
const MyCompiledContract = CompiledContract.make(
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
resolvedConfig.contractName,
|
|
227
|
+
resolvedConfig.contractClass,
|
|
216
228
|
).pipe(
|
|
217
|
-
CompiledContract.withWitnesses(
|
|
229
|
+
CompiledContract.withWitnesses(resolvedConfig.witnesses as never),
|
|
218
230
|
CompiledContract.withCompiledFileAssets(managedDir),
|
|
219
231
|
);
|
|
220
232
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
233
|
+
let contractAddress: string;
|
|
234
|
+
if (resolvedConfig.phasedVerifierKeys) {
|
|
235
|
+
// Opt-in path for contracts with too many circuits to deploy in a single
|
|
236
|
+
// transaction: deploy with no verifier keys, then insert each circuit's
|
|
237
|
+
// key one transaction at a time.
|
|
238
|
+
log.info("Phased verifier-key deployment enabled.");
|
|
239
|
+
contractAddress = await deployMidnightContractPhased(
|
|
240
|
+
providers,
|
|
241
|
+
MyCompiledContract,
|
|
242
|
+
resolvedConfig,
|
|
243
|
+
deployArgs,
|
|
244
|
+
walletResult,
|
|
245
|
+
zkConfigPath,
|
|
246
|
+
);
|
|
247
|
+
log.info("Contract deployed (phased).");
|
|
248
|
+
} else {
|
|
249
|
+
const deployOptions: {
|
|
250
|
+
compiledContract: CompiledContract.CompiledContract<
|
|
251
|
+
Contract<undefined, Witnesses<undefined>>,
|
|
252
|
+
undefined,
|
|
253
|
+
never
|
|
254
|
+
>;
|
|
255
|
+
privateStateId: PrivateStateId;
|
|
256
|
+
initialPrivateState: Contract.PrivateState<any>;
|
|
257
|
+
signingKey?: SigningKey;
|
|
258
|
+
args: Contract.InitializeParameters<any>;
|
|
259
|
+
} = {
|
|
260
|
+
compiledContract: MyCompiledContract as any,
|
|
261
|
+
privateStateId: resolvedConfig.privateStateId as PrivateStateId,
|
|
262
|
+
initialPrivateState:
|
|
263
|
+
resolvedConfig.initialPrivateState as Contract.PrivateState<any>,
|
|
264
|
+
args: (deployArgs && deployArgs.length > 0
|
|
265
|
+
? deployArgs
|
|
266
|
+
: []) as Contract.InitializeParameters<any>,
|
|
267
|
+
signingKey: undefined,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const deployedContract = await deployContract(providers, deployOptions);
|
|
271
|
+
log.info("Contract deployed.");
|
|
272
|
+
contractAddress = deployedContract.deployTxData.public.contractAddress;
|
|
273
|
+
}
|
|
247
274
|
log.info(`Contract address: ${contractAddress}`);
|
|
248
275
|
|
|
249
276
|
const baseContractFileName =
|
|
250
|
-
|
|
277
|
+
resolvedConfig.contractFileName ?? `${config.contractName}.json`;
|
|
251
278
|
const {
|
|
252
279
|
dir: contractFileDir,
|
|
253
280
|
name: contractFileBaseName,
|
package/src/mod.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -11,21 +11,21 @@ import type { UnshieldedKeystore } from "@midnight-ntwrk/wallet-sdk-unshielded-w
|
|
|
11
11
|
* Configuration for deploying a Midnight contract
|
|
12
12
|
*/
|
|
13
13
|
export interface DeployConfig {
|
|
14
|
-
/** Name of the contract directory (e.g., "contract-counter", "contract-eip-20") */
|
|
14
|
+
/** Name of the contract directory containing `src/managed` (e.g., "contract-counter", "contract-eip-20"). Required. */
|
|
15
15
|
contractName: string;
|
|
16
|
-
/**
|
|
17
|
-
contractFileName: string;
|
|
18
|
-
/** The Contract class to deploy */
|
|
16
|
+
/** The compiled Contract class to deploy (e.g. `Foo.Contract`). Required. */
|
|
19
17
|
// deno-lint-ignore no-explicit-any
|
|
20
18
|
contractClass: any;
|
|
21
|
-
/**
|
|
19
|
+
/** Base filename the deployed address is written to; a network suffix is appended. Defaults to `${contractName}.json`. */
|
|
20
|
+
contractFileName?: string;
|
|
21
|
+
/** Witness definitions. Defaults to `{}` (no witnesses). */
|
|
22
22
|
// deno-lint-ignore no-explicit-any
|
|
23
|
-
witnesses
|
|
24
|
-
/** On-chain private state ID */
|
|
25
|
-
privateStateId
|
|
26
|
-
/** Initial private state object */
|
|
23
|
+
witnesses?: any;
|
|
24
|
+
/** On-chain private state ID. Defaults to `"privateState"`. */
|
|
25
|
+
privateStateId?: string;
|
|
26
|
+
/** Initial private state object. Defaults to `{}` (for contracts with no private state). */
|
|
27
27
|
// deno-lint-ignore no-explicit-any
|
|
28
|
-
initialPrivateState
|
|
28
|
+
initialPrivateState?: any;
|
|
29
29
|
/** Optional deployment arguments array */
|
|
30
30
|
// deno-lint-ignore no-explicit-any
|
|
31
31
|
deployArgs?: any[];
|
|
@@ -35,6 +35,25 @@ export interface DeployConfig {
|
|
|
35
35
|
baseDir?: string;
|
|
36
36
|
/** Optional flag to extract wallet address info (for contracts that need initialOwner) */
|
|
37
37
|
extractWalletAddress?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Deploy in phases instead of a single transaction.
|
|
40
|
+
*
|
|
41
|
+
* When `true`, the contract is first deployed with NO verifier keys, then each
|
|
42
|
+
* circuit's verifier key is inserted in its own transaction. Use this for
|
|
43
|
+
* contracts with many circuits whose combined verifier keys would otherwise
|
|
44
|
+
* exceed the node's per-block transaction limits ("Transaction would exhaust
|
|
45
|
+
* block limits"). Defaults to `false` (single-transaction deploy).
|
|
46
|
+
*/
|
|
47
|
+
phasedVerifierKeys?: boolean;
|
|
48
|
+
/** Per-circuit retry count when inserting verifier keys in phased mode (default 3). */
|
|
49
|
+
vkInsertRetries?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Path to the resume-state file used by phased mode to track progress so an
|
|
52
|
+
* interrupted deployment can be resumed. Defaults to `deployment-state.json`
|
|
53
|
+
* in the current working directory. This file is removed once the phased
|
|
54
|
+
* deployment completes successfully.
|
|
55
|
+
*/
|
|
56
|
+
phasedStateFile?: string;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
/**
|