@midnightntwrk/wallet-sdk-testkit 1.0.0-beta.3 → 1.0.0-beta.4
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 +49 -1
- package/dist/core.d.ts +1 -0
- package/dist/core.js +1 -0
- package/dist/dust-sync.d.ts +126 -0
- package/dist/dust-sync.js +140 -0
- package/dist/logger.js +1 -1
- package/dist/scenarios/dust.d.ts +14 -2
- package/dist/scenarios/dust.js +12 -6
- package/dist/scenarios/token-transfer.d.ts +17 -3
- package/dist/scenarios/token-transfer.js +23 -10
- package/dist/state-waiters.d.ts +26 -0
- package/dist/state-waiters.js +50 -22
- package/dist/wallet.d.ts +63 -3
- package/dist/wallet.js +73 -24
- package/package.json +14 -11
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ files.
|
|
|
11
11
|
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
12
12
|
| Environment | `createRemoteEnvironment`, `NETWORK_PRESETS`, `makeEnvironment`, `WalletTestEnvironment`, `ResolvedEndpoints` |
|
|
13
13
|
| Environment (Docker) | `createTestContainersEnvironment` — from `@midnightntwrk/wallet-sdk-testkit/testcontainers` |
|
|
14
|
-
| Wallet | `provideWallet`, `initWalletWithSeed`, `saveState`, `WalletInit`
|
|
14
|
+
| Wallet | `provideWallet`, `initWalletWithSeed`, `saveState`, `shouldPersistState`, `WalletInit` |
|
|
15
|
+
| Dust sync model | `dustWalletFromEnv`, `dustWalletFor`, `DustSyncModel`, `projectionsDustSyncOptions`, … — see below |
|
|
15
16
|
| Seeds | `getShieldedSeed`, `getUnshieldedSeed`, `getDustSeed` |
|
|
16
17
|
| Sync waiters | `waitForSyncUnshielded`, `waitForDustBalance`, `waitForTxInHistory`, … |
|
|
17
18
|
| Assertions | `expectSenderShieldedTxHistory`, `expectReceiverUnshieldedTxHistory`, … |
|
|
@@ -61,6 +62,53 @@ import { createTestContainersEnvironment } from '@midnightntwrk/wallet-sdk-testk
|
|
|
61
62
|
const getEnv = useWalletTestEnvironment(() => createTestContainersEnvironment({ network: 'undeployed' }));
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
## Choosing the Dust sync model
|
|
66
|
+
|
|
67
|
+
Dust can sync from the indexer's event stream or from its projections (the "event-less" fast sync). `DUST_SYNC` selects
|
|
68
|
+
which, for a whole run:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
DUST_SYNC=projections yarn test-remote -- -t @healthcheck
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Unset, `provideWallet` and `initWalletWithSeed` build the event-based sync; the dust and token-transfer healthcheck
|
|
75
|
+
scenarios fall back to projections instead, so the network being monitored is exercised against the model the wallet
|
|
76
|
+
ships to users. `DUST_SYNC` overrides both, so a run can never end up half on one model and half on the other. An
|
|
77
|
+
unrecognized value is rejected rather than defaulted, so a typo cannot report a run as covering a model it did not
|
|
78
|
+
exercise.
|
|
79
|
+
|
|
80
|
+
A test that needs a specific model regardless of the environment pins it in code, which does override `DUST_SYNC`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
provideWallet(env, { ...projectionsDustSyncOptions, seed, syncCacheDir, filename });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `manualProjectionsDustSyncOptions` instead when the test drives each sync pass itself with `facade.doSync()`; the
|
|
87
|
+
plain `projectionsDustSyncOptions` leaves background synchronization on, so the usual state waiters work unchanged.
|
|
88
|
+
|
|
89
|
+
Pin a model only when the test genuinely requires it. A pinned wallet stops following `DUST_SYNC`, so a lane switched by
|
|
90
|
+
environment will leave it behind — which reports coverage the run did not have.
|
|
91
|
+
|
|
92
|
+
### Building a Dust wallet directly
|
|
93
|
+
|
|
94
|
+
A test that constructs its own Dust wallet rather than going through the helpers must build it from the same source, or
|
|
95
|
+
it will silently disagree with the rest of the run:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const Dust = dustWalletFromEnv()({ ...walletConfig, ...env.getDustWalletConfig() });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This matters most for serialize/restore tests. The two models disagree about the meaning of the single progress value a
|
|
102
|
+
Dust snapshot carries — an event cursor to one, a composite of tree indices and nullifier count to the other — so state
|
|
103
|
+
written by one model and restored into the other resumes from a position that is not a cursor at all, and the wallet
|
|
104
|
+
never reports synced. Hard-coding `DustWallet(...)` as the restore target while the run uses projections does exactly
|
|
105
|
+
that.
|
|
106
|
+
|
|
107
|
+
Snapshots on disk are protected from this automatically: they are stored per model, so switching models finds no
|
|
108
|
+
snapshot and rebuilds from scratch rather than resuming wrongly, with no cache to clear by hand. That protection does
|
|
109
|
+
not extend to state serialized and restored in memory within a test — hence the rule above. The shielded and unshielded
|
|
110
|
+
snapshots are model-independent and shared.
|
|
111
|
+
|
|
64
112
|
## Peer dependencies
|
|
65
113
|
|
|
66
114
|
- `vitest` — required (assertions and the environment hooks use it).
|
package/dist/core.d.ts
CHANGED
package/dist/core.js
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type DustParameters } from '@midnightntwrk/ledger-v9';
|
|
2
|
+
import { type DefaultDustConfiguration, type DustWalletAPI } from '@midnightntwrk/wallet-sdk-dust-wallet';
|
|
3
|
+
/**
|
|
4
|
+
* The factory shape {@link provideWallet} and the scenarios accept for the dust sub-wallet.
|
|
5
|
+
*
|
|
6
|
+
* Structural rather than `DustWalletClass`, so a single-variant composition is acceptable too — but note that
|
|
7
|
+
* {@link eventLessDustWallet}, the projections factory this exists for, is **not** one: it is the shipped two-variant
|
|
8
|
+
* wallet with the V2 variant's sync service swapped, which is what lets it build transactions as well as sync. On a
|
|
9
|
+
* chain running ledger-v9 from its first block the V1 variant never applies, so it reaches the projections sync
|
|
10
|
+
* immediately. A start resolving asynchronously is accepted because such a wallet may ask the chain where it is before
|
|
11
|
+
* it picks a variant.
|
|
12
|
+
*/
|
|
13
|
+
export type DustWalletFactory = (config: DefaultDustConfiguration) => {
|
|
14
|
+
startWithSeed(seed: Uint8Array, dustParameters?: DustParameters): DustWalletAPI | Promise<DustWalletAPI>;
|
|
15
|
+
/** Also required, because `provideWallet` restores a snapshot into whatever composition it was given. */
|
|
16
|
+
restore(serializedState: string): DustWalletAPI;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A dust sub-wallet that syncs from indexer projections instead of the event stream.
|
|
20
|
+
*
|
|
21
|
+
* The shipped `DustWallet` with exactly one substitution — the V2 variant's sync service. The V1 variant keeps the
|
|
22
|
+
* event stream, because the projections sync is a ledger-v9 capability: it rests on `DustLocalState` members no
|
|
23
|
+
* ledger-v8 has. On a chain that runs ledger-v9 from its first block the V1 variant never applies, so a wallet built
|
|
24
|
+
* this way reaches the projections sync immediately.
|
|
25
|
+
*
|
|
26
|
+
* The projections sync synchronizes in finite passes rather than over a live subscription, but background
|
|
27
|
+
* synchronization re-runs those passes on an interval, so this factory can be used on its own and the usual state
|
|
28
|
+
* waiters behave as they do for the event-based sync. Pair it with `manualSync` — see
|
|
29
|
+
* {@link manualProjectionsDustSyncOptions} — only when a caller wants to decide when each pass happens.
|
|
30
|
+
*
|
|
31
|
+
* **Both variants are registered, so a wallet built this way transacts.** A single-variant composition would answer for
|
|
32
|
+
* the whole protocol timeline and therefore report the minimum supported version; because a facade acts at the lowest
|
|
33
|
+
* version its three sub-wallets report, such a sub-wallet holds the facade below the ledger-v9 boundary and the facade
|
|
34
|
+
* then refuses the shielded wallet's ledger-v9 transaction, the two sides of a boundary being unmergeable. Registering
|
|
35
|
+
* both variants leaves the version to the chain, which is what lets the healthcheck scenarios pay fees and transfer
|
|
36
|
+
* while syncing their Dust from projections.
|
|
37
|
+
*/
|
|
38
|
+
export declare const eventLessDustWallet: DustWalletFactory;
|
|
39
|
+
/**
|
|
40
|
+
* The event-stream dust sub-wallet, with the long-lived subscription.
|
|
41
|
+
*
|
|
42
|
+
* A dust snapshot stores one progress value, `appliedIndex`, and the two sync models mean different things by it: this
|
|
43
|
+
* service treats it as a ledger-event cursor to resume its subscription from, while the projections service writes a
|
|
44
|
+
* composite of tree indices and nullifier count. Restoring a projections-written snapshot here would therefore resume
|
|
45
|
+
* the subscription from a position that is not an event id at all, and because the cursor is trusted rather than
|
|
46
|
+
* validated, the effect would be silently skipped events rather than an error.
|
|
47
|
+
*
|
|
48
|
+
* Callers do not have to guard against that: dust snapshots are stored per sync model (see {@link dustSnapshotPath}), so
|
|
49
|
+
* switching a wallet's model finds no snapshot and rebuilds from scratch instead of resuming from a cursor that is not
|
|
50
|
+
* one.
|
|
51
|
+
*/
|
|
52
|
+
export declare const eventBasedDustWallet: DustWalletFactory;
|
|
53
|
+
/** Which of the two dust sync implementations a wallet is built with. */
|
|
54
|
+
export type DustSyncModel = 'events' | 'projections';
|
|
55
|
+
/**
|
|
56
|
+
* How a dust snapshot on disk is labelled with the sync model that wrote it.
|
|
57
|
+
*
|
|
58
|
+
* `custom` covers a caller-supplied factory whose model cannot be identified. Giving it its own namespace is the safe
|
|
59
|
+
* default: an unidentified factory then shares a snapshot with neither known model rather than with the wrong one.
|
|
60
|
+
*/
|
|
61
|
+
export type DustSnapshotModel = DustSyncModel | 'custom';
|
|
62
|
+
/** The dust sub-wallet factory for a sync model. */
|
|
63
|
+
export declare const dustWalletFor: (model: DustSyncModel) => DustWalletFactory;
|
|
64
|
+
/** The sync model a factory implements, or `custom` if it is neither of the two built-in ones. */
|
|
65
|
+
export declare const dustSyncModelOf: (factory: DustWalletFactory) => DustSnapshotModel;
|
|
66
|
+
/** Environment variable selecting the dust sync model for a whole test run. */
|
|
67
|
+
export declare const DUST_SYNC_ENV_VAR = "DUST_SYNC";
|
|
68
|
+
/**
|
|
69
|
+
* Reads a dust sync model from its environment-variable spelling, falling back to `fallback` when unset or empty.
|
|
70
|
+
*
|
|
71
|
+
* An unrecognized value is rejected rather than defaulted. A silent fallback would mean a typo in `DUST_SYNC` reports a
|
|
72
|
+
* run as covering one sync model while it actually covered the other, which is worse than not running at all.
|
|
73
|
+
*
|
|
74
|
+
* @throws Error if `raw` is neither empty nor a known model
|
|
75
|
+
*/
|
|
76
|
+
export declare const parseDustSyncModel: (raw: string | undefined, fallback?: DustSyncModel) => DustSyncModel;
|
|
77
|
+
/** The dust sync model selected by `DUST_SYNC`, or `fallback` when it is unset. */
|
|
78
|
+
export declare const dustSyncModelFromEnv: (env?: Record<string, string | undefined>, fallback?: DustSyncModel) => DustSyncModel;
|
|
79
|
+
/**
|
|
80
|
+
* The dust sub-wallet factory selected by `DUST_SYNC`, or the one for `fallback` when it is unset.
|
|
81
|
+
*
|
|
82
|
+
* This is the default an explicit `dustWallet` option overrides, so a whole lane can be switched between the two sync
|
|
83
|
+
* models by configuration while individual tests that need a specific model keep pinning it in code.
|
|
84
|
+
*
|
|
85
|
+
* `fallback` lets a caller choose which model applies when nothing is configured **without** taking the choice away
|
|
86
|
+
* from `DUST_SYNC`. Passing an explicit `dustWallet` instead would pin the model outright and make the variable
|
|
87
|
+
* ineffective for that wallet — which silently splits a lane, since some of its tests would switch and others would
|
|
88
|
+
* not.
|
|
89
|
+
*/
|
|
90
|
+
export declare const dustWalletFromEnv: (env?: Record<string, string | undefined>, fallback?: DustSyncModel) => DustWalletFactory;
|
|
91
|
+
/**
|
|
92
|
+
* Where the dust sub-wallet's snapshot lives, namespaced by the sync model that wrote it.
|
|
93
|
+
*
|
|
94
|
+
* The two models disagree on the meaning of the one progress value a snapshot carries, so a snapshot must never be
|
|
95
|
+
* restored into the other model — see {@link eventBasedDustWallet}. Keeping them in separate files makes a model switch
|
|
96
|
+
* degrade to a from-scratch build rather than to a wrong resume position, with no cache-clearing step to remember.
|
|
97
|
+
*
|
|
98
|
+
* The shielded and unshielded snapshots are model-independent and are not namespaced.
|
|
99
|
+
*/
|
|
100
|
+
export declare const dustSnapshotPath: (syncCacheDir: string, filename: string, model: DustSnapshotModel) => string;
|
|
101
|
+
/**
|
|
102
|
+
* The projections dust sync, synchronizing in the background like the event-based sync does.
|
|
103
|
+
*
|
|
104
|
+
* This is the plain drop-in: spread it into the wallet options and the usual state waiters work unchanged, because
|
|
105
|
+
* background synchronization re-runs the projections passes on an interval.
|
|
106
|
+
*
|
|
107
|
+
* Use {@link manualProjectionsDustSyncOptions} instead when a test needs to decide when each pass happens.
|
|
108
|
+
*/
|
|
109
|
+
export declare const projectionsDustSyncOptions: {
|
|
110
|
+
readonly dustWallet: DustWalletFactory;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The projections dust sync with background synchronization switched off, so the caller decides when each pass runs.
|
|
114
|
+
*
|
|
115
|
+
* Use this when a test needs passes to happen at known points — asserting on the state a specific pass produced, for
|
|
116
|
+
* instance. A caller that spreads this in must drive `facade.doSync(seeds)` itself, after start and again after
|
|
117
|
+
* anything that changes dust state; waiting on `waitForSyncedState()` alone will block, because with `manualSync`
|
|
118
|
+
* nothing advances the dust wallet until `doSync` runs.
|
|
119
|
+
*
|
|
120
|
+
* For a test that just wants the wallet to keep up on its own, spread {@link projectionsDustSyncOptions} instead and let
|
|
121
|
+
* background synchronization run the passes.
|
|
122
|
+
*/
|
|
123
|
+
export declare const manualProjectionsDustSyncOptions: {
|
|
124
|
+
readonly dustWallet: DustWalletFactory;
|
|
125
|
+
readonly manualSync: true;
|
|
126
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { LedgerParameters } from '@midnightntwrk/ledger-v9';
|
|
14
|
+
import { makeIndexerChainVersionProbe } from '@midnightntwrk/wallet-sdk-capabilities/chainVersion';
|
|
15
|
+
import { asV8DustParameters, CustomForkingDustWallet, DustWallet, makeEventLessSyncCapability, makeEventLessSyncService, } from '@midnightntwrk/wallet-sdk-dust-wallet';
|
|
16
|
+
import { V1Builder } from '@midnightntwrk/wallet-sdk-dust-wallet/v1';
|
|
17
|
+
import { Migration, V2Builder } from '@midnightntwrk/wallet-sdk-dust-wallet/v2';
|
|
18
|
+
/**
|
|
19
|
+
* A dust sub-wallet that syncs from indexer projections instead of the event stream.
|
|
20
|
+
*
|
|
21
|
+
* The shipped `DustWallet` with exactly one substitution — the V2 variant's sync service. The V1 variant keeps the
|
|
22
|
+
* event stream, because the projections sync is a ledger-v9 capability: it rests on `DustLocalState` members no
|
|
23
|
+
* ledger-v8 has. On a chain that runs ledger-v9 from its first block the V1 variant never applies, so a wallet built
|
|
24
|
+
* this way reaches the projections sync immediately.
|
|
25
|
+
*
|
|
26
|
+
* The projections sync synchronizes in finite passes rather than over a live subscription, but background
|
|
27
|
+
* synchronization re-runs those passes on an interval, so this factory can be used on its own and the usual state
|
|
28
|
+
* waiters behave as they do for the event-based sync. Pair it with `manualSync` — see
|
|
29
|
+
* {@link manualProjectionsDustSyncOptions} — only when a caller wants to decide when each pass happens.
|
|
30
|
+
*
|
|
31
|
+
* **Both variants are registered, so a wallet built this way transacts.** A single-variant composition would answer for
|
|
32
|
+
* the whole protocol timeline and therefore report the minimum supported version; because a facade acts at the lowest
|
|
33
|
+
* version its three sub-wallets report, such a sub-wallet holds the facade below the ledger-v9 boundary and the facade
|
|
34
|
+
* then refuses the shielded wallet's ledger-v9 transaction, the two sides of a boundary being unmergeable. Registering
|
|
35
|
+
* both variants leaves the version to the chain, which is what lets the healthcheck scenarios pay fees and transfer
|
|
36
|
+
* while syncing their Dust from projections.
|
|
37
|
+
*/
|
|
38
|
+
export const eventLessDustWallet = (config) => {
|
|
39
|
+
const dustParameters = config.dustParameters ?? LedgerParameters.initialParameters().dust;
|
|
40
|
+
return CustomForkingDustWallet({ ...config, chainVersionProbe: config.chainVersionProbe ?? makeIndexerChainVersionProbe(config) }, {
|
|
41
|
+
builder: new V1Builder().withDefaults(),
|
|
42
|
+
// The one field that cannot be shared: `dustParameters` is a WASM object of whichever ledger module produced it,
|
|
43
|
+
// so the V1 variant is handed the ledger-v8 rebuild of the same rates rather than the object itself.
|
|
44
|
+
configuration: { ...config, dustParameters: asV8DustParameters(dustParameters) },
|
|
45
|
+
}, {
|
|
46
|
+
builder: new V2Builder()
|
|
47
|
+
.withDefaults()
|
|
48
|
+
.withSync(makeEventLessSyncService, makeEventLessSyncCapability)
|
|
49
|
+
// Restated because `withSync` drops it: the seed-to-key derivation a start from a seed needs.
|
|
50
|
+
.withStartAuxDefaults()
|
|
51
|
+
.withMigration(() => Migration.makeCrossLedgerMigration({ dustParameters })),
|
|
52
|
+
configuration: config,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The event-stream dust sub-wallet, with the long-lived subscription.
|
|
57
|
+
*
|
|
58
|
+
* A dust snapshot stores one progress value, `appliedIndex`, and the two sync models mean different things by it: this
|
|
59
|
+
* service treats it as a ledger-event cursor to resume its subscription from, while the projections service writes a
|
|
60
|
+
* composite of tree indices and nullifier count. Restoring a projections-written snapshot here would therefore resume
|
|
61
|
+
* the subscription from a position that is not an event id at all, and because the cursor is trusted rather than
|
|
62
|
+
* validated, the effect would be silently skipped events rather than an error.
|
|
63
|
+
*
|
|
64
|
+
* Callers do not have to guard against that: dust snapshots are stored per sync model (see {@link dustSnapshotPath}), so
|
|
65
|
+
* switching a wallet's model finds no snapshot and rebuilds from scratch instead of resuming from a cursor that is not
|
|
66
|
+
* one.
|
|
67
|
+
*/
|
|
68
|
+
export const eventBasedDustWallet = DustWallet;
|
|
69
|
+
/** The dust sub-wallet factory for a sync model. */
|
|
70
|
+
export const dustWalletFor = (model) => model === 'projections' ? eventLessDustWallet : eventBasedDustWallet;
|
|
71
|
+
/** The sync model a factory implements, or `custom` if it is neither of the two built-in ones. */
|
|
72
|
+
export const dustSyncModelOf = (factory) => factory === eventLessDustWallet ? 'projections' : factory === eventBasedDustWallet ? 'events' : 'custom';
|
|
73
|
+
/** Environment variable selecting the dust sync model for a whole test run. */
|
|
74
|
+
export const DUST_SYNC_ENV_VAR = 'DUST_SYNC';
|
|
75
|
+
/**
|
|
76
|
+
* Reads a dust sync model from its environment-variable spelling, falling back to `fallback` when unset or empty.
|
|
77
|
+
*
|
|
78
|
+
* An unrecognized value is rejected rather than defaulted. A silent fallback would mean a typo in `DUST_SYNC` reports a
|
|
79
|
+
* run as covering one sync model while it actually covered the other, which is worse than not running at all.
|
|
80
|
+
*
|
|
81
|
+
* @throws Error if `raw` is neither empty nor a known model
|
|
82
|
+
*/
|
|
83
|
+
export const parseDustSyncModel = (raw, fallback = 'events') => {
|
|
84
|
+
const value = raw?.trim();
|
|
85
|
+
if (value === undefined || value === '')
|
|
86
|
+
return fallback;
|
|
87
|
+
if (value === 'events' || value === 'projections')
|
|
88
|
+
return value;
|
|
89
|
+
throw new Error(`${DUST_SYNC_ENV_VAR} must be 'events' or 'projections', got '${raw}'`);
|
|
90
|
+
};
|
|
91
|
+
/** The dust sync model selected by `DUST_SYNC`, or `fallback` when it is unset. */
|
|
92
|
+
export const dustSyncModelFromEnv = (env = process.env, fallback = 'events') => parseDustSyncModel(env[DUST_SYNC_ENV_VAR], fallback);
|
|
93
|
+
/**
|
|
94
|
+
* The dust sub-wallet factory selected by `DUST_SYNC`, or the one for `fallback` when it is unset.
|
|
95
|
+
*
|
|
96
|
+
* This is the default an explicit `dustWallet` option overrides, so a whole lane can be switched between the two sync
|
|
97
|
+
* models by configuration while individual tests that need a specific model keep pinning it in code.
|
|
98
|
+
*
|
|
99
|
+
* `fallback` lets a caller choose which model applies when nothing is configured **without** taking the choice away
|
|
100
|
+
* from `DUST_SYNC`. Passing an explicit `dustWallet` instead would pin the model outright and make the variable
|
|
101
|
+
* ineffective for that wallet — which silently splits a lane, since some of its tests would switch and others would
|
|
102
|
+
* not.
|
|
103
|
+
*/
|
|
104
|
+
export const dustWalletFromEnv = (env = process.env, fallback = 'events') => dustWalletFor(dustSyncModelFromEnv(env, fallback));
|
|
105
|
+
/**
|
|
106
|
+
* Where the dust sub-wallet's snapshot lives, namespaced by the sync model that wrote it.
|
|
107
|
+
*
|
|
108
|
+
* The two models disagree on the meaning of the one progress value a snapshot carries, so a snapshot must never be
|
|
109
|
+
* restored into the other model — see {@link eventBasedDustWallet}. Keeping them in separate files makes a model switch
|
|
110
|
+
* degrade to a from-scratch build rather than to a wrong resume position, with no cache-clearing step to remember.
|
|
111
|
+
*
|
|
112
|
+
* The shielded and unshielded snapshots are model-independent and are not namespaced.
|
|
113
|
+
*/
|
|
114
|
+
export const dustSnapshotPath = (syncCacheDir, filename, model) => `${syncCacheDir}/dust-${model}-${filename}`;
|
|
115
|
+
/**
|
|
116
|
+
* The projections dust sync, synchronizing in the background like the event-based sync does.
|
|
117
|
+
*
|
|
118
|
+
* This is the plain drop-in: spread it into the wallet options and the usual state waiters work unchanged, because
|
|
119
|
+
* background synchronization re-runs the projections passes on an interval.
|
|
120
|
+
*
|
|
121
|
+
* Use {@link manualProjectionsDustSyncOptions} instead when a test needs to decide when each pass happens.
|
|
122
|
+
*/
|
|
123
|
+
export const projectionsDustSyncOptions = {
|
|
124
|
+
dustWallet: eventLessDustWallet,
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The projections dust sync with background synchronization switched off, so the caller decides when each pass runs.
|
|
128
|
+
*
|
|
129
|
+
* Use this when a test needs passes to happen at known points — asserting on the state a specific pass produced, for
|
|
130
|
+
* instance. A caller that spreads this in must drive `facade.doSync(seeds)` itself, after start and again after
|
|
131
|
+
* anything that changes dust state; waiting on `waitForSyncedState()` alone will block, because with `manualSync`
|
|
132
|
+
* nothing advances the dust wallet until `doSync` runs.
|
|
133
|
+
*
|
|
134
|
+
* For a test that just wants the wallet to keep up on its own, spread {@link projectionsDustSyncOptions} instead and let
|
|
135
|
+
* background synchronization run the passes.
|
|
136
|
+
*/
|
|
137
|
+
export const manualProjectionsDustSyncOptions = {
|
|
138
|
+
dustWallet: eventLessDustWallet,
|
|
139
|
+
manualSync: true,
|
|
140
|
+
};
|
package/dist/logger.js
CHANGED
|
@@ -30,7 +30,7 @@ export const getLogger = () => current;
|
|
|
30
30
|
// whatever `setLogger` installed, without every helper having to call `getLogger()`.
|
|
31
31
|
export const logger = new Proxy({}, {
|
|
32
32
|
get(_target, property) {
|
|
33
|
-
const value = current
|
|
33
|
+
const value = Reflect.get(current, property);
|
|
34
34
|
return typeof value === 'function' ? value.bind(current) : value;
|
|
35
35
|
},
|
|
36
36
|
});
|
package/dist/scenarios/dust.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type WalletTestEnvironment } from '../types.js';
|
|
2
|
+
import { type ProvideWalletOptions } from '../wallet.js';
|
|
2
3
|
/** Dependencies the dust scenarios need from the consumer. */
|
|
3
4
|
export interface DustScenarioDeps {
|
|
4
5
|
/** Accessor for the active environment (typically the return of `useWalletTestEnvironment`). */
|
|
@@ -7,8 +8,19 @@ export interface DustScenarioDeps {
|
|
|
7
8
|
seed: string;
|
|
8
9
|
/** Optional dir to persist/restore wallet state across runs. (Was the `SYNC_CACHE` env var.) */
|
|
9
10
|
syncCacheDir?: string | undefined;
|
|
10
|
-
/** Per-test timeout in ms. Defaults to
|
|
11
|
+
/** Per-test timeout in ms. Defaults to 15 minutes; a longer bound only spends lane time on a wedged wallet. */
|
|
11
12
|
timeout?: number | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Selects the dust sync model. Defaults to the projections sync with background synchronization, so the network being
|
|
15
|
+
* monitored is exercised against the sync model the wallet ships to users.
|
|
16
|
+
*
|
|
17
|
+
* Left unset, the model still follows `DUST_SYNC` when that is configured, so a whole lane can be switched by
|
|
18
|
+
* environment; projections is only the fallback for when nothing is. Pass `{ dustWallet: eventBasedDustWallet }` to
|
|
19
|
+
* pin the event-stream sync regardless of the environment — dust snapshots are namespaced per sync model, so there is
|
|
20
|
+
* no cache to clear when switching. Avoid adding `manualSync` here: these scenarios wait on the state stream rather
|
|
21
|
+
* than driving passes themselves, so they need background synchronization to be running.
|
|
22
|
+
*/
|
|
23
|
+
walletOptions?: Pick<ProvideWalletOptions, 'dustWallet' | 'manualSync'> | undefined;
|
|
12
24
|
}
|
|
13
25
|
/** Registers the dust register/deregister healthchecks under a `describe('Dust tests')` block. */
|
|
14
|
-
export declare function registerDustHealthchecks({ getEnv, seed, syncCacheDir, timeout }: DustScenarioDeps): void;
|
|
26
|
+
export declare function registerDustHealthchecks({ getEnv, seed, syncCacheDir, timeout, walletOptions, }: DustScenarioDeps): void;
|
package/dist/scenarios/dust.js
CHANGED
|
@@ -19,10 +19,10 @@ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
|
19
19
|
import { inspect } from 'node:util';
|
|
20
20
|
import * as rx from 'rxjs';
|
|
21
21
|
import * as ledger from '@midnightntwrk/ledger-v9';
|
|
22
|
-
import { provideWallet, saveState } from '../wallet.js';
|
|
22
|
+
import { provideWallet, saveState, shouldPersistState, withProjectionsDefault, } from '../wallet.js';
|
|
23
23
|
import { logger } from '../logger.js';
|
|
24
24
|
/** Registers the dust register/deregister healthchecks under a `describe('Dust tests')` block. */
|
|
25
|
-
export function registerDustHealthchecks({ getEnv, seed, syncCacheDir, timeout =
|
|
25
|
+
export function registerDustHealthchecks({ getEnv, seed, syncCacheDir, timeout = 900_000, walletOptions, }) {
|
|
26
26
|
describe('Dust tests', () => {
|
|
27
27
|
const unshieldedTokenRaw = ledger.unshieldedToken().raw;
|
|
28
28
|
let wallet;
|
|
@@ -30,11 +30,17 @@ export function registerDustHealthchecks({ getEnv, seed, syncCacheDir, timeout =
|
|
|
30
30
|
beforeEach(async () => {
|
|
31
31
|
const env = getEnv();
|
|
32
32
|
filenameWallet = `${seed.substring(0, 7)}-${env.network}.state`;
|
|
33
|
-
wallet = await provideWallet(env, {
|
|
33
|
+
wallet = await provideWallet(env, {
|
|
34
|
+
...withProjectionsDefault(walletOptions),
|
|
35
|
+
seed,
|
|
36
|
+
syncCacheDir,
|
|
37
|
+
filename: filenameWallet,
|
|
38
|
+
});
|
|
34
39
|
});
|
|
35
|
-
afterEach(async () => {
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
afterEach(async (context) => {
|
|
41
|
+
// Only a passing test leaves its wallets in a resumable position; see `shouldPersistState`.
|
|
42
|
+
if (syncCacheDir && shouldPersistState(context)) {
|
|
43
|
+
await saveState(wallet, syncCacheDir, filenameWallet);
|
|
38
44
|
}
|
|
39
45
|
await wallet.wallet.stop();
|
|
40
46
|
logger.info('Wallet stopped');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type NetworkId } from '@midnightntwrk/wallet-sdk-abstractions';
|
|
2
2
|
import { type WalletTestEnvironment } from '../types.js';
|
|
3
|
-
import { type WalletInit } from '../wallet.js';
|
|
3
|
+
import { type ProvideWalletOptions, type WalletInit } from '../wallet.js';
|
|
4
4
|
/** Dependencies the token-transfer setup + scenarios need from the consumer. */
|
|
5
5
|
export interface TokenTransferScenarioDeps {
|
|
6
6
|
/** Accessor for the active environment (typically the return of `useWalletTestEnvironment`). */
|
|
@@ -11,10 +11,24 @@ export interface TokenTransferScenarioDeps {
|
|
|
11
11
|
secondSeed: string;
|
|
12
12
|
/** Optional dir to persist/restore wallet state across runs. (Was the `SYNC_CACHE` env var.) */
|
|
13
13
|
syncCacheDir?: string | undefined;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Timeout for the sync-heavy `beforeEach` and the healthcheck test in ms. Defaults to 15 minutes — a longer bound
|
|
16
|
+
* does not rescue a wedged wallet, it only spends lane time before reporting.
|
|
17
|
+
*/
|
|
15
18
|
syncTimeout?: number | undefined;
|
|
16
19
|
/** Timeout for the `afterEach` teardown in ms. Defaults to 10 minutes. */
|
|
17
20
|
timeout?: number | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Selects the dust sync model for both wallets. Defaults to the projections sync with background synchronization, so
|
|
23
|
+
* these transfers' fees are paid out of a projections-synced dust wallet.
|
|
24
|
+
*
|
|
25
|
+
* Left unset, the model still follows `DUST_SYNC` when that is configured, so a whole lane can be switched by
|
|
26
|
+
* environment; projections is only the fallback for when nothing is. Pass `{ dustWallet: eventBasedDustWallet }` to
|
|
27
|
+
* pin the event-stream sync regardless of the environment — dust snapshots are namespaced per sync model, so there is
|
|
28
|
+
* no cache to clear when switching. Avoid adding `manualSync` here: these scenarios wait on the state stream rather
|
|
29
|
+
* than driving passes themselves, so they need background synchronization to be running.
|
|
30
|
+
*/
|
|
31
|
+
walletOptions?: Pick<ProvideWalletOptions, 'dustWallet' | 'manualSync'> | undefined;
|
|
18
32
|
}
|
|
19
33
|
/** Accessors returned by {@link useTokenTransferWallets}, valid inside `test`/`it` bodies. */
|
|
20
34
|
export interface TokenTransferWallets {
|
|
@@ -29,7 +43,7 @@ export interface TokenTransferWallets {
|
|
|
29
43
|
* Call once inside a `describe`. Both the healthcheck scenario below and the upstream-only token-transfer tests reuse
|
|
30
44
|
* this so the sender/receiver selection lives in one place.
|
|
31
45
|
*/
|
|
32
|
-
export declare function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCacheDir, syncTimeout, timeout, }: TokenTransferScenarioDeps): TokenTransferWallets;
|
|
46
|
+
export declare function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCacheDir, syncTimeout, timeout, walletOptions, }: TokenTransferScenarioDeps): TokenTransferWallets;
|
|
33
47
|
/**
|
|
34
48
|
* Registers the single `@healthcheck`-tagged token-transfer test under a `describe('Token transfer')` block. This is
|
|
35
49
|
* the only token-transfer test shipped in the testkit; downstream monitoring selects it with `vitest run -t
|
|
@@ -21,10 +21,10 @@ import { firstValueFrom } from 'rxjs';
|
|
|
21
21
|
import { inspect } from 'node:util';
|
|
22
22
|
import * as ledger from '@midnightntwrk/ledger-v9';
|
|
23
23
|
import { ProtocolVersion, WalletTransaction } from '@midnightntwrk/wallet-sdk-abstractions';
|
|
24
|
-
import { provideWallet, saveState } from '../wallet.js';
|
|
24
|
+
import { provideWallet, saveState, shouldPersistState, withProjectionsDefault, } from '../wallet.js';
|
|
25
25
|
import { tNightAmount } from '../primitives.js';
|
|
26
26
|
import { getShieldedAddress, getUnshieldedAddress } from '../addresses.js';
|
|
27
|
-
import { waitForTxInHistory } from '../state-waiters.js';
|
|
27
|
+
import { waitForFacadePendingClear, waitForTxInHistory } from '../state-waiters.js';
|
|
28
28
|
import { expectReceiverShieldedTxHistory, expectReceiverUnshieldedTxHistory, expectSenderShieldedTxHistory, expectSenderUnshieldedTxHistory, } from '../tx-history-asserts.js';
|
|
29
29
|
import { logger } from '../logger.js';
|
|
30
30
|
/**
|
|
@@ -32,7 +32,7 @@ import { logger } from '../logger.js';
|
|
|
32
32
|
* Call once inside a `describe`. Both the healthcheck scenario below and the upstream-only token-transfer tests reuse
|
|
33
33
|
* this so the sender/receiver selection lives in one place.
|
|
34
34
|
*/
|
|
35
|
-
export function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCacheDir, syncTimeout =
|
|
35
|
+
export function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCacheDir, syncTimeout = 15 * 60 * 1000, timeout = 600_000, walletOptions, }) {
|
|
36
36
|
const shieldedTokenRaw = ledger.shieldedToken().raw;
|
|
37
37
|
let sender;
|
|
38
38
|
let receiver;
|
|
@@ -46,8 +46,18 @@ export function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCa
|
|
|
46
46
|
networkId = env.endpoints.networkId;
|
|
47
47
|
filenameWallet = `${fundedSeed.substring(0, 7)}-${env.network}.state`;
|
|
48
48
|
filenameWallet2 = `${secondSeed.substring(0, 7)}-${env.network}.state`;
|
|
49
|
-
wallet = await provideWallet(env, {
|
|
50
|
-
|
|
49
|
+
wallet = await provideWallet(env, {
|
|
50
|
+
...withProjectionsDefault(walletOptions),
|
|
51
|
+
seed: fundedSeed,
|
|
52
|
+
syncCacheDir,
|
|
53
|
+
filename: filenameWallet,
|
|
54
|
+
});
|
|
55
|
+
wallet2 = await provideWallet(env, {
|
|
56
|
+
...withProjectionsDefault(walletOptions),
|
|
57
|
+
seed: secondSeed,
|
|
58
|
+
syncCacheDir,
|
|
59
|
+
filename: filenameWallet2,
|
|
60
|
+
});
|
|
51
61
|
logger.info('Two wallets started');
|
|
52
62
|
const [state1, state2] = await Promise.all([
|
|
53
63
|
wallet.wallet.waitForSyncedState(),
|
|
@@ -67,10 +77,11 @@ export function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCa
|
|
|
67
77
|
receiver = wallet;
|
|
68
78
|
}
|
|
69
79
|
}, syncTimeout);
|
|
70
|
-
afterEach(async () => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await saveState(
|
|
80
|
+
afterEach(async (context) => {
|
|
81
|
+
// Only a passing test leaves its wallets in a resumable position; see `shouldPersistState`.
|
|
82
|
+
if (syncCacheDir && shouldPersistState(context)) {
|
|
83
|
+
await saveState(wallet, syncCacheDir, filenameWallet);
|
|
84
|
+
await saveState(wallet2, syncCacheDir, filenameWallet2);
|
|
74
85
|
}
|
|
75
86
|
await sender.wallet.stop();
|
|
76
87
|
await receiver.wallet.stop();
|
|
@@ -99,7 +110,7 @@ export function registerTokenTransferHealthchecks(deps) {
|
|
|
99
110
|
const shieldedTokenRaw = ledger.shieldedToken().raw;
|
|
100
111
|
const unshieldedTokenRaw = ledger.unshieldedToken().raw;
|
|
101
112
|
const outputValue = tNightAmount(10n);
|
|
102
|
-
const syncTimeout = deps.syncTimeout ??
|
|
113
|
+
const syncTimeout = deps.syncTimeout ?? 15 * 60 * 1000;
|
|
103
114
|
test('Is working for valid transfer @healthcheck', async () => {
|
|
104
115
|
const sender = getSender();
|
|
105
116
|
const receiver = getReceiver();
|
|
@@ -169,6 +180,7 @@ export function registerTokenTransferHealthchecks(deps) {
|
|
|
169
180
|
logger.info('Transaction id: ' + txId);
|
|
170
181
|
logger.info('waiting for tx in history');
|
|
171
182
|
const senderTxEntry = await waitForTxInHistory(txHash, sender.wallet, (e) => e.shielded !== undefined && e.unshielded !== undefined);
|
|
183
|
+
await waitForFacadePendingClear(sender.wallet);
|
|
172
184
|
const senderFinalState = await sender.wallet.waitForSyncedState();
|
|
173
185
|
const senderFinalShieldedBalance = senderFinalState.shielded.balances[shieldedTokenRaw];
|
|
174
186
|
const senderFinalUnshieldedBalance = senderFinalState.unshielded.balances[unshieldedTokenRaw];
|
|
@@ -193,6 +205,7 @@ export function registerTokenTransferHealthchecks(deps) {
|
|
|
193
205
|
expect(senderFinalTxHistory.length).toBeGreaterThanOrEqual(senderInitialTxHistory.length + 1);
|
|
194
206
|
expectSenderShieldedTxHistory(senderTxEntry);
|
|
195
207
|
expectSenderUnshieldedTxHistory(senderTxEntry);
|
|
208
|
+
await waitForFacadePendingClear(receiver.wallet);
|
|
196
209
|
const receiverFinalState = await receiver.wallet.waitForSyncedState();
|
|
197
210
|
const receiverFinalShieldedBalance = receiverFinalState.shielded.balances[shieldedTokenRaw] ?? 0n;
|
|
198
211
|
const receiverFinalUnshieldedBalance = receiverFinalState.unshielded.balances[unshieldedTokenRaw] ?? 0n;
|
package/dist/state-waiters.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
+
import * as rx from 'rxjs';
|
|
1
2
|
import type * as ledger from '@midnightntwrk/ledger-v9';
|
|
2
3
|
import { type ShieldedWalletAPI } from '@midnightntwrk/wallet-sdk-shielded';
|
|
3
4
|
import { type UnshieldedWallet } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
|
|
4
5
|
import { type WalletFacade, type WalletEntry } from '@midnightntwrk/wallet-sdk-facade';
|
|
5
6
|
/** Default dust-balance threshold to wait for: 7 * 10^14. */
|
|
6
7
|
export declare const DEFAULT_DUST_BALANCE_THRESHOLD: bigint;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves with the wallet's current state once `predicate` has held continuously for `settleMs`.
|
|
10
|
+
*
|
|
11
|
+
* A settle window is needed because conditions like `pendingCoins.length === 0` are also the _resting_ state before a
|
|
12
|
+
* transaction is submitted: a waiter that resolved on the first matching emission would hand back the stale
|
|
13
|
+
* pre-transaction state.
|
|
14
|
+
*
|
|
15
|
+
* The window has to be applied to the predicate rather than to the source. Debouncing the source waits for the wallet
|
|
16
|
+
* to stop emitting altogether, which never happens while a sub-wallet syncs in the background — a Dust wallet syncing
|
|
17
|
+
* from projections emits about every 5 seconds indefinitely — so the predicate is never evaluated and the wait runs to
|
|
18
|
+
* the test timeout while logging that its condition is already satisfied.
|
|
19
|
+
*
|
|
20
|
+
* `distinctUntilChanged` is load-bearing: without it, `switchMap` would cancel and restart the timer on every emission
|
|
21
|
+
* and the timer would never elapse, reproducing the same starvation.
|
|
22
|
+
*
|
|
23
|
+
* The value is carried through the pipeline rather than re-read from `source` once the window elapses, because `source`
|
|
24
|
+
* must only be subscribed once. The facade's `state()` replays its current value on subscribe, but a sub-wallet's
|
|
25
|
+
* `state` does not: a second subscription there receives nothing until the next change, so on an already-settled wallet
|
|
26
|
+
* it would never produce a value and the wait would hang. `shareReplay` is what reconciles that with returning the
|
|
27
|
+
* _latest_ matching value rather than the one from the moment the condition began to hold: it multicasts a single
|
|
28
|
+
* upstream subscription, so the window and the value read at the end of it come from the same one. Returning the
|
|
29
|
+
* opening value would defeat the point of the window — `pendingCoins.length === 0` is equally true either side of a
|
|
30
|
+
* balance arriving, so the waiter would hand back the stale pre-transaction state it exists to avoid.
|
|
31
|
+
*/
|
|
32
|
+
export declare const waitForStableState: <T>(source: rx.Observable<T>, predicate: (value: T) => boolean, description: string, settleMs?: number, timeoutMs?: number) => Promise<T>;
|
|
7
33
|
export declare const waitForSyncUnshielded: (wallet: UnshieldedWallet) => Promise<import("@midnightntwrk/wallet-sdk-unshielded-wallet").UnshieldedWalletState<string>>;
|
|
8
34
|
export declare const waitForFacadePending: (wallet: WalletFacade) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
9
35
|
export declare const waitForFacadePendingClear: (wallet: WalletFacade) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
package/dist/state-waiters.js
CHANGED
|
@@ -16,6 +16,45 @@ import { expect } from 'vitest';
|
|
|
16
16
|
import { logger } from './logger.js';
|
|
17
17
|
/** Default dust-balance threshold to wait for: 7 * 10^14. */
|
|
18
18
|
export const DEFAULT_DUST_BALANCE_THRESHOLD = 7n * 10n ** 14n;
|
|
19
|
+
/** How long a condition must hold before a settling waiter accepts it. */
|
|
20
|
+
const DEFAULT_SETTLE_MS = 10_000;
|
|
21
|
+
/** How long a settling waiter waits for its condition before failing rather than running to the test timeout. */
|
|
22
|
+
const DEFAULT_SETTLE_TIMEOUT_MS = 180_000;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves with the wallet's current state once `predicate` has held continuously for `settleMs`.
|
|
25
|
+
*
|
|
26
|
+
* A settle window is needed because conditions like `pendingCoins.length === 0` are also the _resting_ state before a
|
|
27
|
+
* transaction is submitted: a waiter that resolved on the first matching emission would hand back the stale
|
|
28
|
+
* pre-transaction state.
|
|
29
|
+
*
|
|
30
|
+
* The window has to be applied to the predicate rather than to the source. Debouncing the source waits for the wallet
|
|
31
|
+
* to stop emitting altogether, which never happens while a sub-wallet syncs in the background — a Dust wallet syncing
|
|
32
|
+
* from projections emits about every 5 seconds indefinitely — so the predicate is never evaluated and the wait runs to
|
|
33
|
+
* the test timeout while logging that its condition is already satisfied.
|
|
34
|
+
*
|
|
35
|
+
* `distinctUntilChanged` is load-bearing: without it, `switchMap` would cancel and restart the timer on every emission
|
|
36
|
+
* and the timer would never elapse, reproducing the same starvation.
|
|
37
|
+
*
|
|
38
|
+
* The value is carried through the pipeline rather than re-read from `source` once the window elapses, because `source`
|
|
39
|
+
* must only be subscribed once. The facade's `state()` replays its current value on subscribe, but a sub-wallet's
|
|
40
|
+
* `state` does not: a second subscription there receives nothing until the next change, so on an already-settled wallet
|
|
41
|
+
* it would never produce a value and the wait would hang. `shareReplay` is what reconciles that with returning the
|
|
42
|
+
* _latest_ matching value rather than the one from the moment the condition began to hold: it multicasts a single
|
|
43
|
+
* upstream subscription, so the window and the value read at the end of it come from the same one. Returning the
|
|
44
|
+
* opening value would defeat the point of the window — `pendingCoins.length === 0` is equally true either side of a
|
|
45
|
+
* balance arriving, so the waiter would hand back the stale pre-transaction state it exists to avoid.
|
|
46
|
+
*/
|
|
47
|
+
export const waitForStableState = (source, predicate, description, settleMs = DEFAULT_SETTLE_MS, timeoutMs = DEFAULT_SETTLE_TIMEOUT_MS) => {
|
|
48
|
+
const observed = source.pipe(rx.map((value) => ({ value, holds: predicate(value) })), rx.shareReplay({ bufferSize: 1, refCount: true }));
|
|
49
|
+
return rx.firstValueFrom(observed.pipe(rx.distinctUntilChanged((a, b) => a.holds === b.holds), rx.switchMap(({ holds }) => (holds ? rx.timer(settleMs) : rx.NEVER)),
|
|
50
|
+
// The window has elapsed, so take the newest value rather than the one that opened it. That value still satisfies
|
|
51
|
+
// the predicate: a value that did not would have been a `holds` transition, and `switchMap` would have cancelled
|
|
52
|
+
// this timer before it could fire.
|
|
53
|
+
rx.withLatestFrom(observed), rx.map(([, latest]) => latest.value), rx.take(1), rx.timeout({
|
|
54
|
+
first: timeoutMs,
|
|
55
|
+
with: () => rx.throwError(() => new Error(`${description}: condition never held continuously for ${settleMs}ms within ${timeoutMs}ms. It may have been met repeatedly and simply never stayed met for that long — a wallet still syncing keeps breaking the window.`)),
|
|
56
|
+
})));
|
|
57
|
+
};
|
|
19
58
|
export const waitForSyncUnshielded = (wallet) => rx.firstValueFrom(wallet.state.pipe(rx.throttleTime(5_000), rx.tap((state) => {
|
|
20
59
|
const applyGap = state.state.progress.highestTransactionId - state.state.progress.appliedId;
|
|
21
60
|
logger.info(`Wallet behind by ${applyGap} indices`);
|
|
@@ -32,32 +71,21 @@ rx.timeout({
|
|
|
32
71
|
first: 120_000,
|
|
33
72
|
with: () => rx.throwError(() => new Error('waitForFacadePending: no pending coins observed within 120s — the pending window was likely missed (tx confirmed before it was observed) or the transaction was never submitted.')),
|
|
34
73
|
})));
|
|
35
|
-
export const waitForFacadePendingClear = (wallet) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const dustPending = state.dust.pendingCoins.length;
|
|
41
|
-
logger.info(`Dust wallet pending coins: ${dustPending}, waiting for pending coins to clear...`);
|
|
42
|
-
}), rx.debounceTime(10_000), rx.filter((state) => state.shielded.pendingCoins.length == 0 &&
|
|
43
|
-
state.unshielded.pendingCoins.length == 0 &&
|
|
44
|
-
state.dust.pendingCoins.length == 0)));
|
|
74
|
+
export const waitForFacadePendingClear = (wallet) => waitForStableState(wallet.state().pipe(rx.tap((state) => {
|
|
75
|
+
logger.info(`Pending coins — shielded: ${state.shielded.pendingCoins.length}, unshielded: ${state.unshielded.pendingCoins.length}, dust: ${state.dust.pendingCoins.length}; waiting for all to clear...`);
|
|
76
|
+
})), (state) => state.shielded.pendingCoins.length === 0 &&
|
|
77
|
+
state.unshielded.pendingCoins.length === 0 &&
|
|
78
|
+
state.dust.pendingCoins.length === 0, 'waitForFacadePendingClear');
|
|
45
79
|
export const waitForDustBalance = (wallet, threshold = DEFAULT_DUST_BALANCE_THRESHOLD) => rx.firstValueFrom(wallet.state().pipe(rx.tap((state) => {
|
|
46
80
|
const dustBalance = state.dust.balance(new Date());
|
|
47
81
|
logger.info(`Dust balance: ${dustBalance}, waiting for dust balance > ${threshold}...`);
|
|
48
82
|
}), rx.filter((state) => state.dust.balance(new Date()) > threshold)));
|
|
49
|
-
export const waitForFinalizedShieldedBalance = (wallet) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// state rather than the stale one.
|
|
56
|
-
rx.debounceTime(10_000), rx.filter((state) => state.pendingCoins.length === 0)));
|
|
57
|
-
export const waitForUnshieldedCoinUpdate = (wallet, initialNumAvailableCoins) => rx.firstValueFrom(wallet.state().pipe(rx.tap((state) => {
|
|
58
|
-
const currentNumAvailableCoins = state.unshielded.availableCoins.length;
|
|
59
|
-
logger.info(`Unshielded available coins: ${currentNumAvailableCoins}, waiting for more than ${initialNumAvailableCoins}...synced = ${state.isSynced}`);
|
|
60
|
-
}), rx.debounceTime(10_000), rx.filter((s) => s.isSynced), rx.filter((s) => s.unshielded.availableCoins.length > initialNumAvailableCoins)));
|
|
83
|
+
export const waitForFinalizedShieldedBalance = (wallet) => waitForStableState(wallet.state.pipe(rx.tap((state) => {
|
|
84
|
+
logger.info(`Wallet pending coins: ${state.pendingCoins.length}, waiting for pending coins cleared...`);
|
|
85
|
+
})), (state) => state.pendingCoins.length === 0, 'waitForFinalizedShieldedBalance');
|
|
86
|
+
export const waitForUnshieldedCoinUpdate = (wallet, initialNumAvailableCoins) => waitForStableState(wallet.state().pipe(rx.tap((state) => {
|
|
87
|
+
logger.info(`Unshielded available coins: ${state.unshielded.availableCoins.length}, waiting for more than ${initialNumAvailableCoins}...synced = ${state.isSynced}`);
|
|
88
|
+
})), (state) => state.isSynced && state.unshielded.availableCoins.length > initialNumAvailableCoins, 'waitForUnshieldedCoinUpdate');
|
|
61
89
|
export const waitForStateAfterDustRegistration = (wallet, finalizedTx) => rx.firstValueFrom(wallet.state().pipe(rx.mergeMap(async (state) => {
|
|
62
90
|
const txInHistory = await wallet.queryTxHistoryByHash(finalizedTx.transactionHash());
|
|
63
91
|
return {
|
package/dist/wallet.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as ledger from '@midnightntwrk/ledger-v9';
|
|
|
3
3
|
import { WalletFacade } from '@midnightntwrk/wallet-sdk-facade';
|
|
4
4
|
import { type UnshieldedKeystore } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
|
|
5
5
|
import { type WalletTestEnvironment } from './types.js';
|
|
6
|
+
import { type DustSnapshotModel, type DustWalletFactory } from './dust-sync.js';
|
|
6
7
|
export type WalletInit = {
|
|
7
8
|
wallet: WalletFacade;
|
|
8
9
|
shieldedSecretKeys: ledger.ZswapSecretKeys;
|
|
@@ -10,6 +11,12 @@ export type WalletInit = {
|
|
|
10
11
|
/** The three per-wallet seeds, which is what the facade is started and stepped with. */
|
|
11
12
|
seeds: WalletSeeds;
|
|
12
13
|
unshieldedKeystore: UnshieldedKeystore;
|
|
14
|
+
/**
|
|
15
|
+
* The dust sync model this wallet was actually built with, whether it came from an explicit option or from the
|
|
16
|
+
* environment. {@link saveState} reads it so a snapshot is always written to the namespace of the model that produced
|
|
17
|
+
* it.
|
|
18
|
+
*/
|
|
19
|
+
dustSyncModel: DustSnapshotModel;
|
|
13
20
|
};
|
|
14
21
|
/** Options for {@link provideWallet}. */
|
|
15
22
|
export interface ProvideWalletOptions {
|
|
@@ -23,7 +30,36 @@ export interface ProvideWalletOptions {
|
|
|
23
30
|
syncCacheDir?: string | undefined;
|
|
24
31
|
/** Filename suffix for the three serialized state files. Required when `syncCacheDir` is set. */
|
|
25
32
|
filename?: string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Replaces the dust sub-wallet factory. Defaults to whatever `DUST_SYNC` selects (the event-based `DustWallet` when
|
|
35
|
+
* unset) — see `dustWalletFromEnv` — so a lane can be switched between sync models by configuration while a test that
|
|
36
|
+
* needs a specific model pins it here. Applies to both the restored and the built-from-scratch paths, so a cold cache
|
|
37
|
+
* cannot silently fall back to a different sync model.
|
|
38
|
+
*/
|
|
39
|
+
dustWallet?: DustWalletFactory | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Starts the facade without background syncing, leaving the caller to drive `facade.doSync()`. Defaults to `false`
|
|
42
|
+
* (background sync), which is what every existing scenario relies on.
|
|
43
|
+
*/
|
|
44
|
+
manualSync?: boolean | undefined;
|
|
26
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Scenario wallet options with the projections default filled in **per field** rather than wholesale.
|
|
48
|
+
*
|
|
49
|
+
* A whole-parameter default is lost the moment a caller passes any `walletOptions` at all: `{ manualSync: true }`
|
|
50
|
+
* leaves `dustWallet` undefined, {@link provideWallet} falls back to the `events` model, and a scenario silently stops
|
|
51
|
+
* monitoring the sync model it exists to monitor. Filling the field in only when the caller supplied one keeps
|
|
52
|
+
* `DUST_SYNC` in charge of the model and the caller's own choices intact.
|
|
53
|
+
*
|
|
54
|
+
* An explicit `dustWallet: undefined` counts as not supplying one, so it is filled in too. The field is declared `|
|
|
55
|
+
* undefined` precisely so an optional factory can be forwarded under `exactOptionalPropertyTypes`, which means a caller
|
|
56
|
+
* forwarding an absent one type-checks; taking that as a choice of the `events` model would drop the scenario onto the
|
|
57
|
+
* sync it exists to stop monitoring, without the caller having named a model at all.
|
|
58
|
+
*
|
|
59
|
+
* @param walletOptions What the caller passed, if anything.
|
|
60
|
+
* @returns The options to hand {@link provideWallet}.
|
|
61
|
+
*/
|
|
62
|
+
export declare const withProjectionsDefault: (walletOptions: Pick<ProvideWalletOptions, "dustWallet" | "manualSync"> | undefined) => Pick<ProvideWalletOptions, "dustWallet" | "manualSync">;
|
|
27
63
|
/**
|
|
28
64
|
* Builds a fully-started {@link WalletFacade} (shielded + unshielded + dust) for the given seed.
|
|
29
65
|
*
|
|
@@ -31,7 +67,31 @@ export interface ProvideWalletOptions {
|
|
|
31
67
|
* otherwise (or on any restore failure) builds from scratch via {@link initWalletWithSeed}.
|
|
32
68
|
*/
|
|
33
69
|
export declare const provideWallet: (env: WalletTestEnvironment, options: ProvideWalletOptions) => Promise<WalletInit>;
|
|
34
|
-
/**
|
|
35
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Whether the test that has just finished is safe to persist wallet state from.
|
|
72
|
+
*
|
|
73
|
+
* State written by a failed or timed-out test is worse than no state at all: the next run restores it, resumes from
|
|
74
|
+
* whatever partial position the failure left behind, and fails in turn — so one transient failure becomes permanent and
|
|
75
|
+
* every later run inherits it. Only a test that passed is known to have left its wallets in a position the chain can be
|
|
76
|
+
* resumed from.
|
|
77
|
+
*
|
|
78
|
+
* Pass the `afterEach` context: `afterEach((ctx) => { if (shouldPersistState(ctx)) await saveState(...) })`.
|
|
79
|
+
*/
|
|
80
|
+
export declare const shouldPersistState: (context: {
|
|
81
|
+
readonly task: {
|
|
82
|
+
readonly result?: {
|
|
83
|
+
readonly state?: string;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
}) => boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Serializes all three sub-wallet states into `syncCacheDir`, keyed by `filename`.
|
|
89
|
+
*
|
|
90
|
+
* Takes the whole {@link WalletInit} rather than the facade alone so the dust snapshot is always written to the
|
|
91
|
+
* namespace of the sync model that produced it — a model and a facade passed separately could disagree.
|
|
92
|
+
*/
|
|
93
|
+
export declare const saveState: (walletInit: Pick<WalletInit, "wallet" | "dustSyncModel">, syncCacheDir: string, filename: string) => Promise<void>;
|
|
94
|
+
/** Options for {@link initWalletWithSeed}, a subset of {@link ProvideWalletOptions}. */
|
|
95
|
+
export type InitWalletOptions = Pick<ProvideWalletOptions, 'dustWallet' | 'manualSync'>;
|
|
36
96
|
/** Builds and starts a fresh {@link WalletFacade} from `seed`, with no disk persistence. */
|
|
37
|
-
export declare const initWalletWithSeed: (env: WalletTestEnvironment, seed: string) => Promise<WalletInit>;
|
|
97
|
+
export declare const initWalletWithSeed: (env: WalletTestEnvironment, seed: string, options?: InitWalletOptions) => Promise<WalletInit>;
|
package/dist/wallet.js
CHANGED
|
@@ -18,9 +18,9 @@ import { InMemoryTransactionHistoryStorage, } from '@midnightntwrk/wallet-sdk-ab
|
|
|
18
18
|
import { ShieldedWallet } from '@midnightntwrk/wallet-sdk-shielded';
|
|
19
19
|
import { WalletFacade, WalletEntrySchema, mergeWalletEntries } from '@midnightntwrk/wallet-sdk-facade';
|
|
20
20
|
import { createKeystore, PublicKey, UnshieldedWallet, } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
|
|
21
|
-
import { DustWallet } from '@midnightntwrk/wallet-sdk-dust-wallet';
|
|
22
21
|
import { logger } from './logger.js';
|
|
23
22
|
import { getDustSeed, getShieldedSeed, getUnshieldedSeed } from './seeds.js';
|
|
23
|
+
import { dustSnapshotPath, dustSyncModelOf, dustWalletFromEnv, } from './dust-sync.js';
|
|
24
24
|
const waitForSyncProgress = async (wallet) => await rx.firstValueFrom(wallet.state().pipe(rx.throttleTime(5000), rx.tap((state) => {
|
|
25
25
|
const applyGap = state.unshielded.progress.highestTransactionId - state.unshielded.progress.appliedId;
|
|
26
26
|
logger.info(`Wallet facade behind by ${applyGap}`);
|
|
@@ -65,11 +65,11 @@ const restoreUnshieldedWallet = async (path, seed, env, readIfExists, txHistoryS
|
|
|
65
65
|
}
|
|
66
66
|
return undefined;
|
|
67
67
|
};
|
|
68
|
-
const restoreDustWallet = async (path, walletConfig, readIfExists) => {
|
|
68
|
+
const restoreDustWallet = async (path, walletConfig, readIfExists, dustWallet) => {
|
|
69
69
|
try {
|
|
70
70
|
const serialized = await readIfExists(path);
|
|
71
71
|
if (serialized) {
|
|
72
|
-
const DustInstance =
|
|
72
|
+
const DustInstance = dustWallet({
|
|
73
73
|
...walletConfig,
|
|
74
74
|
costParameters: walletConfig?.costParameters ?? {
|
|
75
75
|
feeBlocksMargin: 5,
|
|
@@ -86,6 +86,26 @@ const restoreDustWallet = async (path, walletConfig, readIfExists) => {
|
|
|
86
86
|
}
|
|
87
87
|
return undefined;
|
|
88
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Scenario wallet options with the projections default filled in **per field** rather than wholesale.
|
|
91
|
+
*
|
|
92
|
+
* A whole-parameter default is lost the moment a caller passes any `walletOptions` at all: `{ manualSync: true }`
|
|
93
|
+
* leaves `dustWallet` undefined, {@link provideWallet} falls back to the `events` model, and a scenario silently stops
|
|
94
|
+
* monitoring the sync model it exists to monitor. Filling the field in only when the caller supplied one keeps
|
|
95
|
+
* `DUST_SYNC` in charge of the model and the caller's own choices intact.
|
|
96
|
+
*
|
|
97
|
+
* An explicit `dustWallet: undefined` counts as not supplying one, so it is filled in too. The field is declared `|
|
|
98
|
+
* undefined` precisely so an optional factory can be forwarded under `exactOptionalPropertyTypes`, which means a caller
|
|
99
|
+
* forwarding an absent one type-checks; taking that as a choice of the `events` model would drop the scenario onto the
|
|
100
|
+
* sync it exists to stop monitoring, without the caller having named a model at all.
|
|
101
|
+
*
|
|
102
|
+
* @param walletOptions What the caller passed, if anything.
|
|
103
|
+
* @returns The options to hand {@link provideWallet}.
|
|
104
|
+
*/
|
|
105
|
+
export const withProjectionsDefault = (walletOptions) => ({
|
|
106
|
+
...walletOptions,
|
|
107
|
+
dustWallet: walletOptions?.dustWallet ?? dustWalletFromEnv(process.env, 'projections'),
|
|
108
|
+
});
|
|
89
109
|
/**
|
|
90
110
|
* Builds a fully-started {@link WalletFacade} (shielded + unshielded + dust) for the given seed.
|
|
91
111
|
*
|
|
@@ -93,10 +113,15 @@ const restoreDustWallet = async (path, walletConfig, readIfExists) => {
|
|
|
93
113
|
* otherwise (or on any restore failure) builds from scratch via {@link initWalletWithSeed}.
|
|
94
114
|
*/
|
|
95
115
|
export const provideWallet = async (env, options) => {
|
|
96
|
-
const { seed, syncCacheDir, filename } = options;
|
|
116
|
+
const { seed, syncCacheDir, filename, manualSync } = options;
|
|
117
|
+
// Resolved once, here, so the restore path, the snapshot namespace and every from-scratch fallback below all agree on
|
|
118
|
+
// which sync model was asked for.
|
|
119
|
+
const dustWallet = options.dustWallet ?? dustWalletFromEnv();
|
|
120
|
+
const dustSyncModel = dustSyncModelOf(dustWallet);
|
|
121
|
+
const fromScratch = { dustWallet, manualSync };
|
|
97
122
|
if (!syncCacheDir || !filename) {
|
|
98
123
|
logger.info('No sync cache configured; building wallet facade from scratch');
|
|
99
|
-
return initWalletWithSeed(env, seed);
|
|
124
|
+
return initWalletWithSeed(env, seed, fromScratch);
|
|
100
125
|
}
|
|
101
126
|
// Single shared tx-history storage so all three sub-wallets and the facade read/write
|
|
102
127
|
// the same instance; otherwise shielded/unshielded writes go to a storage the facade
|
|
@@ -127,11 +152,11 @@ export const provideWallet = async (env, options) => {
|
|
|
127
152
|
const [restoredShielded, restoredUnshielded, restoredDust] = await Promise.all([
|
|
128
153
|
restoreShieldedWallet(`${syncCacheDir}/shielded-${filename}`, Wallet, readIfExists),
|
|
129
154
|
restoreUnshieldedWallet(`${syncCacheDir}/unshielded-${filename}`, seed, env, readIfExists, txHistoryStorage),
|
|
130
|
-
restoreDustWallet(
|
|
155
|
+
restoreDustWallet(dustSnapshotPath(syncCacheDir, filename, dustSyncModel), { ...walletConfig, ...dustWalletConfig }, readIfExists, dustWallet),
|
|
131
156
|
]);
|
|
132
157
|
if (!restoredShielded || !restoredUnshielded || !restoredDust) {
|
|
133
158
|
logger.info('Building wallet facade from scratch');
|
|
134
|
-
return initWalletWithSeed(env, seed);
|
|
159
|
+
return initWalletWithSeed(env, seed, fromScratch);
|
|
135
160
|
}
|
|
136
161
|
else {
|
|
137
162
|
const restoredWallet = await WalletFacade.init({
|
|
@@ -143,7 +168,7 @@ export const provideWallet = async (env, options) => {
|
|
|
143
168
|
unshielded: () => restoredUnshielded,
|
|
144
169
|
dust: () => restoredDust,
|
|
145
170
|
});
|
|
146
|
-
await restoredWallet.start(seeds);
|
|
171
|
+
await restoredWallet.start(seeds, { manualSync: manualSync ?? false });
|
|
147
172
|
// check if wallet is syncing correctly
|
|
148
173
|
await waitForSyncProgress(restoredWallet);
|
|
149
174
|
const restoredWalletState = await rx.firstValueFrom(restoredWallet.state());
|
|
@@ -152,33 +177,49 @@ export const provideWallet = async (env, options) => {
|
|
|
152
177
|
if ((applyGap ?? 0) < 0) {
|
|
153
178
|
logger.warn('Unable to sync restored wallet. Building wallet facade from scratch');
|
|
154
179
|
await restoredWallet.stop();
|
|
155
|
-
return initWalletWithSeed(env, seed);
|
|
180
|
+
return initWalletWithSeed(env, seed, fromScratch);
|
|
156
181
|
}
|
|
157
182
|
else {
|
|
158
183
|
logger.info('Successfully restored wallet facade.');
|
|
159
|
-
return { wallet: restoredWallet, shieldedSecretKeys, dustSecretKey, seeds, unshieldedKeystore };
|
|
184
|
+
return { wallet: restoredWallet, shieldedSecretKeys, dustSecretKey, seeds, unshieldedKeystore, dustSyncModel };
|
|
160
185
|
}
|
|
161
186
|
}
|
|
162
187
|
};
|
|
163
|
-
/**
|
|
164
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Whether the test that has just finished is safe to persist wallet state from.
|
|
190
|
+
*
|
|
191
|
+
* State written by a failed or timed-out test is worse than no state at all: the next run restores it, resumes from
|
|
192
|
+
* whatever partial position the failure left behind, and fails in turn — so one transient failure becomes permanent and
|
|
193
|
+
* every later run inherits it. Only a test that passed is known to have left its wallets in a position the chain can be
|
|
194
|
+
* resumed from.
|
|
195
|
+
*
|
|
196
|
+
* Pass the `afterEach` context: `afterEach((ctx) => { if (shouldPersistState(ctx)) await saveState(...) })`.
|
|
197
|
+
*/
|
|
198
|
+
export const shouldPersistState = (context) => context.task.result?.state === 'pass';
|
|
199
|
+
/**
|
|
200
|
+
* Serializes all three sub-wallet states into `syncCacheDir`, keyed by `filename`.
|
|
201
|
+
*
|
|
202
|
+
* Takes the whole {@link WalletInit} rather than the facade alone so the dust snapshot is always written to the
|
|
203
|
+
* namespace of the sync model that produced it — a model and a facade passed separately could disagree.
|
|
204
|
+
*/
|
|
205
|
+
export const saveState = async (walletInit, syncCacheDir, filename) => {
|
|
165
206
|
logger.info(`Saving state in ${syncCacheDir}/${filename}`);
|
|
166
207
|
try {
|
|
167
208
|
await fsAsync.mkdir(syncCacheDir, { recursive: true });
|
|
168
209
|
// Serialize all three states
|
|
169
210
|
const [shieldedSerializedState, unshieldedSerializedState, dustSerializedState] = await Promise.all([
|
|
170
|
-
wallet.shielded.serializeState(),
|
|
171
|
-
wallet.unshielded.serializeState(),
|
|
172
|
-
wallet.dust.serializeState(),
|
|
211
|
+
walletInit.wallet.shielded.serializeState(),
|
|
212
|
+
walletInit.wallet.unshielded.serializeState(),
|
|
213
|
+
walletInit.wallet.dust.serializeState(),
|
|
173
214
|
]);
|
|
174
215
|
const files = [
|
|
175
|
-
{
|
|
176
|
-
{
|
|
177
|
-
{
|
|
216
|
+
{ path: `${syncCacheDir}/shielded-${filename}`, data: shieldedSerializedState },
|
|
217
|
+
{ path: `${syncCacheDir}/unshielded-${filename}`, data: unshieldedSerializedState },
|
|
218
|
+
{ path: dustSnapshotPath(syncCacheDir, filename, walletInit.dustSyncModel), data: dustSerializedState },
|
|
178
219
|
];
|
|
179
|
-
const results = await Promise.allSettled(files.map((f) => fsAsync.writeFile(
|
|
220
|
+
const results = await Promise.allSettled(files.map((f) => fsAsync.writeFile(f.path, f.data, 'utf-8')));
|
|
180
221
|
for (const [i, res] of results.entries()) {
|
|
181
|
-
const pathWritten =
|
|
222
|
+
const pathWritten = files[i].path;
|
|
182
223
|
if (res.status === 'fulfilled') {
|
|
183
224
|
logger.info(`State written to file ${pathWritten}`);
|
|
184
225
|
}
|
|
@@ -200,7 +241,8 @@ export const saveState = async (wallet, syncCacheDir, filename) => {
|
|
|
200
241
|
}
|
|
201
242
|
};
|
|
202
243
|
/** Builds and starts a fresh {@link WalletFacade} from `seed`, with no disk persistence. */
|
|
203
|
-
export const initWalletWithSeed = async (env, seed) => {
|
|
244
|
+
export const initWalletWithSeed = async (env, seed, options = {}) => {
|
|
245
|
+
const dustWalletClass = options.dustWallet ?? dustWalletFromEnv();
|
|
204
246
|
const walletConfig = env.getWalletConfig();
|
|
205
247
|
const seeds = {
|
|
206
248
|
shielded: getShieldedSeed(seed),
|
|
@@ -218,8 +260,15 @@ export const initWalletWithSeed = async (env, seed) => {
|
|
|
218
260
|
},
|
|
219
261
|
shielded: (config) => ShieldedWallet(config).startWithSeed(getShieldedSeed(seed)),
|
|
220
262
|
unshielded: (config) => UnshieldedWallet(config).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
|
|
221
|
-
dust: (config) =>
|
|
263
|
+
dust: (config) => dustWalletClass(config).startWithSeed(getDustSeed(seed)),
|
|
222
264
|
});
|
|
223
|
-
await facade.start(seeds);
|
|
224
|
-
return {
|
|
265
|
+
await facade.start(seeds, { manualSync: options.manualSync ?? false });
|
|
266
|
+
return {
|
|
267
|
+
wallet: facade,
|
|
268
|
+
shieldedSecretKeys,
|
|
269
|
+
dustSecretKey,
|
|
270
|
+
seeds,
|
|
271
|
+
unshieldedKeystore,
|
|
272
|
+
dustSyncModel: dustSyncModelOf(dustWalletClass),
|
|
273
|
+
};
|
|
225
274
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midnightntwrk/wallet-sdk-testkit",
|
|
3
3
|
"description": "Reusable wallet-SDK test harness: environment provisioning, wallet bootstrapping, sync waiters and tx-history assertions",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@midnightntwrk/ledger-v9": "1.0.0-rc.
|
|
43
|
-
"@midnightntwrk/wallet-sdk-abstractions": "3.0.0-beta.
|
|
44
|
-
"@midnightntwrk/wallet-sdk-address-format": "4.0.0-beta.
|
|
45
|
-
"@midnightntwrk/wallet-sdk-capabilities": "4.0.0-beta.
|
|
46
|
-
"@midnightntwrk/wallet-sdk-dust-wallet": "5.0.0-beta.
|
|
47
|
-
"@midnightntwrk/wallet-sdk-facade": "5.0.0-beta.
|
|
48
|
-
"@midnightntwrk/wallet-sdk-hd": "3.1.0-beta.
|
|
42
|
+
"@midnightntwrk/ledger-v9": "1.0.0-rc.5",
|
|
43
|
+
"@midnightntwrk/wallet-sdk-abstractions": "3.0.0-beta.2",
|
|
44
|
+
"@midnightntwrk/wallet-sdk-address-format": "4.0.0-beta.4",
|
|
45
|
+
"@midnightntwrk/wallet-sdk-capabilities": "4.0.0-beta.4",
|
|
46
|
+
"@midnightntwrk/wallet-sdk-dust-wallet": "5.0.0-beta.4",
|
|
47
|
+
"@midnightntwrk/wallet-sdk-facade": "5.0.0-beta.4",
|
|
48
|
+
"@midnightntwrk/wallet-sdk-hd": "3.1.0-beta.3",
|
|
49
49
|
"@midnightntwrk/wallet-sdk-indexer-client": "2.0.0-beta.2",
|
|
50
|
-
"@midnightntwrk/wallet-sdk-shielded": "4.0.0-beta.
|
|
51
|
-
"@midnightntwrk/wallet-sdk-unshielded-wallet": "4.0.0-beta.
|
|
50
|
+
"@midnightntwrk/wallet-sdk-shielded": "4.0.0-beta.4",
|
|
51
|
+
"@midnightntwrk/wallet-sdk-unshielded-wallet": "4.0.0-beta.4",
|
|
52
52
|
"pino": "^10.3.1",
|
|
53
53
|
"pino-pretty": "^13.1.3",
|
|
54
54
|
"rxjs": "^7.8.2"
|
|
@@ -71,10 +71,13 @@
|
|
|
71
71
|
"rimraf": "6.1.3",
|
|
72
72
|
"testcontainers": "11.14.0",
|
|
73
73
|
"typescript": "5.9.3",
|
|
74
|
-
"vitest": "4.1.
|
|
74
|
+
"vitest": "4.1.11"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"typecheck": "tsc -b ./tsconfig.json --noEmit --force",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"test:unit": "vitest run --project unit --passWithNoTests",
|
|
80
|
+
"test:integration": "vitest run --project integration --passWithNoTests",
|
|
78
81
|
"lint": "eslint --max-warnings 0",
|
|
79
82
|
"format": "prettier --write \"**/*.{ts,js,json,yaml,yml,md}\"",
|
|
80
83
|
"format:check": "prettier --check \"**/*.{ts,js,json,yaml,yml,md}\"",
|