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