@effectstream/wallets 0.100.18 → 0.100.20

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": "@effectstream/wallets",
3
- "version": "0.100.18",
3
+ "version": "0.100.20",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -18,6 +18,7 @@
18
18
  "./midnight-local": "./src/midnight/local.ts"
19
19
  },
20
20
  "peerDependencies": {
21
+ "@effectstream/midnight-contracts": "0.100.20",
21
22
  "@lucid-evolution/lucid": "^0.4.29",
22
23
  "@lucid-evolution/provider": "^0.1.90",
23
24
  "@lucid-evolution/utils": "^0.1.66",
@@ -26,6 +27,9 @@
26
27
  "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "^2.1.0"
27
28
  },
28
29
  "peerDependenciesMeta": {
30
+ "@effectstream/midnight-contracts": {
31
+ "optional": true
32
+ },
29
33
  "@lucid-evolution/lucid": {
30
34
  "optional": true
31
35
  },
@@ -46,6 +50,7 @@
46
50
  }
47
51
  },
48
52
  "devDependencies": {
53
+ "@effectstream/midnight-contracts": "0.100.20",
49
54
  "@lucid-evolution/lucid": "^0.4.30",
50
55
  "@lucid-evolution/provider": "^0.1.91",
51
56
  "@lucid-evolution/utils": "^0.1.66",
@@ -54,10 +59,10 @@
54
59
  "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0"
55
60
  },
56
61
  "dependencies": {
57
- "@effectstream/concise": "0.100.18",
58
- "@effectstream/crypto": "0.100.18",
59
- "@effectstream/event-client": "0.100.18",
60
- "@effectstream/utils": "0.100.18",
62
+ "@effectstream/concise": "0.100.20",
63
+ "@effectstream/crypto": "0.100.20",
64
+ "@effectstream/event-client": "0.100.20",
65
+ "@effectstream/utils": "0.100.20",
61
66
  "@aurowallet/mina-provider": "^1.0.12",
62
67
  "@metamask/providers": "^22.1.0",
63
68
  "@midnight-ntwrk/dapp-connector-api": "4.0.1",
@@ -5,6 +5,7 @@ import { type Chain, numberToHex } from "viem";
5
5
  import { utf8ToHex } from "web3-utils";
6
6
  import type { EthersEvmProvider } from "./evm/ethers.ts";
7
7
  import type { EvmInjectedProvider } from "./evm/injected.ts";
8
+ import type { ViemEvmProvider } from "./evm/viem.ts";
8
9
  import type { AbiItem } from "web3-utils";
9
10
  import { type TransactionReceipt, Web3 } from "web3";
10
11
  import { createMessageForBatcher } from "@effectstream/concise";
@@ -189,7 +190,8 @@ export async function sendSelfSequencedTransaction(
189
190
 
190
191
  const evmProvider = wallet.provider as
191
192
  | EthersEvmProvider
192
- | EvmInjectedProvider;
193
+ | EvmInjectedProvider
194
+ | ViemEvmProvider;
193
195
 
194
196
  const hexData = utf8ToHex(JSON.stringify(conciseData));
195
197
  const effectstreamL2Contract = await effectstreamConfig.getEffectstreamL2Contract();
package/src/evm/viem.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ createPublicClient,
2
3
  createWalletClient,
3
4
  http,
4
5
  type Chain,
@@ -119,4 +120,76 @@ export class ViemEvmProvider implements IProvider<ViemApi> {
119
120
  message,
120
121
  });
121
122
  };
123
+
124
+ /**
125
+ * Submit an EIP-1193-shaped transaction request (matching the
126
+ * `EvmInjectedProvider.sendTransaction` shape — all numeric fields are
127
+ * hex strings) by delegating to viem's hoisted `walletClient.sendTransaction`.
128
+ *
129
+ * `account` + `chain` are hoisted on the underlying `WalletClient` at
130
+ * connect time (see `ViemConnector.connectFromPrivateKey`), so we only
131
+ * need to convert the hex-string numeric fields to bigints that viem
132
+ * expects, and pass through `to` / `data` as 0x-strings.
133
+ *
134
+ * Without this method, the high-level `sendTransaction(wallet, …)` helper
135
+ * in `effectstream.ts` rejects with `evmProvider.sendTransaction is not a
136
+ * function` when invoked against a `WalletMode.EvmViem` wallet — blocking
137
+ * any frontend that wants to drive the engine's tx flow via the local-JS
138
+ * wallet (e.g. headless Playwright e2e tests).
139
+ */
140
+ sendTransaction = async (tx: {
141
+ to?: string;
142
+ from: string;
143
+ gas?: string;
144
+ gasPrice?: string;
145
+ data: string;
146
+ value?: string;
147
+ maxPriorityFeePerGas?: string;
148
+ maxFeePerGas?: string;
149
+ }): Promise<{ txHash: string }> => {
150
+ if (this.account == null) {
151
+ throw new Error("ViemEvmProvider.sendTransaction: account is missing");
152
+ }
153
+ const toBigInt = (h?: string): bigint | undefined =>
154
+ h == null ? undefined : BigInt(h);
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ const hash = await (this.conn.api as any).sendTransaction({
157
+ account: this.account,
158
+ to: tx.to as `0x${string}` | undefined,
159
+ data: tx.data as `0x${string}`,
160
+ value: toBigInt(tx.value),
161
+ gas: toBigInt(tx.gas),
162
+ gasPrice: toBigInt(tx.gasPrice),
163
+ maxFeePerGas: toBigInt(tx.maxFeePerGas),
164
+ maxPriorityFeePerGas: toBigInt(tx.maxPriorityFeePerGas),
165
+ });
166
+ if (typeof hash !== "string") {
167
+ throw new Error(
168
+ `[ViemEvmProvider.sendTransaction] expected string tx hash, got ${typeof hash}`,
169
+ );
170
+ }
171
+
172
+ // Wait for the receipt + verify status. viem's sendTransaction returns
173
+ // the hash as soon as the tx is broadcast, NOT once it has mined. If we
174
+ // returned the hash without checking the receipt, a reverted tx would
175
+ // look "successful" to the engine's `wait-receipt` confirmation path,
176
+ // and the indexer would never see a matching event because the contract
177
+ // call reverted on chain. Match the EvmInjected / EthersEvmProvider
178
+ // contract: only return when the tx is mined AND its status is 0x1.
179
+ const chain = (this.conn.api as WalletClient).chain;
180
+ const transport = (this.conn.api as WalletClient).transport;
181
+ const publicClient = createPublicClient({
182
+ chain: chain ?? undefined,
183
+ transport: http((transport as { url?: string })?.url),
184
+ });
185
+ const receipt = await publicClient.waitForTransactionReceipt({
186
+ hash: hash as `0x${string}`,
187
+ });
188
+ if (receipt.status !== "success") {
189
+ throw new Error(
190
+ `[ViemEvmProvider.sendTransaction] tx ${hash} reverted on chain (status=${receipt.status})`,
191
+ );
192
+ }
193
+ return { txHash: hash };
194
+ };
122
195
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
  import { Buffer } from "node:buffer";
3
3
  import { AddressType } from "@effectstream/utils";
4
4
  import { verifySignature } from "@midnight-ntwrk/ledger-v8";
@@ -55,6 +55,81 @@ describe("MidnightLocalProvider", () => {
55
55
  expect(okAgain).toBe(true);
56
56
  });
57
57
 
58
+ test("facade mode: networkUrls triggers buildWalletFacade and exposes the full WalletFacade on the api", async () => {
59
+ // Sentinel objects so we can assert the facade-path code actually wired
60
+ // through whatever buildWalletFacade returned.
61
+ const fakeFacade = { __kind: "WalletFacade", shielded: {}, dust: {}, unshielded: {} };
62
+ const fakeShielded = {
63
+ address: {
64
+ coinPublicKeyString: () => "deadbeef-coin-pub-key",
65
+ encryptionPublicKeyString: () => "deadbeef-enc-pub-key",
66
+ },
67
+ balances: {},
68
+ };
69
+ const buildSpy = mock(async (
70
+ networkUrls: unknown,
71
+ seed: string,
72
+ _networkId: unknown,
73
+ _syncMode: unknown,
74
+ ) => ({
75
+ wallet: fakeFacade,
76
+ zswapSecretKeys: {} as never,
77
+ walletZswapSecretKeys: {} as never,
78
+ dustSecretKey: {} as never,
79
+ walletDustSecretKey: {} as never,
80
+ dustAddress: `dust-for-${seed.slice(0, 8)}`,
81
+ unshieldedAddress: `unshielded-for-${seed.slice(0, 8)}`,
82
+ unshieldedKeystore: {} as never,
83
+ __echoedNetworkUrls: networkUrls,
84
+ }));
85
+ const getInitialSpy = mock(async (_shielded: unknown) => fakeShielded);
86
+
87
+ mock.module("@effectstream/midnight-contracts/wallet-info", () => ({
88
+ buildWalletFacade: buildSpy,
89
+ getInitialShieldedState: getInitialSpy,
90
+ }));
91
+
92
+ const provider = await MidnightLocalConnector.instance().connectFromSeed({
93
+ seed: DETERMINISTIC_SEED,
94
+ networkId: "undeployed",
95
+ networkUrls: {
96
+ indexer: "http://127.0.0.1:8088/api/v3/graphql",
97
+ indexerWS: "ws://127.0.0.1:8088/api/v3/graphql/ws",
98
+ node: "http://127.0.0.1:9944",
99
+ proofServer: "http://127.0.0.1:6300",
100
+ },
101
+ });
102
+
103
+ expect(buildSpy).toHaveBeenCalledTimes(1);
104
+ const [networkUrlsArg, seedArg, networkIdArg, syncModeArg] = buildSpy.mock.calls[0]!;
105
+ expect(seedArg).toBe(DETERMINISTIC_SEED);
106
+ expect(networkIdArg).toBe("undeployed");
107
+ expect(syncModeArg).toBe("all");
108
+ expect(networkUrlsArg).toMatchObject({
109
+ indexer: "http://127.0.0.1:8088/api/v3/graphql",
110
+ indexerWS: "ws://127.0.0.1:8088/api/v3/graphql/ws",
111
+ node: "http://127.0.0.1:9944",
112
+ proofServer: "http://127.0.0.1:6300",
113
+ id: "undeployed",
114
+ });
115
+
116
+ const api = provider.getConnection().api as unknown as MidnightLocalApi;
117
+ expect(api.walletFacade).toBe(fakeFacade);
118
+ expect(api.dustAddress).toBe(`dust-for-${DETERMINISTIC_SEED.slice(0, 8)}`);
119
+ expect(api.shieldedAddress).toBe("deadbeef-coin-pub-key");
120
+
121
+ // getShieldedAddresses no longer throws in facade mode.
122
+ await expect(api.getShieldedAddresses()).resolves.toEqual({
123
+ shieldedAddress: "deadbeef-coin-pub-key",
124
+ });
125
+
126
+ expect(provider.getConnection().metadata.displayName).toBe(
127
+ "Midnight (local seed, facade)",
128
+ );
129
+
130
+ mock.restore();
131
+ });
132
+
58
133
  test("signMessage round-trips through CryptoManager.Midnight().verifySignature", async () => {
59
134
  const provider = await MidnightLocalConnector.instance().connectFromSeed({
60
135
  seed: DETERMINISTIC_SEED,
@@ -8,20 +8,55 @@ import type {
8
8
  } from "../IProvider.ts";
9
9
  import type { MidnightApi } from "./midnight.ts";
10
10
 
11
+ /**
12
+ * Network endpoints required to bring up the full WalletFacade. Mirrors
13
+ * `NetworkUrls` from `@effectstream/midnight-contracts` but with every field
14
+ * required (so consumers can't accidentally leave one unset).
15
+ */
16
+ export type MidnightLocalNetworkUrls = {
17
+ /** GraphQL indexer HTTP endpoint. */
18
+ indexer: string;
19
+ /** GraphQL indexer WebSocket endpoint. */
20
+ indexerWS: string;
21
+ /** Midnight node RPC endpoint (used as the wallet relay; http→ws is rewritten internally). */
22
+ node: string;
23
+ /** Proof-server HTTP endpoint. */
24
+ proofServer: string;
25
+ /** Optional network id override; defaults to `connectFromSeed`'s `networkId` arg. */
26
+ id?: string;
27
+ };
28
+
29
+ export type MidnightLocalSyncMode = "all" | "dust-only";
30
+
11
31
  export type MidnightLocalConnectArgs = {
12
32
  /** 64-character hex seed. Generated when omitted. */
13
33
  seed?: string;
14
34
  /** Midnight network id — sets the bech32 prefix on the unshielded address. */
15
35
  networkId: string;
36
+ /**
37
+ * Network URLs for the full WalletFacade. When supplied, the connector builds
38
+ * a complete wallet (shielded + dust + unshielded) via
39
+ * `@effectstream/midnight-contracts.buildWalletFacade` and exposes it on
40
+ * `getConnection().api.walletFacade`. Required for shielded tx submission,
41
+ * `balanceAndProveTransaction`, dust fee payment, etc.
42
+ *
43
+ * When omitted, the connector returns the signing-only path (unshielded
44
+ * keystore `signData`) — useful for offline signature flows or signature
45
+ * verification dry runs that don't need to stand up indexer + proof-server.
46
+ */
47
+ networkUrls?: MidnightLocalNetworkUrls;
48
+ /** Defaults to "all" when `networkUrls` is supplied. Ignored otherwise. */
49
+ syncMode?: MidnightLocalSyncMode;
16
50
  };
17
51
 
18
52
  /**
19
- * Shape exposed on `getConnection().api`. A minimal subset of `ConnectedAPI`
53
+ * Shape exposed on `getConnection().api`. Mirrors a subset of `ConnectedAPI`
20
54
  * that the rest of Effectstream's Midnight code consumes:
21
- * - `signData` mirrors the dapp-connector-api shape.
22
- * - `getShieldedAddresses` is stubbed (shielded sync is out of scope for the
23
- * lite/seed-only path — callers that need shielded/dust addresses should
24
- * build the full WalletFacade via `@effectstream/midnight-contracts`).
55
+ * - `signData` mirrors the dapp-connector-api shape (works in both modes).
56
+ * - `getShieldedAddresses` returns the real shielded coin public key when
57
+ * the full facade is built; throws in signing-only mode.
58
+ * - `walletFacade` / `dustAddress` / `shieldedAddress` are populated only
59
+ * when `networkUrls` was supplied at connect time.
25
60
  */
26
61
  export type MidnightLocalApi = {
27
62
  signData: (
@@ -35,6 +70,28 @@ export type MidnightLocalApi = {
35
70
  verifyingKey: string;
36
71
  /** Bech32-encoded unshielded address. */
37
72
  unshieldedAddress: string;
73
+ /**
74
+ * Full Midnight `WalletFacade`. Populated only when `networkUrls` was passed
75
+ * to `connectFromSeed`. Cast to
76
+ * `import("@midnight-ntwrk/wallet-sdk-facade").WalletFacade` for typed
77
+ * access. Carries `submitTransaction`, `balanceFinalizedTransaction`,
78
+ * `balanceUnboundTransaction`, `balanceUnprovenTransaction`, `signRecipe`,
79
+ * `signUnprovenTransaction`, `state()`, `waitForSyncedState()`, etc.
80
+ */
81
+ walletFacade?: unknown;
82
+ /** Bech32-encoded dust address. Populated only in facade mode. */
83
+ dustAddress?: string;
84
+ /** Hex-encoded shielded coin public key. Populated only in facade mode. */
85
+ shieldedAddress?: string;
86
+ /**
87
+ * Escape hatch: the entire `WalletResult` returned by
88
+ * `@effectstream/midnight-contracts.buildWalletFacade`. Exposes the raw
89
+ * secret keys (`zswapSecretKeys`, `dustSecretKey`, `unshieldedKeystore`) that
90
+ * advanced flows (manual tx balancing, custom dust handling) need. Cast to
91
+ * `import("@effectstream/midnight-contracts").WalletResult`.
92
+ * Populated only in facade mode.
93
+ */
94
+ walletResult?: unknown;
38
95
  };
39
96
 
40
97
  export class MidnightLocalConnector {
@@ -51,27 +108,34 @@ export class MidnightLocalConnector {
51
108
  /**
52
109
  * Build a Midnight wallet identity from a hex seed.
53
110
  *
54
- * This is the "lite" / signing-only path: it derives the unshielded
55
- * keystore from the seed and exposes a CIP-30-shaped `signData` backed by
56
- * `unshieldedKeystore.signData(...)`. It does NOT bring up the shielded /
57
- * dust wallets — that requires a live indexer + proof server and lives in
58
- * `@effectstream/midnight-contracts`'s `buildWalletFacade`.
111
+ * Two modes, picked by whether `args.networkUrls` is supplied:
112
+ *
113
+ * - **Facade mode** (`networkUrls` set): builds the full Midnight wallet via
114
+ * `@effectstream/midnight-contracts.buildWalletFacade` — shielded + dust +
115
+ * unshielded sub-wallets, connected to a live indexer + node + proof
116
+ * server. The complete `WalletFacade` is exposed on
117
+ * `getConnection().api.walletFacade`, enabling `submitTransaction`,
118
+ * `balanceAndProveTransaction`, dust fee payment, etc. Requires
119
+ * `@effectstream/midnight-contracts` to be installed (optional peer dep).
120
+ *
121
+ * - **Signing-only mode** (`networkUrls` omitted): derives the unshielded
122
+ * keystore from the seed and exposes a CIP-30-shaped `signData` backed by
123
+ * `unshieldedKeystore.signData(...)`. No network IO. Suitable for offline
124
+ * signature flows and signature-verification dry runs.
59
125
  */
60
126
  connectFromSeed = async (
61
127
  args: MidnightLocalConnectArgs,
62
128
  ): Promise<MidnightLocalProvider> => {
63
- const hdMod = await import("@midnight-ntwrk/wallet-sdk-hd").catch(() => {
64
- throw new Error(
65
- "@midnight-ntwrk/wallet-sdk-hd is required for WalletMode.MidnightLocal.",
66
- );
67
- });
68
- const keystoreMod = await import(
69
- "@midnight-ntwrk/wallet-sdk-unshielded-wallet"
70
- ).catch(() => {
71
- throw new Error(
72
- "@midnight-ntwrk/wallet-sdk-unshielded-wallet is required for WalletMode.MidnightLocal.",
73
- );
74
- });
129
+ if (args.networkUrls != null) {
130
+ return await this.connectWithFacade(args, args.networkUrls);
131
+ }
132
+ return await this.connectSigningOnly(args);
133
+ };
134
+
135
+ private connectSigningOnly = async (
136
+ args: MidnightLocalConnectArgs,
137
+ ): Promise<MidnightLocalProvider> => {
138
+ const { hdMod, keystoreMod } = await loadKeystoreDeps();
75
139
 
76
140
  const seed = args.seed ?? generateRandomHexSeed(32);
77
141
  const unshieldedSeed = deriveUnshieldedSeed(
@@ -102,7 +166,7 @@ export class MidnightLocalConnector {
102
166
  },
103
167
  getShieldedAddresses: async () => {
104
168
  throw new Error(
105
- "MidnightLocal lite mode does not expose a shielded address. Build a WalletFacade via @effectstream/midnight-contracts for shielded/dust support.",
169
+ "MidnightLocal signing-only mode does not expose a shielded address. Pass `networkUrls` to connectFromSeed to build the full WalletFacade.",
106
170
  );
107
171
  },
108
172
  seed,
@@ -126,6 +190,93 @@ export class MidnightLocalConnector {
126
190
  return this.provider;
127
191
  };
128
192
 
193
+ private connectWithFacade = async (
194
+ args: MidnightLocalConnectArgs,
195
+ networkUrls: MidnightLocalNetworkUrls,
196
+ ): Promise<MidnightLocalProvider> => {
197
+ // Use the `/wallet-info` subpath, not the package barrel: the barrel
198
+ // re-exports `deploy.ts` (`node:fs/promises`), which breaks browser bundles
199
+ // of `@effectstream/wallets`.
200
+ const contractsMod = await import(
201
+ "@effectstream/midnight-contracts/wallet-info"
202
+ ).catch(() => {
203
+ throw new Error(
204
+ "@effectstream/midnight-contracts is required when `networkUrls` is passed to MidnightLocal.connectFromSeed. Install it as a peer dependency.",
205
+ );
206
+ });
207
+ const { hdMod, keystoreMod } = await loadKeystoreDeps();
208
+
209
+ const seed = args.seed ?? generateRandomHexSeed(32);
210
+ const unshieldedSeed = deriveUnshieldedSeed(
211
+ hdMod,
212
+ seed,
213
+ hdMod.Roles.NightExternal,
214
+ );
215
+ const unshieldedKeystore = keystoreMod.createKeystore(
216
+ unshieldedSeed,
217
+ args.networkId as never,
218
+ );
219
+ const verifyingKey = String(unshieldedKeystore.getPublicKey());
220
+
221
+ const fullNetworkUrls = {
222
+ indexer: networkUrls.indexer,
223
+ indexerWS: networkUrls.indexerWS,
224
+ node: networkUrls.node,
225
+ proofServer: networkUrls.proofServer,
226
+ id: networkUrls.id ?? args.networkId,
227
+ };
228
+
229
+ const walletResult = await contractsMod.buildWalletFacade(
230
+ fullNetworkUrls,
231
+ seed,
232
+ args.networkId as never,
233
+ args.syncMode ?? "all",
234
+ );
235
+
236
+ const initialShielded = await contractsMod.getInitialShieldedState(
237
+ walletResult.wallet.shielded,
238
+ );
239
+ const shieldedAddress = initialShielded.address.coinPublicKeyString();
240
+
241
+ const api: MidnightLocalApi = {
242
+ signData: async (data, options) => {
243
+ const bytes =
244
+ options.encoding === "hex"
245
+ ? Buffer.from(data, "hex")
246
+ : Buffer.from(data, "utf-8");
247
+ const signature = unshieldedKeystore.signData(bytes);
248
+ return {
249
+ data,
250
+ signature: String(signature),
251
+ verifyingKey,
252
+ };
253
+ },
254
+ getShieldedAddresses: async () => ({ shieldedAddress }),
255
+ seed,
256
+ verifyingKey,
257
+ unshieldedAddress: walletResult.unshieldedAddress,
258
+ walletFacade: walletResult.wallet,
259
+ dustAddress: walletResult.dustAddress,
260
+ shieldedAddress,
261
+ walletResult,
262
+ };
263
+
264
+ const conn: ActiveConnection<MidnightApi> = {
265
+ api: api as unknown as MidnightApi,
266
+ metadata: {
267
+ name: "midnight-local",
268
+ displayName: "Midnight (local seed, facade)",
269
+ },
270
+ };
271
+
272
+ this.provider = new MidnightLocalProvider(
273
+ conn,
274
+ walletResult.unshieldedAddress as MidnightAddress,
275
+ unshieldedKeystore,
276
+ );
277
+ return this.provider;
278
+ };
279
+
129
280
  getProvider = (): undefined | MidnightLocalProvider => {
130
281
  return this.provider;
131
282
  };
@@ -176,6 +327,25 @@ export class MidnightLocalProvider implements IProvider<MidnightApi> {
176
327
  getUnshieldedKeystore = (): unknown => this.unshieldedKeystore;
177
328
  }
178
329
 
330
+ async function loadKeystoreDeps(): Promise<{
331
+ hdMod: typeof import("@midnight-ntwrk/wallet-sdk-hd");
332
+ keystoreMod: typeof import("@midnight-ntwrk/wallet-sdk-unshielded-wallet");
333
+ }> {
334
+ const hdMod = await import("@midnight-ntwrk/wallet-sdk-hd").catch(() => {
335
+ throw new Error(
336
+ "@midnight-ntwrk/wallet-sdk-hd is required for WalletMode.MidnightLocal.",
337
+ );
338
+ });
339
+ const keystoreMod = await import(
340
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet"
341
+ ).catch(() => {
342
+ throw new Error(
343
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet is required for WalletMode.MidnightLocal.",
344
+ );
345
+ });
346
+ return { hdMod, keystoreMod };
347
+ }
348
+
179
349
  function deriveUnshieldedSeed(
180
350
  hdMod: typeof import("@midnight-ntwrk/wallet-sdk-hd"),
181
351
  seedHex: string,