@effectstream/midnight-contracts 0.100.19 → 0.100.21
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 +11 -12
- package/src/deploy.ts +3 -0
- package/src/get-wallet-info.ts +271 -85
- package/src/ledger-from-tx-state.ts +26 -0
- package/src/midnight-env.ts +1 -1
- package/src/mod.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/midnight-contracts",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -17,27 +17,26 @@
|
|
|
17
17
|
"./deploy": "./src/deploy.ts",
|
|
18
18
|
"./wallet-info": "./src/get-wallet-info.ts",
|
|
19
19
|
"./midnight-env": "./src/midnight-env.ts",
|
|
20
|
+
"./ledger-from-tx-state": "./src/ledger-from-tx-state.ts",
|
|
20
21
|
"./types": "./src/types.ts"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@effectstream/utils": "0.100.
|
|
24
|
+
"@effectstream/utils": "0.100.21",
|
|
24
25
|
"@scure/bip39": "^2.0.1",
|
|
25
26
|
"dotenv": "^16.4.7",
|
|
26
|
-
"@midnight-ntwrk/wallet": "5.0.0",
|
|
27
|
-
"@midnight-ntwrk/wallet-api": "5.0.0",
|
|
28
27
|
"@midnight-ntwrk/zswap": "4.0.0",
|
|
29
28
|
"@midnight-ntwrk/ledger": "4.0.0",
|
|
30
29
|
"rxjs": "^7.8.2",
|
|
31
30
|
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
|
|
32
|
-
"@midnight-ntwrk/midnight-js-network-id": "4.0.
|
|
33
|
-
"@midnight-ntwrk/midnight-js-types": "4.0.
|
|
34
|
-
"@midnight-ntwrk/midnight-js-utils": "4.0.
|
|
31
|
+
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
|
|
32
|
+
"@midnight-ntwrk/midnight-js-types": "4.0.4",
|
|
33
|
+
"@midnight-ntwrk/midnight-js-utils": "4.0.4",
|
|
35
34
|
"@midnight-ntwrk/onchain-runtime-v3": "3.0.0",
|
|
36
|
-
"@midnight-ntwrk/midnight-js-contracts": "4.0.
|
|
37
|
-
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.
|
|
38
|
-
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.
|
|
39
|
-
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.
|
|
40
|
-
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.
|
|
35
|
+
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
|
|
36
|
+
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
|
|
37
|
+
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.4",
|
|
38
|
+
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.4",
|
|
39
|
+
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.4",
|
|
41
40
|
"@midnight-ntwrk/wallet-sdk-abstractions": "2.0.0",
|
|
42
41
|
"@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
|
|
43
42
|
"@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
|
package/src/deploy.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// TODO Remove references to "src/managed" as this is not standard.
|
|
2
2
|
|
|
3
|
+
// Single WASM instance for ContractState / ContractMaintenanceAuthority.
|
|
4
|
+
import "@midnight-ntwrk/onchain-runtime-v3";
|
|
5
|
+
|
|
3
6
|
import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
|
|
4
7
|
import { Buffer } from "node:buffer";
|
|
5
8
|
import { statSync, readdirSync } from "node:fs";
|
package/src/get-wallet-info.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
LedgerParameters,
|
|
19
19
|
ZswapSecretKeys,
|
|
20
20
|
shieldedToken,
|
|
21
|
-
type UnprovenTransaction,
|
|
22
21
|
} from "@midnight-ntwrk/ledger-v8";
|
|
23
22
|
|
|
24
23
|
const log = console;
|
|
@@ -166,6 +165,150 @@ export function safeStringifyProgress(value: unknown): string {
|
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
|
|
168
|
+
/** Read spendable dust from façade state or a dust sub-wallet emission. */
|
|
169
|
+
export function resolveFacadeDustBalance(
|
|
170
|
+
facadeOrDustState: unknown,
|
|
171
|
+
at: Date = new Date(),
|
|
172
|
+
): bigint {
|
|
173
|
+
const d =
|
|
174
|
+
(facadeOrDustState as { dust?: unknown } | null | undefined)?.dust ??
|
|
175
|
+
facadeOrDustState;
|
|
176
|
+
if (d === undefined || d === null || typeof d !== "object") return 0n;
|
|
177
|
+
let w = 0n;
|
|
178
|
+
let b = 0n;
|
|
179
|
+
try {
|
|
180
|
+
const fn = (d as { walletBalance?: (t: Date) => unknown }).walletBalance;
|
|
181
|
+
if (typeof fn === "function") {
|
|
182
|
+
const v = fn.call(d, at);
|
|
183
|
+
if (typeof v === "bigint") w = v;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
/* ignore transient read errors until dust subtree fully wires */
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const fn = (d as { balance?: (t: Date) => unknown }).balance;
|
|
190
|
+
if (typeof fn === "function") {
|
|
191
|
+
const v = fn.call(d, at);
|
|
192
|
+
if (typeof v === "bigint") b = v;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
/* ignore */
|
|
196
|
+
}
|
|
197
|
+
return w > b ? w : b;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveDustHeartbeatPollMs(waitNonZero: boolean, override?: number): number {
|
|
201
|
+
if (override !== undefined) return Math.max(0, Math.floor(override));
|
|
202
|
+
if (!waitNonZero) return 0;
|
|
203
|
+
const raw = getEnv("MIDNIGHT_WAIT_DUST_POLL_INTERVAL_MS");
|
|
204
|
+
if (raw !== undefined && raw !== "") {
|
|
205
|
+
if (raw === "0") return 0;
|
|
206
|
+
const n = Number(raw);
|
|
207
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
208
|
+
}
|
|
209
|
+
return 7500;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function dustWalletState$(wallet: WalletFacade): Rx.Observable<unknown> {
|
|
213
|
+
const dust = (wallet.dust as { state?: Rx.Observable<unknown> }).state;
|
|
214
|
+
if (!dust || typeof dust.pipe !== "function") {
|
|
215
|
+
throw new Error("Dust wallet state stream is not available");
|
|
216
|
+
}
|
|
217
|
+
return dust;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Heartbeat over dust sub-wallet — safe after dust-only stops shielded/unshielded. */
|
|
221
|
+
function dustWalletStateWithHeartbeat(
|
|
222
|
+
wallet: WalletFacade,
|
|
223
|
+
pollMs: number,
|
|
224
|
+
): Rx.Observable<unknown> {
|
|
225
|
+
const dust$ = dustWalletState$(wallet);
|
|
226
|
+
if (pollMs <= 0) return dust$;
|
|
227
|
+
return Rx.combineLatest([
|
|
228
|
+
dust$,
|
|
229
|
+
Rx.interval(pollMs).pipe(Rx.startWith(-1 as const)),
|
|
230
|
+
]).pipe(Rx.map(([s]) => s));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function dustProgressFromState(ds: unknown): {
|
|
234
|
+
appliedIndex: bigint;
|
|
235
|
+
highestRelevantWalletIndex: bigint;
|
|
236
|
+
isConnected: boolean;
|
|
237
|
+
} | null {
|
|
238
|
+
const progress =
|
|
239
|
+
(ds as { state?: { progress?: unknown } })?.state?.progress ??
|
|
240
|
+
(ds as { progress?: unknown })?.progress;
|
|
241
|
+
if (!progress || typeof progress !== "object") return null;
|
|
242
|
+
const p = progress as {
|
|
243
|
+
appliedIndex?: bigint;
|
|
244
|
+
highestRelevantWalletIndex?: bigint;
|
|
245
|
+
isConnected?: boolean;
|
|
246
|
+
isStrictlyComplete?: () => boolean;
|
|
247
|
+
isCompleteWithin?: (gap: bigint) => boolean;
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
appliedIndex: p.appliedIndex ?? 0n,
|
|
251
|
+
highestRelevantWalletIndex: p.highestRelevantWalletIndex ?? 0n,
|
|
252
|
+
isConnected: p.isConnected ?? false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isDustProgressCaughtUp(ds: unknown): boolean {
|
|
257
|
+
const progress =
|
|
258
|
+
(ds as { state?: { progress?: unknown } })?.state?.progress ??
|
|
259
|
+
(ds as { progress?: unknown })?.progress;
|
|
260
|
+
if (!progress || typeof progress !== "object") return false;
|
|
261
|
+
const p = progress as {
|
|
262
|
+
appliedIndex?: bigint;
|
|
263
|
+
highestRelevantWalletIndex?: bigint;
|
|
264
|
+
isStrictlyComplete?: () => boolean;
|
|
265
|
+
};
|
|
266
|
+
if (typeof p.isStrictlyComplete === "function" && p.isStrictlyComplete()) return true;
|
|
267
|
+
const applied = p.appliedIndex ?? 0n;
|
|
268
|
+
const target = p.highestRelevantWalletIndex ?? 0n;
|
|
269
|
+
if (applied === 0n && target === 0n) return true;
|
|
270
|
+
return target > 0n && applied >= target;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Wait until dust applied index reaches chain tip (not just DUST_COMPLETION_GAP). */
|
|
274
|
+
async function waitForDustWalletProgressCatchUp(
|
|
275
|
+
wallet: WalletFacade,
|
|
276
|
+
timeoutMs: number,
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
if (timeoutMs <= 0) return;
|
|
279
|
+
const startedAt = Date.now();
|
|
280
|
+
await Rx.firstValueFrom(
|
|
281
|
+
dustWalletState$(wallet).pipe(
|
|
282
|
+
Rx.tap((ds) => {
|
|
283
|
+
const p = dustProgressFromState(ds);
|
|
284
|
+
if (!p) return;
|
|
285
|
+
const elapsedSec = ((Date.now() - startedAt) / 1000).toFixed(0);
|
|
286
|
+
log.info(
|
|
287
|
+
`Dust catch-up (${elapsedSec}s): ${p.appliedIndex}/${p.highestRelevantWalletIndex} connected=${p.isConnected}`,
|
|
288
|
+
);
|
|
289
|
+
}),
|
|
290
|
+
Rx.filter((ds) => isDustProgressCaughtUp(ds)),
|
|
291
|
+
Rx.timeout({
|
|
292
|
+
first: timeoutMs,
|
|
293
|
+
with: () =>
|
|
294
|
+
Rx.throwError(
|
|
295
|
+
() => new Error(`Dust wallet sync catch-up timeout after ${timeoutMs}ms`),
|
|
296
|
+
),
|
|
297
|
+
}),
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
log.info("Dust wallet sync catch-up complete");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Stop shielded + unshielded sync after dust is ready (dust-only ergonomics). */
|
|
304
|
+
export async function suspendAuxWalletSyncForFees(wallet: WalletFacade): Promise<void> {
|
|
305
|
+
log.info("Stopping shielded and unshielded wallet sync (dust-only mode)");
|
|
306
|
+
await Promise.all([
|
|
307
|
+
(wallet.shielded as { stop?: () => Promise<void> }).stop?.(),
|
|
308
|
+
(wallet.unshielded as { stop?: () => Promise<void> }).stop?.(),
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
|
|
169
312
|
/**
|
|
170
313
|
* Wait for wallet to be synced and funded
|
|
171
314
|
*/
|
|
@@ -250,10 +393,7 @@ export async function syncAndWaitForFunds(
|
|
|
250
393
|
|
|
251
394
|
let dustBalance = 0n;
|
|
252
395
|
try {
|
|
253
|
-
|
|
254
|
-
// Try both so this stays portable across SDK upgrades.
|
|
255
|
-
const now = new Date();
|
|
256
|
-
dustBalance = state.dust?.walletBalance?.(now) ?? state.dust?.balance?.(now) ?? 0n;
|
|
396
|
+
dustBalance = resolveFacadeDustBalance(state, new Date());
|
|
257
397
|
} catch (_err) {
|
|
258
398
|
log.warn("Could not read dust balance from synced state; continuing with dustBalance=0");
|
|
259
399
|
}
|
|
@@ -262,11 +402,25 @@ export async function syncAndWaitForFunds(
|
|
|
262
402
|
}
|
|
263
403
|
|
|
264
404
|
/**
|
|
265
|
-
* Wait for dust wallet sync and return dust balance
|
|
405
|
+
* Wait for dust wallet sync and return dust balance.
|
|
406
|
+
*
|
|
407
|
+
* Reads **`wallet.dust.state`** directly so dust-only mode (shielded/unshielded
|
|
408
|
+
* stopped) does not wedge on façade `combineLatest`.
|
|
409
|
+
*
|
|
410
|
+
* When `waitNonZero` is true, first waits for dust appliedIndex to reach tip,
|
|
411
|
+
* then polls the dust sub-wallet (heartbeat default 7500ms).
|
|
266
412
|
*/
|
|
267
413
|
export async function waitForDustFunds(
|
|
268
414
|
wallet: WalletFacade,
|
|
269
|
-
optionsOrTimeout?:
|
|
415
|
+
optionsOrTimeout?:
|
|
416
|
+
| number
|
|
417
|
+
| {
|
|
418
|
+
timeoutMs?: number;
|
|
419
|
+
waitNonZero?: boolean;
|
|
420
|
+
dustPollIntervalMs?: number;
|
|
421
|
+
/** When true with waitNonZero, skip catch-up wait (e.g. fresh 0/0 chain). */
|
|
422
|
+
skipCatchUp?: boolean;
|
|
423
|
+
},
|
|
270
424
|
): Promise<bigint> {
|
|
271
425
|
log.info("Waiting for dust wallet to sync and receive funds...");
|
|
272
426
|
|
|
@@ -276,51 +430,62 @@ export async function waitForDustFunds(
|
|
|
276
430
|
|
|
277
431
|
const syncTimeoutMs = options?.timeoutMs ?? resolveWalletSyncTimeoutMs();
|
|
278
432
|
const waitNonZero = options?.waitNonZero ?? false;
|
|
433
|
+
const pollMs = resolveDustHeartbeatPollMs(waitNonZero, options?.dustPollIntervalMs);
|
|
434
|
+
const deadline = Date.now() + syncTimeoutMs;
|
|
435
|
+
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
436
|
+
|
|
437
|
+
if (pollMs > 0) {
|
|
438
|
+
log.info(`Dust wait: dust sub-wallet heartbeat every ${pollMs}ms (disable with MIDNIGHT_WAIT_DUST_POLL_INTERVAL_MS=0)`);
|
|
439
|
+
}
|
|
279
440
|
const dustSyncStartedAt = Date.now();
|
|
280
441
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
442
|
+
if (waitNonZero && !options?.skipCatchUp && remainingMs() > 0) {
|
|
443
|
+
try {
|
|
444
|
+
await waitForDustWalletProgressCatchUp(wallet, remainingMs());
|
|
445
|
+
} catch (e) {
|
|
446
|
+
log.warn(
|
|
447
|
+
`Dust catch-up incomplete (${e instanceof Error ? e.message : String(e)}); continuing balance wait`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
285
450
|
}
|
|
286
451
|
|
|
452
|
+
const balanceBudgetMs = remainingMs();
|
|
453
|
+
if (balanceBudgetMs <= 0) {
|
|
454
|
+
throw new Error(`Dust wallet sync timeout after ${syncTimeoutMs}ms`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const state$ = dustWalletStateWithHeartbeat(wallet, pollMs);
|
|
458
|
+
const sampled$ =
|
|
459
|
+
pollMs > 0
|
|
460
|
+
? state$
|
|
461
|
+
: state$.pipe(Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS));
|
|
462
|
+
|
|
287
463
|
const dustBalance = (await Rx.firstValueFrom(
|
|
288
|
-
|
|
289
|
-
Rx.
|
|
290
|
-
Rx.tap((state: any) => {
|
|
464
|
+
sampled$.pipe(
|
|
465
|
+
Rx.tap((ds: unknown) => {
|
|
291
466
|
try {
|
|
292
467
|
const elapsedSec = ((Date.now() - dustSyncStartedAt) / 1000).toFixed(0);
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}),
|
|
306
|
-
Rx.map((state: any) => {
|
|
307
|
-
try {
|
|
308
|
-
if (typeof state.balance === "function") {
|
|
309
|
-
return state.balance(new Date()) as bigint;
|
|
310
|
-
}
|
|
311
|
-
} catch (_err) {
|
|
312
|
-
// fall through
|
|
468
|
+
const at = new Date();
|
|
469
|
+
const p = dustProgressFromState(ds);
|
|
470
|
+
const bal = resolveFacadeDustBalance(ds, at);
|
|
471
|
+
log.info(
|
|
472
|
+
`Dust wallet sync progress (${elapsedSec}s): ` +
|
|
473
|
+
`${p?.appliedIndex ?? "?"}/${p?.highestRelevantWalletIndex ?? "?"} ` +
|
|
474
|
+
`balance=${bal}`,
|
|
475
|
+
);
|
|
476
|
+
} catch {
|
|
477
|
+
/* ignore */
|
|
313
478
|
}
|
|
314
|
-
return 0n;
|
|
315
479
|
}),
|
|
480
|
+
Rx.map((ds: unknown) => resolveFacadeDustBalance(ds)),
|
|
481
|
+
Rx.filter((balance: bigint) => !waitNonZero || balance > 0n),
|
|
316
482
|
Rx.timeout({
|
|
317
|
-
|
|
483
|
+
first: balanceBudgetMs,
|
|
318
484
|
with: () =>
|
|
319
485
|
Rx.throwError(
|
|
320
486
|
() => new Error(`Dust wallet sync timeout after ${syncTimeoutMs}ms`)
|
|
321
487
|
),
|
|
322
488
|
}),
|
|
323
|
-
Rx.filter((balance: bigint) => !waitNonZero || balance > 0n),
|
|
324
489
|
Rx.tap((balance: bigint) => {
|
|
325
490
|
if (balance > 0n) log.info(`Dust wallet balance: ${balance}`);
|
|
326
491
|
})
|
|
@@ -344,6 +509,11 @@ export interface DustSyncWithRetryOptions {
|
|
|
344
509
|
networkId: NetworkId.NetworkId;
|
|
345
510
|
/** Per-emission stall timeout in ms. Default: 60_000 (1 minute). */
|
|
346
511
|
stallTimeoutMs?: number;
|
|
512
|
+
/**
|
|
513
|
+
* How long to wait for a non-zero façade dust balance after dust sync.
|
|
514
|
+
* Default: {@link resolveWalletSyncTimeoutMs} (not `stallTimeoutMs`).
|
|
515
|
+
*/
|
|
516
|
+
balanceWaitTimeoutMs?: number;
|
|
347
517
|
/** Maximum retry attempts on stall. Default: 5. */
|
|
348
518
|
maxRetries?: number;
|
|
349
519
|
/** Throttle interval for state emissions in ms. Default: 10_000 (10s). */
|
|
@@ -374,6 +544,7 @@ export async function waitForDustFundsWithRetry(
|
|
|
374
544
|
seed,
|
|
375
545
|
networkId,
|
|
376
546
|
stallTimeoutMs = DUST_STALL_TIMEOUT_MS,
|
|
547
|
+
balanceWaitTimeoutMs = resolveWalletSyncTimeoutMs(),
|
|
377
548
|
maxRetries = DUST_MAX_RETRIES,
|
|
378
549
|
throttleMs = CONSTANTS.WALLET_SYNC_THROTTLE_MS,
|
|
379
550
|
syncMode = 'dust-only',
|
|
@@ -440,6 +611,7 @@ export async function waitForDustFundsWithRetry(
|
|
|
440
611
|
networkId,
|
|
441
612
|
syncMode,
|
|
442
613
|
stateToRestore,
|
|
614
|
+
{ stopAuxWalletsImmediately: syncMode !== 'dust-only' },
|
|
443
615
|
);
|
|
444
616
|
}
|
|
445
617
|
|
|
@@ -531,20 +703,44 @@ export async function waitForDustFundsWithRetry(
|
|
|
531
703
|
`connected=${syncProgress?.isConnected ?? "?"}`,
|
|
532
704
|
);
|
|
533
705
|
|
|
534
|
-
// Sync complete —
|
|
706
|
+
// Sync complete — read balance from dust sub-wallet (not façade).
|
|
535
707
|
let dustBalance = 0n;
|
|
536
708
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
709
|
+
dustBalance = await waitForDustFunds(walletResult!.wallet, {
|
|
710
|
+
timeoutMs: balanceWaitTimeoutMs,
|
|
711
|
+
waitNonZero: true,
|
|
712
|
+
});
|
|
542
713
|
} catch {
|
|
543
|
-
// ignore
|
|
714
|
+
// ignore — will return 0n
|
|
544
715
|
}
|
|
545
716
|
|
|
546
717
|
log.info(`Dust sync complete (attempt ${attempt}). Balance: ${dustBalance}`);
|
|
547
718
|
|
|
719
|
+
if (dustBalance === 0n) {
|
|
720
|
+
log.info("No dust after sync; attempting unshielded NIGHT registration...");
|
|
721
|
+
try {
|
|
722
|
+
if (await registerNightForDust(walletResult!)) {
|
|
723
|
+
dustBalance = await waitForDustFunds(walletResult!.wallet, {
|
|
724
|
+
timeoutMs: balanceWaitTimeoutMs,
|
|
725
|
+
waitNonZero: true,
|
|
726
|
+
});
|
|
727
|
+
log.info(`Dust balance after registration: ${dustBalance}`);
|
|
728
|
+
}
|
|
729
|
+
} catch (e) {
|
|
730
|
+
log.warn(
|
|
731
|
+
`NIGHT→dust registration failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (dustBalance === 0n) {
|
|
737
|
+
log.warn("Dust balance still 0 — wallet needs dust or unshielded NIGHT to register.");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (syncMode === 'dust-only') {
|
|
741
|
+
await suspendAuxWalletSyncForFees(walletResult!.wallet);
|
|
742
|
+
}
|
|
743
|
+
|
|
548
744
|
// Save final state to disk
|
|
549
745
|
await serializeAndSave(walletResult);
|
|
550
746
|
|
|
@@ -587,9 +783,21 @@ export async function waitForDustFundsWithRetry(
|
|
|
587
783
|
}
|
|
588
784
|
|
|
589
785
|
export function getInitialDustState(
|
|
590
|
-
dustWallet:
|
|
591
|
-
|
|
592
|
-
|
|
786
|
+
dustWallet: { state: Rx.Observable<unknown> },
|
|
787
|
+
options?: { timeoutMs?: number },
|
|
788
|
+
): Promise<unknown> {
|
|
789
|
+
const obs = dustWallet.state;
|
|
790
|
+
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
791
|
+
return Rx.firstValueFrom(
|
|
792
|
+
obs.pipe(
|
|
793
|
+
Rx.timeout({
|
|
794
|
+
first: options.timeoutMs,
|
|
795
|
+
with: () => Rx.throwError(() => new Error("Timeout reading dust wallet state")),
|
|
796
|
+
}),
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return Rx.firstValueFrom(obs);
|
|
593
801
|
}
|
|
594
802
|
|
|
595
803
|
/**
|
|
@@ -664,6 +872,7 @@ export async function buildWalletFacade(
|
|
|
664
872
|
networkId: NetworkId.NetworkId,
|
|
665
873
|
syncMode: WalletSyncMode = 'all',
|
|
666
874
|
dustSerializedState?: string | null,
|
|
875
|
+
options?: { stopAuxWalletsImmediately?: boolean },
|
|
667
876
|
): Promise<WalletResult> {
|
|
668
877
|
const shieldedSeed = deriveSeedForRole(seed, Roles.Zswap);
|
|
669
878
|
const dustSeed = deriveSeedForRole(seed, Roles.Dust);
|
|
@@ -689,17 +898,18 @@ export async function buildWalletFacade(
|
|
|
689
898
|
|
|
690
899
|
await wallet.start(zswapSecretKeys, dustSecretKey);
|
|
691
900
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
(wallet.unshielded as any).stop?.(),
|
|
697
|
-
]);
|
|
901
|
+
const stopAuxNow =
|
|
902
|
+
syncMode === 'dust-only' && (options?.stopAuxWalletsImmediately ?? true);
|
|
903
|
+
if (stopAuxNow) {
|
|
904
|
+
await suspendAuxWalletSyncForFees(wallet);
|
|
698
905
|
}
|
|
699
906
|
|
|
700
907
|
const unshieldedAddress = unshieldedKeystore.getBech32Address().asString();
|
|
701
|
-
const dustState = await getInitialDustState(wallet.dust);
|
|
702
|
-
const dustAddress = MidnightBech32m.encode(
|
|
908
|
+
const dustState = await getInitialDustState(wallet.dust as { state: Rx.Observable<unknown> });
|
|
909
|
+
const dustAddress = MidnightBech32m.encode(
|
|
910
|
+
networkId,
|
|
911
|
+
(dustState as { address: unknown }).address,
|
|
912
|
+
).asString();
|
|
703
913
|
|
|
704
914
|
return {
|
|
705
915
|
wallet,
|
|
@@ -759,9 +969,7 @@ export async function registerNightForDust(
|
|
|
759
969
|
|
|
760
970
|
const state = await Rx.firstValueFrom(
|
|
761
971
|
walletResult.wallet.state().pipe(
|
|
762
|
-
|
|
763
972
|
Rx.filter((s: any) => {
|
|
764
|
-
// Only require unshielded+dust sync for dust registration (shielded is not needed)
|
|
765
973
|
const dustSynced = s.dust?.state?.progress?.isStrictlyComplete() ?? false;
|
|
766
974
|
const unshieldedSynced = s.unshielded?.progress?.isStrictlyComplete() ?? false;
|
|
767
975
|
return dustSynced && unshieldedSynced;
|
|
@@ -791,37 +999,17 @@ export async function registerNightForDust(
|
|
|
791
999
|
(payload: Uint8Array) => walletResult.unshieldedKeystore.signData(payload)
|
|
792
1000
|
);
|
|
793
1001
|
|
|
794
|
-
const signedRecipe: UnprovenTransaction = await (walletResult.wallet as any).signUnprovenTransaction(
|
|
795
|
-
recipe.transaction,
|
|
796
|
-
(payload: Uint8Array) => walletResult.unshieldedKeystore.signData(payload),
|
|
797
|
-
);
|
|
798
|
-
|
|
799
1002
|
log.info("Submitting dust registration transaction...");
|
|
800
1003
|
const txId = await walletResult.wallet.submitTransaction(
|
|
801
|
-
await (walletResult.wallet as any).
|
|
1004
|
+
await (walletResult.wallet as any).finalizeRecipe(recipe),
|
|
802
1005
|
);
|
|
803
1006
|
log.info(`Dust registration submitted with tx id: ${txId}`);
|
|
804
1007
|
|
|
805
1008
|
log.info("Waiting for dust to be generated...");
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return s.dust?.walletBalance?.(now) ?? s.dust?.balance?.(now) ?? 0n;
|
|
811
|
-
};
|
|
812
|
-
await Rx.firstValueFrom(
|
|
813
|
-
walletResult.wallet.state().pipe(
|
|
814
|
-
Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS),
|
|
815
|
-
Rx.tap((s: any) => {
|
|
816
|
-
log.info(`Current dust balance: ${readDustBalance(s)}`);
|
|
817
|
-
}),
|
|
818
|
-
Rx.filter((s: any) => readDustBalance(s) > 0n),
|
|
819
|
-
Rx.timeout({
|
|
820
|
-
each: resolveWalletSyncTimeoutMs(),
|
|
821
|
-
with: () => Rx.throwError(() => new Error("Timeout waiting for dust generation"))
|
|
822
|
-
})
|
|
823
|
-
)
|
|
824
|
-
);
|
|
1009
|
+
await waitForDustFunds(walletResult.wallet, {
|
|
1010
|
+
waitNonZero: true,
|
|
1011
|
+
timeoutMs: resolveWalletSyncTimeoutMs(),
|
|
1012
|
+
});
|
|
825
1013
|
|
|
826
1014
|
log.info("Dust registration complete!");
|
|
827
1015
|
return true;
|
|
@@ -973,9 +1161,7 @@ async function main() {
|
|
|
973
1161
|
|
|
974
1162
|
try {
|
|
975
1163
|
const state = await Rx.firstValueFrom(walletResult.wallet.state());
|
|
976
|
-
|
|
977
|
-
const now = new Date();
|
|
978
|
-
dustBalance = (state as any).dust?.walletBalance?.(now) ?? (state as any).dust?.balance?.(now) ?? 0n;
|
|
1164
|
+
dustBalance = resolveFacadeDustBalance(state, new Date());
|
|
979
1165
|
|
|
980
1166
|
if (dustBalance === 0n) {
|
|
981
1167
|
log.info("Dust balance is 0. Attempting to sync dust wallet...");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type MidnightLedgerFn = (state: unknown) => unknown;
|
|
2
|
+
|
|
3
|
+
export type MidnightContractStateDeserializer = {
|
|
4
|
+
deserialize: (bytes: Uint8Array) => { data: { state: unknown } };
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a sync-safe ledger parser for raw transaction contract state hex.
|
|
9
|
+
*
|
|
10
|
+
* Pass `ContractState` from the app's `@midnight-ntwrk/compact-runtime` so the
|
|
11
|
+
* deserialized `StateValue` matches the WASM module used by generated
|
|
12
|
+
* `contract.ledger()` helpers (required when @effectstream/sync resolves
|
|
13
|
+
* dependencies from a different node_modules tree than the app template).
|
|
14
|
+
*/
|
|
15
|
+
export function midnightLedgerFromTxStateHex(
|
|
16
|
+
ledger: MidnightLedgerFn,
|
|
17
|
+
contractState: MidnightContractStateDeserializer,
|
|
18
|
+
): (rawHexState: string) => Record<string, unknown> {
|
|
19
|
+
return (rawHexState: string) => {
|
|
20
|
+
const byteState = new Uint8Array(
|
|
21
|
+
rawHexState.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),
|
|
22
|
+
);
|
|
23
|
+
const stateValue = contractState.deserialize(byteState).data.state;
|
|
24
|
+
return ledger(stateValue) as Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/midnight-env.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { NetworkId } from "@midnight-ntwrk/wallet-sdk-abstractions";
|
|
2
|
-
import { mnemonicToSeed } from "@scure/bip39";
|
|
3
2
|
import { Buffer } from "node:buffer";
|
|
4
3
|
import { getEnv } from "@effectstream/utils/runtime";
|
|
5
4
|
|
|
@@ -69,6 +68,7 @@ let walletSeed: string;
|
|
|
69
68
|
if (env("MIDNIGHT_WALLET_SEED")) {
|
|
70
69
|
walletSeed = env("MIDNIGHT_WALLET_SEED");
|
|
71
70
|
} else if (env("MIDNIGHT_WALLET_MNEMONIC")) {
|
|
71
|
+
const { mnemonicToSeed } = await import("@scure/bip39");
|
|
72
72
|
walletSeed = Buffer.from(await mnemonicToSeed(env("MIDNIGHT_WALLET_MNEMONIC"))).toString('hex');
|
|
73
73
|
} else {
|
|
74
74
|
walletSeed = selectedNetworkConfig.genesisWalletSeed;
|
package/src/mod.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type {
|
|
|
8
8
|
} from "./types.ts";
|
|
9
9
|
export {
|
|
10
10
|
resolveWalletSyncTimeoutMs,
|
|
11
|
+
suspendAuxWalletSyncForFees,
|
|
12
|
+
resolveFacadeDustBalance,
|
|
11
13
|
safeStringifyProgress,
|
|
12
14
|
syncAndWaitForFunds,
|
|
13
15
|
buildWalletFacade,
|
|
@@ -19,7 +21,10 @@ export {
|
|
|
19
21
|
saveDustState,
|
|
20
22
|
loadDustState,
|
|
21
23
|
} from "./get-wallet-info.ts";
|
|
22
|
-
export type {
|
|
24
|
+
export type {
|
|
25
|
+
WalletSyncMode,
|
|
26
|
+
DustSyncWithRetryOptions,
|
|
27
|
+
} from "./get-wallet-info.ts";
|
|
23
28
|
export { CONSTANTS } from "./constants.ts";
|
|
24
29
|
export {
|
|
25
30
|
buildWalletAndWaitForFunds,
|
|
@@ -27,4 +32,4 @@ export {
|
|
|
27
32
|
} from "./build-wallet.ts";
|
|
28
33
|
export { readMidnightContract } from "./read-contract.ts";
|
|
29
34
|
export { configureMidnightNodeProviders } from "./providers.ts";
|
|
30
|
-
export { midnightNetworkConfig } from "./midnight-env.ts";
|
|
35
|
+
export { midnightNetworkConfig } from "./midnight-env.ts";
|