create-mn-app 0.4.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mn-app",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Create Midnight Network applications with zero configuration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -128,6 +128,19 @@ npm run network undeployed # or: npm run setup -- --network undeployed
128
128
  Your preview/preprod wallet seeds and deploy addresses stay in
129
129
  `.midnight-state.json`. Switch back later, and they're still there.
130
130
 
131
+ ### Wallet sync cache
132
+
133
+ After each `deploy`, `cli`, or `check-balance` run, the scripts serialize the
134
+ wallet's synced state to `.midnight-wallet-state/<network>/` (gitignored).
135
+ The next run on the same network restores from that snapshot and only catches
136
+ up to the latest block instead of replaying from genesis — meaningful on
137
+ `preview` / `preprod` where a from-seed sync takes minutes.
138
+
139
+ If the cache is stale or corrupt (e.g. after an SDK upgrade with an
140
+ incompatible state format) the wallet falls back to a fresh from-seed sync
141
+ with a one-line warning. `npm run clean` removes the cache along with other
142
+ generated state.
143
+
131
144
  ## Available scripts
132
145
 
133
146
  | Script | Description |
@@ -138,7 +151,7 @@ Your preview/preprod wallet seeds and deploy addresses stay in
138
151
  | `npm run cli` | Interactive CLI to call circuits on the deployed contract. |
139
152
  | `npm run check-balance` | Print the genesis-seed wallet's NIGHT and DUST balances. |
140
153
  | `npm run test:e2e` | Smoke + read-back check against the deployed contract. |
141
- | `npm run clean` | Remove `contracts/managed/` and `.midnight-state.json`. |
154
+ | `npm run clean` | Remove `contracts/managed/`, `.midnight-state.json`, and `.midnight-wallet-state/`. |
142
155
  | `npm run proof-server:start` / `:stop` | Compose lifecycle for just the proof-server service. |
143
156
 
144
157
  ## Project structure
@@ -151,12 +164,14 @@ Your preview/preprod wallet seeds and deploy addresses stay in
151
164
  │ └── e2e-check.ts # smoke + read-back
152
165
  ├── src/
153
166
  │ ├── network.ts # network selection + state file management
167
+ │ ├── wallet.ts # wallet construction + sync-state cache
154
168
  │ ├── setup.ts # orchestrator for `npm run setup`
155
169
  │ ├── deploy.ts # deploy the contract
156
170
  │ ├── cli.ts # interact with deployed contract
157
171
  │ └── check-balance.ts # NIGHT / DUST balance
158
172
  ├── docker-compose.yml # node + indexer + proof-server
159
173
  ├── .midnight-state.json # written by deploy (gitignored)
174
+ ├── .midnight-wallet-state/ # serialized sync state per network (gitignored)
160
175
  ├── package.json
161
176
  └── tsconfig.json
162
177
  ```
@@ -55,3 +55,6 @@ temp/
55
55
 
56
56
  # Midnight network state (per-network seeds, deploy addresses)
57
57
  .midnight-state.json
58
+
59
+ # Midnight wallet sync state (per-network serialized wallet state cache)
60
+ .midnight-wallet-state/
@@ -15,7 +15,7 @@
15
15
  "network": "npx tsx src/network.ts",
16
16
  "proof-server:start": "docker compose up -d",
17
17
  "proof-server:stop": "docker compose down",
18
- "clean": "rm -rf contracts/managed .midnight-state.json",
18
+ "clean": "rm -rf contracts/managed .midnight-state.json .midnight-wallet-state",
19
19
  "test": "echo \"Error: no test specified\" && exit 1",
20
20
  "test:e2e": "tsx scripts/e2e-check.ts"
21
21
  },
@@ -8,21 +8,14 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
10
  import { WebSocket } from 'ws';
11
- import { Buffer } from 'buffer';
12
11
 
13
12
  import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
14
13
  import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
15
14
  import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
16
15
  import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
17
16
  import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
18
- import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
19
17
  import { resolveNetwork, getOrCreateSeed, getDeployment } from '../src/network';
20
- import * as ledger from '@midnight-ntwrk/ledger-v8';
21
- import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
22
- import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
23
- import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
24
- import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
25
- import { createKeystore, NoOpTransactionHistoryStorage, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
18
+ import { createWallet, persistWalletState } from '../src/wallet';
26
19
  import { CompiledContract } from '@midnight-ntwrk/compact-js';
27
20
 
28
21
  // @ts-expect-error wallet sync requires WebSocket
@@ -31,7 +24,6 @@ globalThis.WebSocket = WebSocket;
31
24
  // ─── Network configuration ─────────────────────────────────────────────────────
32
25
 
33
26
  const { network, config: networkConfig } = resolveNetwork();
34
- setNetworkId(networkConfig.networkId);
35
27
  const SEED = getOrCreateSeed(network);
36
28
 
37
29
  function fail(msg: string): never {
@@ -54,7 +46,7 @@ async function main() {
54
46
  fail(`Deployment address missing or invalid: ${JSON.stringify(deployment, null, 2)}`);
55
47
  }
56
48
 
57
- // 2. Build wallet (genesis seed) and providers
49
+ // 2. Build wallet and providers
58
50
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
59
51
  const zkConfigPath = path.resolve(__dirname, '..', 'contracts', 'managed', 'hello-world');
60
52
  const contractPath = path.join(zkConfigPath, 'contract', 'index.js');
@@ -65,37 +57,11 @@ async function main() {
65
57
  CompiledContract.withCompiledFileAssets(zkConfigPath),
66
58
  );
67
59
 
68
- const hd = HDWallet.fromSeed(Buffer.from(SEED, 'hex'));
69
- if (hd.type !== 'seedOk') fail('Bad seed.');
70
- const derived = hd.hdWallet
71
- .selectAccount(0)
72
- .selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
73
- .deriveKeysAt(0);
74
- if (derived.type !== 'keysDerived') fail('Key derivation failed.');
75
- hd.hdWallet.clear();
76
-
77
- const networkId = getNetworkId();
78
- const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(derived.keys[Roles.Zswap]);
79
- const dustSecretKey = ledger.DustSecretKey.fromSeed(derived.keys[Roles.Dust]);
80
- const unshieldedKeystore = createKeystore(derived.keys[Roles.NightExternal], networkId);
81
-
82
- const wallet = await WalletFacade.init({
83
- configuration: {
84
- networkId,
85
- indexerClientConnection: { indexerHttpUrl: networkConfig.indexer, indexerWsUrl: networkConfig.indexerWS },
86
- provingServerUrl: new URL(networkConfig.proofServer),
87
- relayURL: new URL(networkConfig.node.replace(/^http/, 'ws')),
88
- txHistoryStorage: new NoOpTransactionHistoryStorage(),
89
- costParameters: { additionalFeeOverhead: 300_000_000_000_000n, feeBlocksMargin: 5 },
90
- },
91
- shielded: async (c) => ShieldedWallet(c).startWithSecretKeys(shieldedSecretKeys),
92
- unshielded: async (c) =>
93
- UnshieldedWallet(c).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
94
- dust: async (c) =>
95
- DustWallet(c).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust),
96
- });
97
- await wallet.start(shieldedSecretKeys, dustSecretKey);
98
- const state = await wallet.waitForSyncedState();
60
+ const walletCtx = await createWallet({ network, networkConfig, seed: SEED });
61
+ const state = await walletCtx.wallet.waitForSyncedState();
62
+ // Persist the sync state — saves time on the next e2e-check invocation in CI
63
+ // when run against the same persistent wallet directory.
64
+ await persistWalletState(network, walletCtx);
99
65
 
100
66
  const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath);
101
67
  const walletProvider = {
@@ -112,7 +78,7 @@ async function main() {
112
78
  const providers = {
113
79
  privateStateProvider: levelPrivateStateProvider({
114
80
  privateStateStoreName: 'hello-world-state',
115
- accountId: unshieldedKeystore.getBech32Address().toString(),
81
+ accountId: walletCtx.unshieldedKeystore.getBech32Address().toString(),
116
82
  // SDK requires ≥16 chars. e2e-check is read-only so we don't expose
117
83
  // the env-var override here — match the deploy script's local-devnet default.
118
84
  privateStoragePasswordProvider: () => 'Local-Devnet-Development-Placeholder-1',
@@ -131,7 +97,7 @@ async function main() {
131
97
  compiledContract: compiledContract as any,
132
98
  });
133
99
  } catch (err: any) {
134
- await wallet.stop();
100
+ await walletCtx.wallet.stop();
135
101
  fail(`findDeployedContract threw: ${err?.message ?? err}`);
136
102
  }
137
103
 
@@ -140,7 +106,7 @@ async function main() {
140
106
  // we know how to construct the local handle.
141
107
  const onChainState = await providers.publicDataProvider.queryContractState(deployment.address);
142
108
  if (!onChainState) {
143
- await wallet.stop();
109
+ await walletCtx.wallet.stop();
144
110
  fail(`queryContractState returned null for ${deployment.address}`);
145
111
  }
146
112
 
@@ -148,7 +114,7 @@ async function main() {
148
114
  console.log(` contractAddress: ${deployment.address}`);
149
115
  console.log(` network: ${network}`);
150
116
 
151
- await wallet.stop();
117
+ await walletCtx.wallet.stop();
152
118
  process.exit(0);
153
119
  }
154
120
 
@@ -2,19 +2,11 @@
2
2
  * Check wallet balance on the local Midnight devnet
3
3
  */
4
4
  import { WebSocket } from 'ws';
5
- import * as Rx from 'rxjs';
6
- import { Buffer } from 'buffer';
7
5
 
8
6
  // Midnight SDK imports
9
- import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
10
- import * as ledger from '@midnight-ntwrk/ledger-v8';
11
7
  import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
12
- import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
13
- import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
14
- import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
15
- import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
16
- import { createKeystore, NoOpTransactionHistoryStorage, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
17
8
  import { resolveNetwork, getOrCreateSeed } from './network';
9
+ import { createWallet, persistWalletState } from './wallet';
18
10
 
19
11
  // Enable WebSocket for GraphQL subscriptions
20
12
  // @ts-expect-error Required for wallet sync
@@ -23,48 +15,8 @@ globalThis.WebSocket = WebSocket;
23
15
  // ─── Network configuration ─────────────────────────────────────────────────────
24
16
 
25
17
  const { network, config: networkConfig } = resolveNetwork();
26
- setNetworkId(networkConfig.networkId);
27
18
  const SEED = getOrCreateSeed(network);
28
19
 
29
- // ─── Wallet Functions ──────────────────────────────────────────────────────────
30
-
31
- function deriveKeys(seed: string) {
32
- const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
33
- if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
34
- const result = hdWallet.hdWallet.selectAccount(0).selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust]).deriveKeysAt(0);
35
- if (result.type !== 'keysDerived') throw new Error('Key derivation failed');
36
- hdWallet.hdWallet.clear();
37
- return result.keys;
38
- }
39
-
40
- async function createWallet(seed: string) {
41
- const keys = deriveKeys(seed);
42
- const networkId = getNetworkId();
43
- const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
44
- const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
45
- const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], networkId);
46
-
47
- const walletConfig = {
48
- networkId,
49
- indexerClientConnection: { indexerHttpUrl: networkConfig.indexer, indexerWsUrl: networkConfig.indexerWS },
50
- provingServerUrl: new URL(networkConfig.proofServer),
51
- relayURL: new URL(networkConfig.node.replace(/^http/, 'ws')),
52
- txHistoryStorage: new NoOpTransactionHistoryStorage(),
53
- costParameters: { additionalFeeOverhead: 300_000_000_000_000n, feeBlocksMargin: 5 },
54
- };
55
-
56
- const wallet = await WalletFacade.init({
57
- configuration: walletConfig,
58
- shielded: async (config) => ShieldedWallet(config).startWithSecretKeys(shieldedSecretKeys),
59
- unshielded: async (config) => UnshieldedWallet(config).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
60
- dust: async (config) => DustWallet(config).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust),
61
- });
62
-
63
- await wallet.start(shieldedSecretKeys, dustSecretKey);
64
-
65
- return { wallet, unshieldedKeystore };
66
- }
67
-
68
20
  // ─── Main ──────────────────────────────────────────────────────────────────────
69
21
 
70
22
  async function main() {
@@ -72,11 +24,13 @@ async function main() {
72
24
  console.log('║ Wallet Balance Checker ║');
73
25
  console.log('╚══════════════════════════════════════════════════════════════╝\n');
74
26
 
75
- const seed = SEED;
76
-
77
27
  try {
78
28
  console.log(' Building wallet...');
79
- const { wallet, unshieldedKeystore } = await createWallet(seed);
29
+ const walletCtx = await createWallet({ network, networkConfig, seed: SEED });
30
+ const restoredCount = Object.values(walletCtx.restored).filter(Boolean).length;
31
+ if (restoredCount > 0) {
32
+ console.log(` Restored ${restoredCount}/3 child wallets from .midnight-wallet-state — sync will resume from saved point.`);
33
+ }
80
34
 
81
35
  console.log(' Syncing with network...');
82
36
  console.log(' ℹ This may take several minutes depending on network size.');
@@ -86,11 +40,11 @@ async function main() {
86
40
  const elapsed = Math.round((Date.now() - syncStart) / 1000);
87
41
  process.stdout.write(`\r ⏳ Still syncing... (${elapsed}s elapsed) `);
88
42
  }, 5000);
89
- const state = await wallet.waitForSyncedState();
43
+ const state = await walletCtx.wallet.waitForSyncedState();
90
44
  clearInterval(syncInterval);
91
45
  process.stdout.write('\r ✓ Synced with network. \n');
92
46
 
93
- const address = unshieldedKeystore.getBech32Address();
47
+ const address = walletCtx.unshieldedKeystore.getBech32Address();
94
48
  const tNightBalance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;
95
49
  const dustBalance = state.dust.balance(new Date());
96
50
 
@@ -117,7 +71,8 @@ async function main() {
117
71
  console.log(' ✅ Wallet is funded and ready!\n');
118
72
  }
119
73
 
120
- await wallet.stop();
74
+ await persistWalletState(network, walletCtx);
75
+ await walletCtx.wallet.stop();
121
76
  } catch (error) {
122
77
  console.error('\n❌ Error:', error instanceof Error ? error.message : error);
123
78
  process.exit(1);
@@ -7,7 +7,6 @@ import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { WebSocket } from 'ws';
10
- import * as Rx from 'rxjs';
11
10
  import { Buffer } from 'buffer';
12
11
 
13
12
  // Midnight SDK imports
@@ -16,15 +15,8 @@ import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client
16
15
  import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
17
16
  import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
18
17
  import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
19
- import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
20
18
  import { resolveNetwork, getOrCreateSeed, getDeployment } from './network';
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 } from '@midnight-ntwrk/wallet-sdk-hd';
26
- import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
27
- import { createKeystore, NoOpTransactionHistoryStorage, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
19
+ import { createWallet, persistWalletState, unshieldedToken, type WalletContext } from './wallet';
28
20
  import { CompiledContract } from '@midnight-ntwrk/compact-js';
29
21
 
30
22
  // Enable WebSocket for GraphQL subscriptions
@@ -32,7 +24,6 @@ import { CompiledContract } from '@midnight-ntwrk/compact-js';
32
24
  globalThis.WebSocket = WebSocket;
33
25
 
34
26
  const { network, config: networkConfig } = resolveNetwork();
35
- setNetworkId(networkConfig.networkId);
36
27
  const SEED = getOrCreateSeed(network);
37
28
 
38
29
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -54,46 +45,9 @@ const compiledContract = CompiledContract.make('hello-world', HelloWorld.Contrac
54
45
  CompiledContract.withCompiledFileAssets(zkConfigPath),
55
46
  );
56
47
 
57
- // ─── Wallet Functions ──────────────────────────────────────────────────────────
48
+ // ─── Providers ─────────────────────────────────────────────────────────────────
58
49
 
59
- function deriveKeys(seed: string) {
60
- const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
61
- if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
62
- const result = hdWallet.hdWallet.selectAccount(0).selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust]).deriveKeysAt(0);
63
- if (result.type !== 'keysDerived') throw new Error('Key derivation failed');
64
- hdWallet.hdWallet.clear();
65
- return result.keys;
66
- }
67
-
68
- async function createWallet(seed: string) {
69
- const keys = deriveKeys(seed);
70
- const networkId = getNetworkId();
71
- const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
72
- const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
73
- const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], networkId);
74
-
75
- const walletConfig = {
76
- networkId,
77
- indexerClientConnection: { indexerHttpUrl: networkConfig.indexer, indexerWsUrl: networkConfig.indexerWS },
78
- provingServerUrl: new URL(networkConfig.proofServer),
79
- relayURL: new URL(networkConfig.node.replace(/^http/, 'ws')),
80
- txHistoryStorage: new NoOpTransactionHistoryStorage(),
81
- costParameters: { additionalFeeOverhead: 300_000_000_000_000n, feeBlocksMargin: 5 },
82
- };
83
-
84
- const wallet = await WalletFacade.init({
85
- configuration: walletConfig,
86
- shielded: async (config) => ShieldedWallet(config).startWithSecretKeys(shieldedSecretKeys),
87
- unshielded: async (config) => UnshieldedWallet(config).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
88
- dust: async (config) => DustWallet(config).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust),
89
- });
90
-
91
- await wallet.start(shieldedSecretKeys, dustSecretKey);
92
-
93
- return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
94
- }
95
-
96
- async function createProviders(walletCtx: ReturnType<typeof createWallet> extends Promise<infer T> ? T : never) {
50
+ async function createProviders(walletCtx: WalletContext) {
97
51
  // The SDK requires the private-state password to be at least 16 characters.
98
52
  // The default below is a placeholder for local devnet only — set a strong
99
53
  // password via PRIVATE_STATE_PASSWORD when you move to a non-local target.
@@ -157,7 +111,11 @@ async function main() {
157
111
  const seed = SEED;
158
112
 
159
113
  console.log(' Connecting to wallet...');
160
- const walletCtx = await createWallet(seed);
114
+ const walletCtx = await createWallet({ network, networkConfig, seed });
115
+ const restoredCount = Object.values(walletCtx.restored).filter(Boolean).length;
116
+ if (restoredCount > 0) {
117
+ console.log(` Restored ${restoredCount}/3 child wallets from .midnight-wallet-state — sync will resume from saved point.`);
118
+ }
161
119
 
162
120
  console.log(' Syncing with network...');
163
121
  console.log(' ℹ This may take several minutes depending on network size.');
@@ -170,6 +128,9 @@ async function main() {
170
128
  const state = await walletCtx.wallet.waitForSyncedState();
171
129
  clearInterval(syncInterval);
172
130
  process.stdout.write('\r ✓ Synced with network. \n');
131
+
132
+ // Persist sync state so the next run doesn't have to redo this work.
133
+ await persistWalletState(network, walletCtx);
173
134
  const balance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;
174
135
  console.log(` Balance: ${balance.toLocaleString()} tNight\n`);
175
136
 
@@ -258,6 +219,7 @@ async function main() {
258
219
  }
259
220
  }
260
221
 
222
+ await persistWalletState(network, walletCtx);
261
223
  await walletCtx.wallet.stop();
262
224
  } catch (error) {
263
225
  console.error('\n❌ Error:', error instanceof Error ? error.message : error);
@@ -7,10 +7,10 @@
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { resolveNetwork, getOrCreateSeed, recordDeployment } from './network';
10
+ import { createWallet, persistWalletState, unshieldedToken, type WalletContext } from './wallet';
10
11
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
12
  import { WebSocket } from 'ws';
12
13
  import * as Rx from 'rxjs';
13
- import { Buffer } from 'buffer';
14
14
 
15
15
  // Midnight SDK imports
16
16
  import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
@@ -18,14 +18,6 @@ import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client
18
18
  import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
19
19
  import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
20
20
  import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
21
- import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
22
- import * as ledger from '@midnight-ntwrk/ledger-v8';
23
- import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
24
- import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
25
- import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
26
- import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
27
- import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
28
- import { createKeystore, NoOpTransactionHistoryStorage, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
29
21
  import { CompiledContract } from '@midnight-ntwrk/compact-js';
30
22
 
31
23
  // @ts-expect-error Required for wallet sync
@@ -37,7 +29,6 @@ globalThis.WebSocket = WebSocket;
37
29
  // 'undeployed' (local devnet). Switch networks with: npm run network <name>
38
30
 
39
31
  const { network, config: networkConfig } = resolveNetwork();
40
- setNetworkId(networkConfig.networkId);
41
32
  const SEED = getOrCreateSeed(network);
42
33
 
43
34
  // ─── Proof server readiness ────────────────────────────────────────────────────
@@ -86,51 +77,9 @@ const compiledContract = CompiledContract.make('hello-world', HelloWorld.Contrac
86
77
  CompiledContract.withCompiledFileAssets(zkConfigPath),
87
78
  );
88
79
 
89
- // ─── Wallet helpers ────────────────────────────────────────────────────────────
90
-
91
- function deriveKeys(seed: string) {
92
- const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
93
- if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
94
- const result = hdWallet.hdWallet
95
- .selectAccount(0)
96
- .selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
97
- .deriveKeysAt(0);
98
- if (result.type !== 'keysDerived') throw new Error('Key derivation failed');
99
- hdWallet.hdWallet.clear();
100
- return result.keys;
101
- }
102
-
103
- async function createWallet(seed: string) {
104
- const keys = deriveKeys(seed);
105
- const networkId = getNetworkId();
106
- const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
107
- const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
108
- const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], networkId);
109
-
110
- const walletConfig = {
111
- networkId,
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(),
116
- costParameters: { additionalFeeOverhead: 300_000_000_000_000n, feeBlocksMargin: 5 },
117
- };
118
-
119
- const wallet = await WalletFacade.init({
120
- configuration: walletConfig,
121
- shielded: async (config) => ShieldedWallet(config).startWithSecretKeys(shieldedSecretKeys),
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
- });
127
-
128
- await wallet.start(shieldedSecretKeys, dustSecretKey);
80
+ // ─── Providers ─────────────────────────────────────────────────────────────────
129
81
 
130
- return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
131
- }
132
-
133
- async function createProviders(walletCtx: ReturnType<typeof createWallet> extends Promise<infer T> ? T : never) {
82
+ async function createProviders(walletCtx: WalletContext) {
134
83
  // The SDK requires the private-state password to be at least 16 characters.
135
84
  // The default below is a placeholder for local devnet only — set a strong
136
85
  // password via PRIVATE_STATE_PASSWORD when you move to a non-local target.
@@ -182,7 +131,11 @@ async function main() {
182
131
 
183
132
  console.log('─── Wallet setup ───────────────────────────────────────────────\n');
184
133
  console.log(' Creating wallet...');
185
- const walletCtx = await createWallet(seed);
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
+ }
186
139
 
187
140
  console.log(' Syncing with network...');
188
141
  console.log(' ℹ This may take several minutes depending on network size.');
@@ -196,6 +149,9 @@ async function main() {
196
149
  clearInterval(syncInterval);
197
150
  process.stdout.write('\r ✓ Synced with network. \n');
198
151
 
152
+ // Persist sync state now so a later deploy failure doesn't waste the sync work.
153
+ await persistWalletState(network, walletCtx);
154
+
199
155
  const address = walletCtx.unshieldedKeystore.getBech32Address();
200
156
  let balance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;
201
157
  console.log(`\n Wallet Address: ${address}`);
@@ -391,6 +347,7 @@ async function main() {
391
347
  recordDeployment(network, contractAddress, address.toString());
392
348
  console.log(' Saved to .midnight-state.json\n');
393
349
 
350
+ await persistWalletState(network, walletCtx);
394
351
  await walletCtx.wallet.stop();
395
352
  console.log('─── Deployment complete ────────────────────────────────────────\n');
396
353
  console.log(' Next: npm run cli\n');
@@ -0,0 +1,95 @@
1
+ // Wallet sync-state persistence.
2
+ //
3
+ // Mirrors network.ts: no template substitutions, all I/O via function
4
+ // parameters, no SDK imports — keeps the module unit-testable from the
5
+ // create-mn-app workspace (which doesn't install @midnight-ntwrk/* packages).
6
+ //
7
+ // Why: without persistence, every `npm run deploy` / `npm run cli` rebuilds
8
+ // each child wallet from seed and re-syncs against the chain. On public
9
+ // networks (preview, preprod) that's minutes per run — and painful on retries
10
+ // after a transient failure. The SDK exposes serializeState() and restore()
11
+ // on each child wallet class; wallet.ts is the glue that uses them, and this
12
+ // file is the on-disk format underneath.
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+
17
+ import type { NetworkId } from './network';
18
+
19
+ export const WALLET_STATE_DIR = '.midnight-wallet-state';
20
+ export const WALLET_STATE_VERSION = 1 as const;
21
+
22
+ export type ChildKind = 'shielded' | 'unshielded' | 'dust';
23
+ export const CHILD_KINDS: readonly ChildKind[] = ['shielded', 'unshielded', 'dust'] as const;
24
+
25
+ export interface PersistedWalletState {
26
+ shielded?: unknown;
27
+ unshielded?: unknown;
28
+ dust?: string;
29
+ }
30
+
31
+ export interface FsOptions {
32
+ cwd?: string;
33
+ }
34
+
35
+ function networkDir(network: NetworkId, opts: FsOptions = {}): string {
36
+ return path.join(opts.cwd ?? process.cwd(), WALLET_STATE_DIR, network);
37
+ }
38
+
39
+ function statePath(network: NetworkId, kind: ChildKind, opts: FsOptions = {}): string {
40
+ return path.join(networkDir(network, opts), `${kind}.json`);
41
+ }
42
+
43
+ function atomicWrite(file: string, content: string): void {
44
+ fs.mkdirSync(path.dirname(file), { recursive: true });
45
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
46
+ fs.writeFileSync(tmp, content);
47
+ fs.renameSync(tmp, file);
48
+ }
49
+
50
+ interface VersionedState<T> {
51
+ version: typeof WALLET_STATE_VERSION;
52
+ state: T;
53
+ }
54
+
55
+ function readVersionedState<T>(file: string): T | undefined {
56
+ if (!fs.existsSync(file)) return undefined;
57
+ try {
58
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as VersionedState<T>;
59
+ if (!parsed || typeof parsed !== 'object' || parsed.version !== WALLET_STATE_VERSION) {
60
+ return undefined;
61
+ }
62
+ return parsed.state;
63
+ } catch {
64
+ // Corrupt file — caller falls back to from-seed sync; we'll overwrite on save.
65
+ return undefined;
66
+ }
67
+ }
68
+
69
+ function writeVersionedState<T>(file: string, state: T): void {
70
+ const payload: VersionedState<T> = { version: WALLET_STATE_VERSION, state };
71
+ atomicWrite(file, `${JSON.stringify(payload)}\n`);
72
+ }
73
+
74
+ export function loadWalletState(network: NetworkId, opts: FsOptions = {}): PersistedWalletState {
75
+ return {
76
+ shielded: readVersionedState(statePath(network, 'shielded', opts)),
77
+ unshielded: readVersionedState(statePath(network, 'unshielded', opts)),
78
+ dust: readVersionedState<string>(statePath(network, 'dust', opts)),
79
+ };
80
+ }
81
+
82
+ export function saveWalletState(
83
+ network: NetworkId,
84
+ state: PersistedWalletState,
85
+ opts: FsOptions = {},
86
+ ): void {
87
+ if (state.shielded !== undefined) writeVersionedState(statePath(network, 'shielded', opts), state.shielded);
88
+ if (state.unshielded !== undefined) writeVersionedState(statePath(network, 'unshielded', opts), state.unshielded);
89
+ if (state.dust !== undefined) writeVersionedState(statePath(network, 'dust', opts), state.dust);
90
+ }
91
+
92
+ export function clearWalletState(network: NetworkId, opts: FsOptions = {}): void {
93
+ const dir = networkDir(network, opts);
94
+ if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
95
+ }
@@ -0,0 +1,189 @@
1
+ // Wallet construction + sync-state restore.
2
+ //
3
+ // Mirrors network.ts in structure. The on-disk format and pure I/O live in
4
+ // wallet-state.ts (unit-tested from the scaffolder workspace, no SDK deps);
5
+ // this file is the glue between that format and the wallet SDK.
6
+
7
+ import { Buffer } from 'buffer';
8
+
9
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
10
+ import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
11
+ import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
12
+ import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
13
+ import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
14
+ import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
15
+ import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
16
+ import {
17
+ createKeystore,
18
+ NoOpTransactionHistoryStorage,
19
+ PublicKey,
20
+ UnshieldedWallet,
21
+ } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
22
+
23
+ import type { NetworkConfig, NetworkId } from './network';
24
+ import {
25
+ CHILD_KINDS,
26
+ loadWalletState,
27
+ saveWalletState,
28
+ type ChildKind,
29
+ type PersistedWalletState,
30
+ } from './wallet-state';
31
+
32
+ export { unshieldedToken };
33
+ export type { PersistedWalletState };
34
+ export {
35
+ loadWalletState,
36
+ saveWalletState,
37
+ clearWalletState,
38
+ WALLET_STATE_DIR,
39
+ WALLET_STATE_VERSION,
40
+ } from './wallet-state';
41
+
42
+ function deriveKeys(seed: string) {
43
+ const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
44
+ if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
45
+ const result = hdWallet.hdWallet
46
+ .selectAccount(0)
47
+ .selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
48
+ .deriveKeysAt(0);
49
+ if (result.type !== 'keysDerived') throw new Error('Key derivation failed');
50
+ hdWallet.hdWallet.clear();
51
+ return result.keys;
52
+ }
53
+
54
+ export interface WalletContext {
55
+ wallet: Awaited<ReturnType<typeof WalletFacade.init>>;
56
+ shieldedSecretKeys: ReturnType<typeof ledger.ZswapSecretKeys.fromSeed>;
57
+ dustSecretKey: ReturnType<typeof ledger.DustSecretKey.fromSeed>;
58
+ unshieldedKeystore: ReturnType<typeof createKeystore>;
59
+ restored: { shielded: boolean; unshielded: boolean; dust: boolean };
60
+ }
61
+
62
+ export interface CreateWalletOptions {
63
+ network: NetworkId;
64
+ networkConfig: NetworkConfig;
65
+ seed: string;
66
+ /**
67
+ * Whether to attempt to restore each child wallet from saved state.
68
+ * Defaults to true. Pass false to force a from-seed sync (used by tests).
69
+ */
70
+ restore?: boolean;
71
+ cwd?: string;
72
+ }
73
+
74
+ function warnRestoreFailure(kind: ChildKind, err: unknown): void {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ process.stderr.write(` ⚠ Could not restore ${kind} wallet state (${msg}); falling back to fresh sync.\n`);
77
+ }
78
+
79
+ /**
80
+ * Build the wallet facade, restoring each child from saved state when
81
+ * available and falling back to a from-seed start when not (or when restore
82
+ * throws, e.g. after an SDK upgrade with an incompatible state format).
83
+ *
84
+ * Caller is responsible for `await wallet.waitForSyncedState()` afterwards.
85
+ */
86
+ export async function createWallet(opts: CreateWalletOptions): Promise<WalletContext> {
87
+ setNetworkId(opts.networkConfig.networkId);
88
+
89
+ const keys = deriveKeys(opts.seed);
90
+ const networkId = getNetworkId();
91
+ const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
92
+ const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
93
+ const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], networkId);
94
+
95
+ const saved: PersistedWalletState = opts.restore === false
96
+ ? {}
97
+ : loadWalletState(opts.network, { cwd: opts.cwd });
98
+
99
+ const restored = { shielded: false, unshielded: false, dust: false };
100
+
101
+ const walletConfig = {
102
+ networkId,
103
+ indexerClientConnection: {
104
+ indexerHttpUrl: opts.networkConfig.indexer,
105
+ indexerWsUrl: opts.networkConfig.indexerWS,
106
+ },
107
+ provingServerUrl: new URL(opts.networkConfig.proofServer),
108
+ relayURL: new URL(opts.networkConfig.node.replace(/^http/, 'ws')),
109
+ txHistoryStorage: new NoOpTransactionHistoryStorage(),
110
+ costParameters: { additionalFeeOverhead: 300_000_000_000_000n, feeBlocksMargin: 5 },
111
+ };
112
+
113
+ const wallet = await WalletFacade.init({
114
+ configuration: walletConfig,
115
+ shielded: async (config) => {
116
+ const cls = ShieldedWallet(config);
117
+ if (saved.shielded !== undefined) {
118
+ try {
119
+ const restoredWallet = await (cls as any).restore(saved.shielded);
120
+ restored.shielded = true;
121
+ return restoredWallet;
122
+ } catch (err) {
123
+ warnRestoreFailure('shielded', err);
124
+ }
125
+ }
126
+ return cls.startWithSecretKeys(shieldedSecretKeys);
127
+ },
128
+ unshielded: async (config) => {
129
+ const cls = UnshieldedWallet(config);
130
+ if (saved.unshielded !== undefined) {
131
+ try {
132
+ const restoredWallet = await (cls as any).restore(saved.unshielded);
133
+ restored.unshielded = true;
134
+ return restoredWallet;
135
+ } catch (err) {
136
+ warnRestoreFailure('unshielded', err);
137
+ }
138
+ }
139
+ return cls.startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));
140
+ },
141
+ dust: async (config) => {
142
+ const cls = DustWallet(config);
143
+ if (saved.dust !== undefined) {
144
+ try {
145
+ const restoredWallet = await (cls as any).restore(saved.dust);
146
+ restored.dust = true;
147
+ return restoredWallet;
148
+ } catch (err) {
149
+ warnRestoreFailure('dust', err);
150
+ }
151
+ }
152
+ return cls.startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);
153
+ },
154
+ });
155
+
156
+ await wallet.start(shieldedSecretKeys, dustSecretKey);
157
+
158
+ return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore, restored };
159
+ }
160
+
161
+ /**
162
+ * Serialize each child wallet's current state and persist it for the next run.
163
+ * Safe to call multiple times. Logs but does not throw on individual failures —
164
+ * losing one child's state means the next run re-syncs that child only.
165
+ */
166
+ export async function persistWalletState(
167
+ network: NetworkId,
168
+ ctx: WalletContext,
169
+ cwd?: string,
170
+ ): Promise<void> {
171
+ const next: PersistedWalletState = {};
172
+
173
+ for (const kind of CHILD_KINDS) {
174
+ try {
175
+ const child = (ctx.wallet as unknown as Record<ChildKind, { serializeState: () => Promise<unknown> }>)[kind];
176
+ const serialized = await child.serializeState();
177
+ if (kind === 'dust') {
178
+ next.dust = serialized as string;
179
+ } else {
180
+ next[kind] = serialized;
181
+ }
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ process.stderr.write(` ⚠ Could not serialize ${kind} wallet state (${msg}); next run will re-sync.\n`);
185
+ }
186
+ }
187
+
188
+ saveWalletState(network, next, { cwd });
189
+ }