create-mn-app 0.3.28 → 0.4.1

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.
@@ -1,14 +1,16 @@
1
1
  /**
2
- * Deploy {{projectName}} contract to Midnight Preprod network
2
+ * Deploy {{projectName}} contract to a Midnight network (undeployed by default; use --network preview|preprod for public networks).
3
+ *
4
+ * Non-interactive: scaffold → npm run setup runs straight through.
5
+ * No readline prompts, no .midnight-seed file.
3
6
  */
4
- import { createInterface } from 'node:readline/promises';
5
- import { stdin, stdout } from 'node:process';
6
7
  import * as fs from 'node:fs';
7
8
  import * as path from 'node:path';
9
+ import { resolveNetwork, getOrCreateSeed, recordDeployment } from './network';
10
+ import { createWallet, persistWalletState, unshieldedToken, type WalletContext } from './wallet';
8
11
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
12
  import { WebSocket } from 'ws';
10
13
  import * as Rx from 'rxjs';
11
- import { Buffer } from 'buffer';
12
14
 
13
15
  // Midnight SDK imports
14
16
  import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
@@ -16,53 +18,38 @@ import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client
16
18
  import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
17
19
  import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
18
20
  import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
19
- import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
20
- import { toHex } from '@midnight-ntwrk/midnight-js-utils';
21
- import * as ledger from '@midnight-ntwrk/ledger-v8';
22
- import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
23
- import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
24
- import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
25
- import { HDWallet, Roles, generateRandomSeed } from '@midnight-ntwrk/wallet-sdk-hd';
26
- import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
27
- import { createKeystore, InMemoryTransactionHistoryStorage, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
28
21
  import { CompiledContract } from '@midnight-ntwrk/compact-js';
29
22
 
30
- // Enable WebSocket for GraphQL subscriptions
31
23
  // @ts-expect-error Required for wallet sync
32
24
  globalThis.WebSocket = WebSocket;
33
25
 
34
- // Set network to preprod
35
- setNetworkId('preprod');
26
+ // ─── Network configuration ─────────────────────────────────────────────────────
27
+ //
28
+ // Resolved from --network flag, .midnight-state.json, or defaulting to
29
+ // 'undeployed' (local devnet). Switch networks with: npm run network <name>
36
30
 
37
- // Preprod network configuration
38
- const CONFIG = {
39
- indexer: 'https://indexer.preprod.midnight.network/api/v3/graphql',
40
- indexerWS: 'wss://indexer.preprod.midnight.network/api/v3/graphql/ws',
41
- node: 'https://rpc.preprod.midnight.network',
42
- proofServer: 'http://127.0.0.1:6300',
43
- faucetUrl: 'https://faucet.preprod.midnight.network/',
44
- };
31
+ const { network, config: networkConfig } = resolveNetwork();
32
+ const SEED = getOrCreateSeed(network);
45
33
 
46
- // ─── Proof Server Health Check ─────────────────────────────────────────────────
34
+ // ─── Proof server readiness ────────────────────────────────────────────────────
35
+ //
36
+ // The proof-server image is distroless and has no shell, so it can't run a
37
+ // container-side healthcheck. Poll it from the host before we submit anything
38
+ // that needs proofs.
47
39
 
48
- async function waitForProofServer(maxAttempts = 30, delayMs = 2000): Promise<boolean> {
40
+ async function waitForProofServer(maxAttempts = 60, delayMs = 2000): Promise<boolean> {
49
41
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
50
42
  try {
51
- // Try a simple GET - any response (even 404) means server is up
52
- const response = await fetch(CONFIG.proofServer, {
43
+ await fetch(networkConfig.proofServer, {
53
44
  method: 'GET',
54
45
  signal: AbortSignal.timeout(3000),
55
46
  });
56
- // Any response means the server is running
57
47
  return true;
58
48
  } catch (err: any) {
59
- // Check if it's a connection refused vs other error
60
- const errMsg = err?.cause?.code || err?.code || '';
61
- if (errMsg !== 'ECONNREFUSED' && errMsg !== 'UND_ERR_CONNECT_TIMEOUT') {
62
- // Got some other error - server might be up but returning errors
49
+ const code = err?.cause?.code || err?.code || '';
50
+ if (code !== 'ECONNREFUSED' && code !== 'UND_ERR_CONNECT_TIMEOUT' && code !== 'UND_ERR_SOCKET') {
63
51
  return true;
64
52
  }
65
- // Server not ready yet
66
53
  }
67
54
  if (attempt < maxAttempts) {
68
55
  process.stdout.write(`\r Waiting for proof server... (${attempt}/${maxAttempts}) `);
@@ -72,13 +59,12 @@ async function waitForProofServer(maxAttempts = 30, delayMs = 2000): Promise<boo
72
59
  return false;
73
60
  }
74
61
 
62
+ // ─── Compiled contract loading ─────────────────────────────────────────────────
63
+
75
64
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
76
65
  const zkConfigPath = path.resolve(__dirname, '..', 'contracts', 'managed', 'hello-world');
77
-
78
- // Load compiled contract
79
66
  const contractPath = path.join(zkConfigPath, 'contract', 'index.js');
80
67
 
81
- // Check if contract is compiled
82
68
  if (!fs.existsSync(contractPath)) {
83
69
  console.error('\n❌ Contract not compiled! Run: npm run compile\n');
84
70
  process.exit(1);
@@ -91,48 +77,13 @@ const compiledContract = CompiledContract.make('hello-world', HelloWorld.Contrac
91
77
  CompiledContract.withCompiledFileAssets(zkConfigPath),
92
78
  );
93
79
 
94
- // ─── Wallet Functions ──────────────────────────────────────────────────────────
95
-
96
- function deriveKeys(seed: string) {
97
- const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
98
- if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
99
- const result = hdWallet.hdWallet.selectAccount(0).selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust]).deriveKeysAt(0);
100
- if (result.type !== 'keysDerived') throw new Error('Key derivation failed');
101
- hdWallet.hdWallet.clear();
102
- return result.keys;
103
- }
104
-
105
- async function createWallet(seed: string) {
106
- const keys = deriveKeys(seed);
107
- const networkId = getNetworkId();
108
- const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
109
- const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
110
- const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], networkId);
111
-
112
- const walletConfig = {
113
- networkId,
114
- indexerClientConnection: { indexerHttpUrl: CONFIG.indexer, indexerWsUrl: CONFIG.indexerWS },
115
- provingServerUrl: new URL(CONFIG.proofServer),
116
- relayURL: new URL(CONFIG.node.replace(/^http/, 'ws')),
117
- txHistoryStorage: new InMemoryTransactionHistoryStorage(),
118
- costParameters: { additionalFeeOverhead: 300_000_000_000_000n, feeBlocksMargin: 5 },
119
- };
120
-
121
- const wallet = await WalletFacade.init({
122
- configuration: walletConfig,
123
- shielded: async (config) => ShieldedWallet(config).startWithSecretKeys(shieldedSecretKeys),
124
- unshielded: async (config) => UnshieldedWallet(config).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
125
- dust: async (config) => DustWallet(config).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust),
126
- });
127
-
128
- await wallet.start(shieldedSecretKeys, dustSecretKey);
129
-
130
- return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
131
- }
132
-
133
- async function createProviders(walletCtx: ReturnType<typeof createWallet> extends Promise<infer T> ? T : never) {
134
- const privateStatePassword = process.env.PRIVATE_STATE_PASSWORD?.trim() || 'development';
80
+ // ─── Providers ─────────────────────────────────────────────────────────────────
135
81
 
82
+ async function createProviders(walletCtx: WalletContext) {
83
+ // The SDK requires the private-state password to be at least 16 characters.
84
+ // The default below is a placeholder for local devnet only — set a strong
85
+ // password via PRIVATE_STATE_PASSWORD when you move to a non-local target.
86
+ const privateStatePassword = process.env.PRIVATE_STATE_PASSWORD?.trim() || 'Local-Devnet-Development-Placeholder-1';
136
87
  const state = await walletCtx.wallet.waitForSyncedState();
137
88
 
138
89
  const walletProvider = {
@@ -161,311 +112,248 @@ async function createProviders(walletCtx: ReturnType<typeof createWallet> extend
161
112
  accountId,
162
113
  privateStoragePasswordProvider: () => privateStatePassword,
163
114
  }),
164
- publicDataProvider: indexerPublicDataProvider(CONFIG.indexer, CONFIG.indexerWS),
115
+ publicDataProvider: indexerPublicDataProvider(networkConfig.indexer, networkConfig.indexerWS),
165
116
  zkConfigProvider,
166
- proofProvider: httpClientProofProvider(CONFIG.proofServer, zkConfigProvider),
117
+ proofProvider: httpClientProofProvider(networkConfig.proofServer, zkConfigProvider),
167
118
  walletProvider,
168
119
  midnightProvider: walletProvider,
169
120
  };
170
121
  }
171
122
 
172
- // ─── Main Deploy Script ────────────────────────────────────────────────────────
123
+ // ─── Main ──────────────────────────────────────────────────────────────────────
173
124
 
174
125
  async function main() {
175
126
  console.log('\n╔══════════════════════════════════════════════════════════════╗');
176
- console.log('║ Deploy {{projectName}} to Midnight Preprod ║');
127
+ console.log(`║ Deploy {{projectName}} to ${network}`);
177
128
  console.log('╚══════════════════════════════════════════════════════════════╝\n');
178
129
 
179
- const rl = createInterface({ input: stdin, output: stdout });
180
-
181
- try {
182
- // Check for existing deployment and seed
183
- let existingSeed: string | undefined;
184
- let existingContract: string | undefined;
130
+ const seed = SEED;
185
131
 
186
- if (fs.existsSync('.midnight-seed')) {
187
- try {
188
- existingSeed = fs.readFileSync('.midnight-seed', 'utf-8').trim();
189
- } catch {
190
- // Ignore read errors
191
- }
192
- }
193
-
194
- if (fs.existsSync('deployment.json')) {
195
- try {
196
- const existing = JSON.parse(fs.readFileSync('deployment.json', 'utf-8'));
197
- if (existing.contractAddress) existingContract = existing.contractAddress;
198
- } catch {
199
- // Ignore parse errors
200
- }
201
- }
202
-
203
- // If already deployed, ask if they want to redeploy
204
- if (existingContract) {
205
- console.log('─── Existing Deployment Found ──────────────────────────────────\n');
206
- console.log(` Contract: ${existingContract}`);
207
- const redeploy = await rl.question('\n Deploy a new contract? [y/N] ');
208
- if (redeploy.toLowerCase() !== 'y') {
209
- console.log('\n Run `npm run cli` to interact with your existing contract.\n');
210
- return;
211
- }
212
- existingSeed = undefined; // Fresh deployment = fresh wallet
213
- }
214
-
215
- // 1. Wallet setup
216
- console.log('─── Step 1: Wallet Setup ───────────────────────────────────────\n');
132
+ console.log('─── Wallet setup ───────────────────────────────────────────────\n');
133
+ console.log(' Creating wallet...');
134
+ const walletCtx = await createWallet({ network, networkConfig, seed });
135
+ const restoredCount = Object.values(walletCtx.restored).filter(Boolean).length;
136
+ if (restoredCount > 0) {
137
+ console.log(` Restored ${restoredCount}/3 child wallets from .midnight-wallet-state — sync will resume from saved point.`);
138
+ }
217
139
 
218
- let seed: string;
140
+ console.log(' Syncing with network...');
141
+ console.log(' ℹ This may take several minutes depending on network size.');
142
+ console.log(' RPC disconnection messages during sync are normal and can be safely ignored.\n');
143
+ const syncStart = Date.now();
144
+ const syncInterval = setInterval(() => {
145
+ const elapsed = Math.round((Date.now() - syncStart) / 1000);
146
+ process.stdout.write(`\r ⏳ Still syncing... (${elapsed}s elapsed) `);
147
+ }, 5000);
148
+ const state = await walletCtx.wallet.waitForSyncedState();
149
+ clearInterval(syncInterval);
150
+ process.stdout.write('\r ✓ Synced with network. \n');
151
+
152
+ // Persist sync state now so a later deploy failure doesn't waste the sync work.
153
+ await persistWalletState(network, walletCtx);
154
+
155
+ const address = walletCtx.unshieldedKeystore.getBech32Address();
156
+ let balance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;
157
+ console.log(`\n Wallet Address: ${address}`);
158
+ console.log(` Balance: ${balance.toLocaleString()} tNight\n`);
159
+
160
+ if (network === 'undeployed' && balance === 0n) {
161
+ console.error(
162
+ '\n❌ Genesis-seed wallet has zero NIGHT. The devnet preset may not have minted to it.\n' +
163
+ ' Check `docker compose ps` and `docker compose logs node`. Then `docker compose down -v` and retry.\n',
164
+ );
165
+ await walletCtx.wallet.stop();
166
+ process.exit(1);
167
+ }
219
168
 
220
- if (existingSeed) {
221
- // Resume from previous failed deployment
222
- console.log(' Found saved seed from previous attempt.');
223
- const useSaved = await rl.question(' Use saved wallet? [Y/n] ');
224
- if (useSaved.toLowerCase() !== 'n') {
225
- seed = existingSeed;
226
- console.log(' Using saved wallet...\n');
227
- } else {
228
- const choice = await rl.question(' [1] Create new wallet\n [2] Restore from seed\n > ');
229
- seed = choice.trim() === '2'
230
- ? await rl.question('\n Enter your 64-character seed: ')
231
- : toHex(Buffer.from(generateRandomSeed()));
232
-
233
- if (choice.trim() !== '2') {
234
- fs.writeFileSync('.midnight-seed', seed, { mode: 0o600 });
235
- console.log('\n ⚠️ A new wallet seed has been generated.');
236
- console.log(' It has been saved to .midnight-seed (chmod 600).');
237
- console.log(' Back it up securely and never commit this file.\n');
169
+ // Faucet poll for public networks. The wallet has 0 tNIGHT until the user
170
+ // funds the address from the network's faucet. The display balance is
171
+ // authoritative here (unlike DUST, tNIGHT shows up immediately once the
172
+ // faucet tx lands).
173
+ if (network !== 'undeployed' && networkConfig.faucet) {
174
+ // Same balance idiom used by check-balance.ts:
175
+ // state.unshielded.balances[unshieldedToken().raw] ?? 0n
176
+ const initialBalance = await Rx.firstValueFrom(walletCtx.wallet.state().pipe(
177
+ Rx.filter((s) => s.isSynced),
178
+ ));
179
+ const initialTNight = initialBalance.unshielded.balances[unshieldedToken().raw] ?? 0n;
180
+ if (initialTNight === 0n) {
181
+ console.log('─── Fund Wallet ────────────────────────────────────────────────\n');
182
+ console.log(` Wallet address: ${address}`);
183
+ console.log(` Faucet: ${networkConfig.faucet}`);
184
+ console.log('');
185
+ console.log(' Waiting for tNIGHT to arrive (poll every 10s)...');
186
+ const rawTimeout = Number(process.env.MIDNIGHT_FAUCET_TIMEOUT_MS);
187
+ const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 600_000;
188
+ const start = Date.now();
189
+ while (true) {
190
+ await new Promise((r) => setTimeout(r, 10_000));
191
+ const s = await Rx.firstValueFrom(walletCtx.wallet.state().pipe(Rx.filter((x) => x.isSynced)));
192
+ const tn = s.unshielded.balances[unshieldedToken().raw] ?? 0n;
193
+ if (tn > 0n) {
194
+ console.log(`\n Funded! tNIGHT balance: ${tn.toLocaleString()}\n`);
195
+ break;
238
196
  }
197
+ if (Date.now() - start > timeoutMs) {
198
+ console.log(`\n ❌ Funding not received within ${Math.round(timeoutMs / 60_000)} min.`);
199
+ console.log(` Address: ${address}`);
200
+ console.log(` Faucet: ${networkConfig.faucet}`);
201
+ console.log(' Re-run setup after funding — your seed is preserved.\n');
202
+ await walletCtx.wallet.stop();
203
+ process.exit(1);
204
+ }
205
+ const elapsed = Math.round((Date.now() - start) / 1000);
206
+ process.stdout.write(`\r ...still waiting (${elapsed}s elapsed)`);
239
207
  }
240
- } else {
241
- const choice = await rl.question(' [1] Create new wallet\n [2] Restore from seed\n > ');
242
- seed = choice.trim() === '2'
243
- ? await rl.question('\n Enter your 64-character seed: ')
244
- : toHex(Buffer.from(generateRandomSeed()));
245
-
246
- if (choice.trim() !== '2') {
247
- fs.writeFileSync('.midnight-seed', seed, { mode: 0o600 });
248
- console.log('\n ⚠️ A new wallet seed has been generated.');
249
- console.log(' It has been saved to .midnight-seed (chmod 600).');
250
- console.log(' Back it up securely and never commit this file.\n');
251
- }
252
- }
253
-
254
- console.log(' Creating wallet...');
255
- const walletCtx = await createWallet(seed.trim());
256
-
257
- console.log(' Syncing with network...');
258
- console.log(' ℹ This may take several minutes depending on network size.');
259
- console.log(' RPC disconnection messages during sync are normal and can be safely ignored.\n');
260
- const syncStart = Date.now();
261
- const syncInterval = setInterval(() => {
262
- const elapsed = Math.round((Date.now() - syncStart) / 1000);
263
- process.stdout.write(`\r ⏳ Still syncing... (${elapsed}s elapsed) `);
264
- }, 5000);
265
- const state = await walletCtx.wallet.waitForSyncedState();
266
- clearInterval(syncInterval);
267
- process.stdout.write('\r ✓ Synced with network. \n');
268
- const address = walletCtx.unshieldedKeystore.getBech32Address();
269
- const balance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;
270
-
271
- console.log(`\n Wallet Address: ${address}`);
272
- console.log(` Balance: ${balance.toLocaleString()} tNight\n`);
273
-
274
- // 2. Fund wallet if needed
275
- if (balance === 0n) {
276
- console.log('─── Step 2: Fund Your Wallet ───────────────────────────────────\n');
277
- console.log(` Visit: ${CONFIG.faucetUrl}`);
278
- console.log(` Address: ${address}\n`);
279
- console.log(' Waiting for funds...');
280
-
281
- await Rx.firstValueFrom(
282
- walletCtx.wallet.state().pipe(
283
- Rx.throttleTime(10000),
284
- Rx.filter((s) => s.isSynced),
285
- Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
286
- Rx.filter((b) => b > 0n),
287
- ),
288
- );
289
- console.log(' Funds received!\n');
290
- }
291
-
292
- // 3. Register for DUST
293
- console.log('─── Step 3: DUST Token Setup ───────────────────────────────────\n');
294
- const dustState = await Rx.firstValueFrom(walletCtx.wallet.state().pipe(Rx.filter((s) => s.isSynced)));
295
-
296
- if (dustState.dust.balance(new Date()) === 0n) {
297
- const nightUtxos = dustState.unshielded.availableCoins.filter((c: any) => !c.meta?.registeredForDustGeneration);
298
- if (nightUtxos.length > 0) {
299
- console.log(' Registering for DUST generation...');
300
- const recipe = await walletCtx.wallet.registerNightUtxosForDustGeneration(
301
- nightUtxos,
302
- walletCtx.unshieldedKeystore.getPublicKey(),
303
- (payload) => walletCtx.unshieldedKeystore.signData(payload),
304
- );
305
- const signedRecipe = await walletCtx.wallet.signRecipe(recipe, (payload) => walletCtx.unshieldedKeystore.signData(payload));
306
- await walletCtx.wallet.submitTransaction(await walletCtx.wallet.finalizeRecipe(signedRecipe));
307
- }
308
-
309
- console.log(' Waiting for DUST tokens...');
310
- await Rx.firstValueFrom(
311
- walletCtx.wallet.state().pipe(Rx.throttleTime(5000), Rx.filter((s) => s.isSynced), Rx.filter((s) => s.dust.balance(new Date()) > 0n)),
312
- );
313
- }
314
- console.log(' DUST tokens ready!\n');
315
-
316
- // 4. Deploy contract
317
- console.log('─── Step 4: Deploy Contract ────────────────────────────────────\n');
318
-
319
- // Check proof server is running
320
- console.log(' Checking proof server...');
321
- const proofServerReady = await waitForProofServer();
322
- if (!proofServerReady) {
323
- console.log('\n ❌ Proof server not responding\n');
324
- console.log(' The proof server is required to generate zk-proofs for transactions.\n');
325
- console.log(' ┌─ Start it with ──────────────────────────────────────────────┐');
326
- console.log(' │ │');
327
- console.log(' │ $ docker compose up -d │');
328
- console.log(' │ │');
329
- console.log(' │ Then retry: $ npm run deploy │');
330
- console.log(' │ │');
331
- console.log(' └──────────────────────────────────────────────────────────────┘\n');
332
-
333
- // Save seed for retry
334
- fs.writeFileSync('.midnight-seed', seed, { mode: 0o600 });
335
- const partialInfo = { address, network: 'preprod', status: 'proof_server_unavailable' };
336
- fs.writeFileSync('deployment.json', JSON.stringify(partialInfo, null, 2));
337
- console.log(' Wallet saved to .midnight-seed and deployment.json\n');
338
-
339
- await walletCtx.wallet.stop();
340
- process.exit(1);
341
208
  }
342
- process.stdout.write('\r Proof server ready! \n');
343
-
344
- console.log(' Setting up providers...');
345
- const providers = await createProviders(walletCtx);
209
+ }
346
210
 
347
- console.log(' Deploying contract...\n');
211
+ // Register for DUST.
212
+ console.log('─── DUST Token Setup ───────────────────────────────────────────\n');
213
+ const dustState = await Rx.firstValueFrom(walletCtx.wallet.state().pipe(Rx.filter((s) => s.isSynced)));
214
+
215
+ const unregisteredUtxos = dustState.unshielded.availableCoins.filter(
216
+ (c: any) => !c.meta?.registeredForDustGeneration,
217
+ );
218
+ if (unregisteredUtxos.length > 0) {
219
+ console.log(` Registering ${unregisteredUtxos.length} NIGHT UTXOs for DUST generation...`);
220
+ // The signDustRegistration callback (3rd arg) already produces a recipe
221
+ // with N signatures matching N inputs. Do NOT call signRecipe again — that
222
+ // would double-sign and the chain rejects with InputsSignaturesLengthMismatch
223
+ // (Custom error 192). Matches upstream example-counter and example-bboard.
224
+ const recipe = await walletCtx.wallet.registerNightUtxosForDustGeneration(
225
+ unregisteredUtxos,
226
+ walletCtx.unshieldedKeystore.getPublicKey(),
227
+ (payload) => walletCtx.unshieldedKeystore.signData(payload),
228
+ );
229
+ const finalized = await walletCtx.wallet.finalizeRecipe(recipe);
230
+ await walletCtx.wallet.submitTransaction(finalized);
231
+ }
348
232
 
349
- const MAX_RETRIES = 8;
350
- const RETRY_DELAY_MS = 15000; // 15 seconds between retries
233
+ if (dustState.dust.balance(new Date()) === 0n) {
234
+ console.log(' Waiting for DUST tokens...');
235
+ await Rx.firstValueFrom(
236
+ walletCtx.wallet.state().pipe(
237
+ Rx.throttleTime(5000),
238
+ Rx.filter((s) => s.isSynced),
239
+ Rx.filter((s) => s.dust.balance(new Date()) > 0n),
240
+ ),
241
+ );
242
+ }
243
+ console.log(' DUST tokens ready!\n');
351
244
 
352
- let deployed: Awaited<ReturnType<typeof deployContract>> | undefined;
245
+ // Deploy.
246
+ console.log('─── Deploy Contract ────────────────────────────────────────────\n');
353
247
 
354
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
355
- try {
356
- deployed = await deployContract(providers, {
248
+ console.log(' Checking proof server...');
249
+ const proofServerReady = await waitForProofServer();
250
+ if (!proofServerReady) {
251
+ console.log('\n ❌ Proof server not responding. Run: docker compose up -d\n');
252
+ await walletCtx.wallet.stop();
253
+ process.exit(1);
254
+ }
255
+ process.stdout.write('\r Proof server ready! \n');
256
+
257
+ console.log(' Setting up providers...');
258
+ const providers = await createProviders(walletCtx);
259
+
260
+ // The wallet's reported DUST balance is a *time-projection* of what its
261
+ // registered NIGHT will eventually generate; the tx-builder spends only
262
+ // what the next block's timestamp accounts for, which lags wall-clock by
263
+ // ~1 block on a fresh devnet. Sleeping ~1 block-time before attempt 1
264
+ // closes that gap in the common case; the retry loop covers outliers.
265
+ process.stdout.write(' Generating DUST...');
266
+ await new Promise((r) => setTimeout(r, 6000));
267
+ process.stdout.write(' done.\n');
268
+
269
+ console.log(' Deploying contract...\n');
270
+
271
+ // Fallback timing. The 6s pre-pause above handles the common case; this
272
+ // loop covers genuine outliers (slow blocks, proof-server worker-pool
273
+ // settling). Earlier 2s retries caused CI flakes where attempt 2's /prove
274
+ // hit the proof-server before it had drained attempt 1's state — 5s gives
275
+ // it room to settle between attempts. 20 × 5 = 100s total budget.
276
+ const MAX_RETRIES = 20;
277
+ const RETRY_DELAY_MS = 5000;
278
+ let deployed: Awaited<ReturnType<typeof deployContract>> | undefined;
279
+
280
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
281
+ try {
282
+ deployed = await deployContract(providers, {
357
283
  compiledContract: compiledContract as any,
358
284
  args: [],
359
285
  });
360
- break; // Success - exit retry loop
361
- } catch (err: any) {
362
- const errMsg = err?.message || err?.toString() || '';
363
- const errCause = err?.cause?.message || err?.cause?.toString() || '';
364
- const fullError = `${errMsg} ${errCause}`;
365
-
366
- // Check for proof server errors first
367
- if (fullError.includes('Failed to connect to Proof Server') ||
368
- fullError.includes('Failed to prove') ||
369
- fullError.includes('127.0.0.1:6300')) {
370
- console.log(' ❌ Proof server error\n');
371
- console.log(' The proof server may have stopped or crashed.\n');
372
- console.log(' ┌─ Fix ────────────────────────────────────────────────────────┐');
373
- console.log(' │ │');
374
- console.log(' │ 1. Check if running: $ docker ps │');
375
- console.log(' │ 2. Restart: $ docker compose up -d │');
376
- console.log(' │ 3. Retry: $ npm run deploy │');
377
- console.log(' │ │');
378
- console.log(' └──────────────────────────────────────────────────────────────┘\n');
379
-
380
- fs.writeFileSync('.midnight-seed', seed, { mode: 0o600 });
381
- const partialInfo = { address, network: 'preprod', status: 'proof_server_error' };
382
- fs.writeFileSync('deployment.json', JSON.stringify(partialInfo, null, 2));
383
- console.log(' Wallet saved to .midnight-seed and deployment.json\n');
286
+ break;
287
+ } catch (err: any) {
288
+ const errMsg = err?.message || err?.toString() || '';
289
+ const errCause = err?.cause?.message || err?.cause?.toString() || '';
290
+ const fullError = `${errMsg} ${errCause}`;
291
+
292
+ // DUST shortage is the most common transient failure on a fresh devnet —
293
+ // check it BEFORE proof-server connectivity, because dust-balancing errors
294
+ // can surface through proof-server-shaped messages (the wallet talks to
295
+ // the proof-server while building the dust portion of the tx).
296
+ const isDustShortage =
297
+ fullError.includes('Not enough Dust') ||
298
+ fullError.includes('Insufficient Funds') ||
299
+ fullError.includes('could not balance dust');
300
+
301
+ // Quiet the first DUST-shortage retry: it's the expected race between
302
+ // wall-clock projection and block-timestamp accounting and the loud
303
+ // `Insufficient Funds: <huge number>` message scares first-time users.
304
+ // Real failures still get the full diagnostic from attempt 2 onward.
305
+ if (!(isDustShortage && attempt === 1)) {
306
+ console.error(`\n Attempt ${attempt} error: ${errMsg}`);
307
+ if (errCause && errCause !== errMsg) console.error(` Cause: ${errCause}`);
308
+ }
384
309
 
385
- await walletCtx.wallet.stop();
386
- process.exit(1);
387
- }
310
+ if (
311
+ !isDustShortage &&
312
+ (fullError.includes('Failed to connect to Proof Server') ||
313
+ fullError.includes('connect ECONNREFUSED 127.0.0.1:6300'))
314
+ ) {
315
+ console.log(' ❌ Proof server unreachable. Run: docker compose up -d\n');
316
+ await walletCtx.wallet.stop();
317
+ process.exit(1);
318
+ }
388
319
 
389
- // Check if it's a DUST-related error (must check "Not enough Dust" specifically)
390
- if (fullError.includes('Not enough Dust')) {
391
- // Get current DUST balance
392
- const currentState = await walletCtx.wallet.waitForSyncedState();
393
- const dustBalance = currentState.dust.balance(new Date());
394
-
395
- if (attempt < MAX_RETRIES) {
396
- console.log(` ⏳ DUST balance: ${dustBalance.toLocaleString()} (need more for tx fees)`);
397
- console.log(` Attempt ${attempt}/${MAX_RETRIES} - waiting for DUST to accumulate...`);
398
-
399
- // Countdown display
400
- for (let i = RETRY_DELAY_MS / 1000; i > 0; i -= 5) {
401
- process.stdout.write(`\r Retrying in ${i}s... `);
402
- await new Promise((r) => setTimeout(r, 5000));
403
- }
404
- process.stdout.write('\r \r\n');
320
+ if (isDustShortage) {
321
+ const currentState = await walletCtx.wallet.waitForSyncedState();
322
+ const dustBalance = currentState.dust.balance(new Date());
323
+ if (attempt < MAX_RETRIES) {
324
+ if (attempt === 1) {
325
+ console.log(` Still generating DUST, retrying in ${RETRY_DELAY_MS / 1000}s...`);
405
326
  } else {
406
- // All retries exhausted
407
- console.log(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
408
- console.log(' ❌ Not enough DUST for transaction fees\n');
409
- console.log(` Current DUST: ${dustBalance.toLocaleString()}`);
410
- console.log(' This is a new wallet - DUST generates over time.\n');
411
- console.log(' ┌─ Options ─────────────────────────────────────────────────┐');
412
- console.log(' │ │');
413
- console.log(' │ [1] Wait & retry $ npm run deploy │');
414
- console.log(' │ (DUST accumulates as blocks are produced) │');
415
- console.log(' │ │');
416
- console.log(' │ [2] Send DUST from existing wallet to this address: │');
417
- console.log(` │ ${address.toString().slice(0, 50)}... │`);
418
- console.log(' │ │');
419
- console.log(' │ [3] Import wallet with DUST (choose option 2 on retry) │');
420
- console.log(' │ │');
421
- console.log(' └───────────────────────────────────────────────────────────┘\n');
422
-
423
- // Save partial deployment info so user can resume
424
- fs.writeFileSync('.midnight-seed', seed, { mode: 0o600 });
425
- const partialInfo = {
426
- address,
427
- network: 'preprod',
428
- status: 'pending_dust',
429
- lastAttempt: new Date().toISOString(),
430
- };
431
- fs.writeFileSync('deployment.json', JSON.stringify(partialInfo, null, 2));
432
- console.log(' Wallet saved to .midnight-seed and deployment.json\n');
433
-
434
- await walletCtx.wallet.stop();
435
- process.exit(1);
327
+ console.log(` ⏳ DUST balance: ${dustBalance.toLocaleString()} (attempt ${attempt}/${MAX_RETRIES}); retrying in ${RETRY_DELAY_MS / 1000}s...`);
436
328
  }
329
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
437
330
  } else {
438
- // Not a DUST error - rethrow
439
- throw err;
331
+ console.log(` ❌ Not enough DUST after ${MAX_RETRIES} retries (current: ${dustBalance.toLocaleString()})`);
332
+ await walletCtx.wallet.stop();
333
+ process.exit(1);
440
334
  }
335
+ } else {
336
+ throw err;
441
337
  }
442
338
  }
339
+ }
443
340
 
444
- if (!deployed) {
445
- throw new Error('Deployment failed after all retries');
446
- }
447
-
448
- const contractAddress = deployed.deployTxData.public.contractAddress;
449
- console.log(' ✅ Contract deployed successfully!\n');
450
- console.log(` Contract Address: ${contractAddress}\n`);
341
+ if (!deployed) throw new Error('Deployment failed after all retries');
451
342
 
452
- // 5. Save deployment info
453
- fs.writeFileSync('.midnight-seed', seed, { mode: 0o600 });
454
- const deploymentInfo = {
455
- contractAddress,
456
- network: 'preprod',
457
- deployedAt: new Date().toISOString(),
458
- };
343
+ const contractAddress = deployed.deployTxData.public.contractAddress;
344
+ console.log(' Contract deployed successfully!\n');
345
+ console.log(` Contract Address: ${contractAddress}\n`);
459
346
 
460
- fs.writeFileSync('deployment.json', JSON.stringify(deploymentInfo, null, 2));
461
- console.log(' Saved to deployment.json\n');
347
+ recordDeployment(network, contractAddress, address.toString());
348
+ console.log(' Saved to .midnight-state.json\n');
462
349
 
463
- await walletCtx.wallet.stop();
464
- console.log('─── Deployment Complete! ───────────────────────────────────────\n');
465
- console.log(' Next: Run `npm run cli` to interact with your contract.\n');
466
- } finally {
467
- rl.close();
468
- }
350
+ await persistWalletState(network, walletCtx);
351
+ await walletCtx.wallet.stop();
352
+ console.log('─── Deployment complete ────────────────────────────────────────\n');
353
+ console.log(' Next: npm run cli\n');
469
354
  }
470
355
 
471
- main().catch(console.error);
356
+ main().catch((err) => {
357
+ console.error(err);
358
+ process.exit(1);
359
+ });