create-mn-app 0.3.28 → 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.
- package/README.md +75 -89
- package/dist/utils/templates.js +4 -4
- package/package.json +1 -1
- package/templates/hello-world/README.md.template +145 -60
- package/templates/hello-world/_gitignore +3 -1
- package/templates/hello-world/contracts/hello-world.compact.template +1 -1
- package/templates/hello-world/docker-compose.yml.template +66 -5
- package/templates/hello-world/package.json.template +17 -15
- package/templates/hello-world/scripts/e2e-check.ts.template +158 -0
- package/templates/hello-world/src/check-balance.ts.template +23 -36
- package/templates/hello-world/src/cli.ts.template +34 -32
- package/templates/hello-world/src/deploy.ts.template +241 -310
- package/templates/hello-world/src/network.ts +320 -0
- package/templates/hello-world/src/setup.ts.template +37 -0
- package/templates/hello-world/tsconfig.json.template +1 -1
- package/dist/installers/wallet-generator.d.ts +0 -4
- package/dist/installers/wallet-generator.d.ts.map +0 -1
- package/dist/installers/wallet-generator.js +0 -78
- package/dist/installers/wallet-generator.js.map +0 -1
- package/dist/test.d.ts +0 -2
- package/dist/test.d.ts.map +0 -1
- package/dist/test.js +0 -65
- package/dist/test.js.map +0 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deploy {{projectName}} contract to Midnight
|
|
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
|
|
26
|
+
import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
|
|
26
27
|
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
|
|
27
|
-
import { createKeystore,
|
|
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
|
-
//
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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 =
|
|
49
|
+
async function waitForProofServer(maxAttempts = 60, delayMs = 2000): Promise<boolean> {
|
|
49
50
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
50
51
|
try {
|
|
51
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
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:
|
|
115
|
-
provingServerUrl: new URL(
|
|
116
|
-
relayURL: new URL(
|
|
117
|
-
txHistoryStorage: new
|
|
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) =>
|
|
125
|
-
|
|
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
|
-
|
|
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,311 +163,240 @@ async function createProviders(walletCtx: ReturnType<typeof createWallet> extend
|
|
|
161
163
|
accountId,
|
|
162
164
|
privateStoragePasswordProvider: () => privateStatePassword,
|
|
163
165
|
}),
|
|
164
|
-
publicDataProvider: indexerPublicDataProvider(
|
|
166
|
+
publicDataProvider: indexerPublicDataProvider(networkConfig.indexer, networkConfig.indexerWS),
|
|
165
167
|
zkConfigProvider,
|
|
166
|
-
proofProvider: httpClientProofProvider(
|
|
168
|
+
proofProvider: httpClientProofProvider(networkConfig.proofServer, zkConfigProvider),
|
|
167
169
|
walletProvider,
|
|
168
170
|
midnightProvider: walletProvider,
|
|
169
171
|
};
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
// ─── Main
|
|
174
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
173
175
|
|
|
174
176
|
async function main() {
|
|
175
177
|
console.log('\n╔══════════════════════════════════════════════════════════════╗');
|
|
176
|
-
console.log(
|
|
178
|
+
console.log(`║ Deploy {{projectName}} to ${network}`);
|
|
177
179
|
console.log('╚══════════════════════════════════════════════════════════════╝\n');
|
|
178
180
|
|
|
179
|
-
const
|
|
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
|
-
|
|
216
|
-
|
|
183
|
+
console.log('─── Wallet setup ───────────────────────────────────────────────\n');
|
|
184
|
+
console.log(' Creating wallet...');
|
|
185
|
+
const walletCtx = await createWallet(seed);
|
|
217
186
|
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
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
252
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
console.log(' Setting up providers...');
|
|
345
|
-
const providers = await createProviders(walletCtx);
|
|
253
|
+
}
|
|
346
254
|
|
|
347
|
-
|
|
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
|
+
}
|
|
348
276
|
|
|
349
|
-
|
|
350
|
-
|
|
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');
|
|
351
288
|
|
|
352
|
-
|
|
289
|
+
// Deploy.
|
|
290
|
+
console.log('─── Deploy Contract ────────────────────────────────────────────\n');
|
|
353
291
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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, {
|
|
357
327
|
compiledContract: compiledContract as any,
|
|
358
328
|
args: [],
|
|
359
329
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
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
|
+
}
|
|
384
353
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
}
|
|
388
363
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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');
|
|
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...`);
|
|
405
370
|
} else {
|
|
406
|
-
|
|
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);
|
|
371
|
+
console.log(` ⏳ DUST balance: ${dustBalance.toLocaleString()} (attempt ${attempt}/${MAX_RETRIES}); retrying in ${RETRY_DELAY_MS / 1000}s...`);
|
|
436
372
|
}
|
|
373
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
437
374
|
} else {
|
|
438
|
-
|
|
439
|
-
|
|
375
|
+
console.log(` ❌ Not enough DUST after ${MAX_RETRIES} retries (current: ${dustBalance.toLocaleString()})`);
|
|
376
|
+
await walletCtx.wallet.stop();
|
|
377
|
+
process.exit(1);
|
|
440
378
|
}
|
|
379
|
+
} else {
|
|
380
|
+
throw err;
|
|
441
381
|
}
|
|
442
382
|
}
|
|
383
|
+
}
|
|
443
384
|
|
|
444
|
-
|
|
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`);
|
|
385
|
+
if (!deployed) throw new Error('Deployment failed after all retries');
|
|
451
386
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
contractAddress,
|
|
456
|
-
network: 'preprod',
|
|
457
|
-
deployedAt: new Date().toISOString(),
|
|
458
|
-
};
|
|
387
|
+
const contractAddress = deployed.deployTxData.public.contractAddress;
|
|
388
|
+
console.log(' ✅ Contract deployed successfully!\n');
|
|
389
|
+
console.log(` Contract Address: ${contractAddress}\n`);
|
|
459
390
|
|
|
460
|
-
|
|
461
|
-
|
|
391
|
+
recordDeployment(network, contractAddress, address.toString());
|
|
392
|
+
console.log(' Saved to .midnight-state.json\n');
|
|
462
393
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
} finally {
|
|
467
|
-
rl.close();
|
|
468
|
-
}
|
|
394
|
+
await walletCtx.wallet.stop();
|
|
395
|
+
console.log('─── Deployment complete ────────────────────────────────────────\n');
|
|
396
|
+
console.log(' Next: npm run cli\n');
|
|
469
397
|
}
|
|
470
398
|
|
|
471
|
-
main().catch(
|
|
399
|
+
main().catch((err) => {
|
|
400
|
+
console.error(err);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
});
|