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.
- package/README.md +75 -89
- package/dist/utils/templates.js +4 -4
- package/package.json +1 -1
- package/templates/hello-world/README.md.template +160 -60
- package/templates/hello-world/_gitignore +6 -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 +124 -0
- package/templates/hello-world/src/check-balance.ts.template +26 -84
- package/templates/hello-world/src/cli.ts.template +40 -76
- package/templates/hello-world/src/deploy.ts.template +236 -348
- package/templates/hello-world/src/network.ts +320 -0
- package/templates/hello-world/src/setup.ts.template +37 -0
- package/templates/hello-world/src/wallet-state.ts +95 -0
- package/templates/hello-world/src/wallet.ts +189 -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,14 +1,16 @@
|
|
|
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';
|
|
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
|
-
//
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
const
|
|
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
|
|
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 =
|
|
40
|
+
async function waitForProofServer(maxAttempts = 60, delayMs = 2000): Promise<boolean> {
|
|
49
41
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
50
42
|
try {
|
|
51
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
// ───
|
|
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(
|
|
115
|
+
publicDataProvider: indexerPublicDataProvider(networkConfig.indexer, networkConfig.indexerWS),
|
|
165
116
|
zkConfigProvider,
|
|
166
|
-
proofProvider: httpClientProofProvider(
|
|
117
|
+
proofProvider: httpClientProofProvider(networkConfig.proofServer, zkConfigProvider),
|
|
167
118
|
walletProvider,
|
|
168
119
|
midnightProvider: walletProvider,
|
|
169
120
|
};
|
|
170
121
|
}
|
|
171
122
|
|
|
172
|
-
// ─── Main
|
|
123
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
173
124
|
|
|
174
125
|
async function main() {
|
|
175
126
|
console.log('\n╔══════════════════════════════════════════════════════════════╗');
|
|
176
|
-
console.log(
|
|
127
|
+
console.log(`║ Deploy {{projectName}} to ${network}`);
|
|
177
128
|
console.log('╚══════════════════════════════════════════════════════════════╝\n');
|
|
178
129
|
|
|
179
|
-
const
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
console.log(' Setting up providers...');
|
|
345
|
-
const providers = await createProviders(walletCtx);
|
|
209
|
+
}
|
|
346
210
|
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
245
|
+
// Deploy.
|
|
246
|
+
console.log('─── Deploy Contract ────────────────────────────────────────────\n');
|
|
353
247
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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');
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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');
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
461
|
-
|
|
347
|
+
recordDeployment(network, contractAddress, address.toString());
|
|
348
|
+
console.log(' Saved to .midnight-state.json\n');
|
|
462
349
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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(
|
|
356
|
+
main().catch((err) => {
|
|
357
|
+
console.error(err);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
});
|