@midnight-ntwrk/wallet-sdk-dust-wallet 5.0.0-beta.3 → 5.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/dist/v1/RunningV1Variant.d.ts +9 -1
- package/dist/v1/RunningV1Variant.js +54 -6
- package/dist/v1/Sync.d.ts +88 -1
- package/dist/v1/Sync.js +47 -3
- package/dist/v1/WalletError.d.ts +26 -1
- package/dist/v1/WalletError.js +16 -0
- package/dist/v2/RunningV2Variant.d.ts +9 -1
- package/dist/v2/RunningV2Variant.js +54 -6
- package/dist/v2/Sync.d.ts +113 -1
- package/dist/v2/Sync.js +105 -16
- package/dist/v2/WalletError.d.ts +26 -1
- package/dist/v2/WalletError.js +16 -0
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Stream, Scope } from 'effect';
|
|
1
|
+
import { Effect, Stream, Scope, Duration, Schedule } from 'effect';
|
|
2
2
|
import { type TransactionHistoryService } from './TransactionHistory.js';
|
|
3
3
|
import { type DustSecretKey, type Signature, type SignatureVerifyingKey, type FinalizedTransaction, type UnprovenTransaction } from '@midnight-ntwrk/ledger-v8';
|
|
4
4
|
import { type WalletError } from './WalletError.js';
|
|
@@ -27,6 +27,14 @@ export declare namespace RunningV1Variant {
|
|
|
27
27
|
type AnyContext = Context<any, any, any, any>;
|
|
28
28
|
}
|
|
29
29
|
export declare const V1Tag: unique symbol;
|
|
30
|
+
/**
|
|
31
|
+
* How a failing synchronization pass is retried.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Exported so the delays it actually produces can be asserted. A schedule reads as though its combinators apply to the
|
|
35
|
+
* delay, and for some of them they do not — which is worth pinning in a test rather than trusting by eye.
|
|
36
|
+
*/
|
|
37
|
+
export declare const syncRetrySchedule: Schedule.Schedule<[Duration.Duration, number], unknown, never>;
|
|
30
38
|
export type DefaultRunningV1 = RunningV1Variant<string, V8.SimulatorState, FinalizedTransaction, DustSecretKey>;
|
|
31
39
|
export declare class RunningV1Variant<TSerialized, TSyncUpdate, TTransaction, TStartAux> implements Variant.RunningVariant<typeof V1Tag, CoreWallet> {
|
|
32
40
|
#private;
|
|
@@ -15,6 +15,7 @@ import { ProtocolVersion, Token } from '@midnight-ntwrk/wallet-sdk-abstractions'
|
|
|
15
15
|
import { OtherWalletError } from './WalletError.js';
|
|
16
16
|
import { ArrayOps, EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
17
17
|
import { StateChange, VersionChangeType, } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
|
|
18
|
+
import { BackgroundRepeat } from './Sync.js';
|
|
18
19
|
const progress = (state) => {
|
|
19
20
|
const appliedIndex = state.progress?.appliedIndex ?? 0n;
|
|
20
21
|
const highestRelevantWalletIndex = state.progress?.highestRelevantWalletIndex ?? 0n;
|
|
@@ -48,6 +49,21 @@ const protocolVersionChange = (previous, current, isInitial, activationRange) =>
|
|
|
48
49
|
: [];
|
|
49
50
|
};
|
|
50
51
|
export const V1Tag = Symbol('V1');
|
|
52
|
+
/**
|
|
53
|
+
* How a failing synchronization pass is retried.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Exported so the delays it actually produces can be asserted. A schedule reads as though its combinators apply to the
|
|
57
|
+
* delay, and for some of them they do not — which is worth pinning in a test rather than trusting by eye.
|
|
58
|
+
*/
|
|
59
|
+
export const syncRetrySchedule = pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.jittered,
|
|
60
|
+
// `union` retries as soon as either side would, so pairing the backoff with a fixed two-minute schedule takes the
|
|
61
|
+
// shorter of the two — which is the backoff until it grows past two minutes, and two minutes from then on. Written
|
|
62
|
+
// this way rather than as a `Schedule.map` over the delay: `map` rewrites a schedule's *output*, which it takes
|
|
63
|
+
// **after** the delay has already been read from it, so mapping an exponential leaves the timing untouched. The
|
|
64
|
+
// earlier form did exactly that, and backed off unboundedly — past an hour by the twelfth attempt — while reading
|
|
65
|
+
// as though it were capped.
|
|
66
|
+
Schedule.union(Schedule.spaced(Duration.minutes(2))));
|
|
51
67
|
export class RunningV1Variant {
|
|
52
68
|
__polyTag__ = V1Tag;
|
|
53
69
|
#scope;
|
|
@@ -59,11 +75,26 @@ export class RunningV1Variant {
|
|
|
59
75
|
#txHistoryPermits = Effect.makeSemaphore(8).pipe(Effect.runSync);
|
|
60
76
|
state;
|
|
61
77
|
#syncLock;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a background synchronization worker is already running.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* Distinct from {@link #syncLock}, which is released at the end of every pass and so says nothing about the worker
|
|
83
|
+
* that runs them. Held for the worker's whole lifetime — polling delays and retries included — and released as it
|
|
84
|
+
* exits, so a wallet started again while it is already synchronizing keeps the worker it has instead of gaining a
|
|
85
|
+
* second one that polls alongside it until the wallet stops.
|
|
86
|
+
*
|
|
87
|
+
* The guard asks only whether a worker is running, not what it was started with, so a second start is ignored even
|
|
88
|
+
* when it carries different key material. Starting a running wallet on a second key is not a supported way to
|
|
89
|
+
* change keys: stop it first. Two workers would be no better — they would apply two keys' updates to one state.
|
|
90
|
+
*/
|
|
91
|
+
#backgroundWorkerLock;
|
|
62
92
|
constructor(scope, context, v1Context) {
|
|
63
93
|
this.#scope = scope;
|
|
64
94
|
this.#context = context;
|
|
65
95
|
this.#v1Context = v1Context;
|
|
66
96
|
this.#syncLock = Effect.runSync(Ref.make(false));
|
|
97
|
+
this.#backgroundWorkerLock = Effect.runSync(Ref.make(false));
|
|
67
98
|
this.state = Stream.fromEffect(context.stateRef.get).pipe(Stream.flatMap((initialState) => context.stateRef.changes.pipe(
|
|
68
99
|
// The accumulator carries the "have we seen anything yet" flag alongside the previous state: the first
|
|
69
100
|
// observation is the only one that can be a restored state nobody has inspected against this variant's
|
|
@@ -78,12 +109,29 @@ export class RunningV1Variant {
|
|
|
78
109
|
}));
|
|
79
110
|
}
|
|
80
111
|
startSyncInBackground(startAux) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
// One pass, in a scope of its own. `Stream.runScoped` acquires into whichever scope it is handed, and the source's
|
|
113
|
+
// client layers take `Scope` as an input requirement rather than discharging one of their own — so a pass drained
|
|
114
|
+
// straight into the variant scope leaves its clients open there until the wallet stops, one more set per repeat.
|
|
115
|
+
// Scoping here closes them as the pass ends, on success, failure or interruption alike, and before any retry delay.
|
|
116
|
+
// The tx-history fan-out `startSync` forks is deliberately unaffected: it is pinned to the variant scope
|
|
117
|
+
// explicitly, and none of its work needs a client this scope holds.
|
|
118
|
+
const pass = this.startSync(startAux).pipe(Stream.runScoped(Sink.drain), Effect.scoped);
|
|
119
|
+
// Retrying the drained pass rather than the stream keeps the scope inside the retry: a failed attempt gives its
|
|
120
|
+
// resources back before the backoff, instead of holding them for the length of it.
|
|
121
|
+
const attempt = pass.pipe(Effect.retry(syncRetrySchedule));
|
|
122
|
+
// A service that synchronizes in finite passes would otherwise leave the wallet frozen at whatever its first pass
|
|
123
|
+
// saw. Repeating re-runs `startSync`, which re-reads the wallet state, so each pass resumes from what the previous
|
|
124
|
+
// one applied. The retry above cannot serve this purpose: it re-runs a pass on failure, not on completion.
|
|
125
|
+
// Repeating outside it keeps a transient failure to the pass it happened in, rather than restarting the cycle.
|
|
126
|
+
const worker = BackgroundRepeat.$match(this.#v1Context.syncService.backgroundRepeat, {
|
|
127
|
+
Once: () => attempt,
|
|
128
|
+
WithDelay: ({ delay }) => Effect.repeat(attempt, Schedule.spaced(delay)).pipe(Effect.asVoid),
|
|
129
|
+
});
|
|
130
|
+
// Taken before the worker is forked and given back as it exits, so the guard covers the gaps between passes that
|
|
131
|
+
// the sync lock cannot. A second start finds it held and leaves the running worker alone.
|
|
132
|
+
return Ref.modify(this.#backgroundWorkerLock, (isRunning) => [!isRunning, true]).pipe(Effect.flatMap((acquired) => acquired
|
|
133
|
+
? worker.pipe(Effect.ensuring(Ref.set(this.#backgroundWorkerLock, false)), Effect.forkScoped, Effect.asVoid)
|
|
134
|
+
: Effect.void), Effect.provideService(Scope.Scope, this.#scope));
|
|
87
135
|
}
|
|
88
136
|
/**
|
|
89
137
|
* Runs one sync pass to completion, rather than forking it into the background.
|
package/dist/v1/Sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Layer, Schema, type Scope, Stream, Option } from 'effect';
|
|
1
|
+
import { Data, Effect, Layer, Schema, type Scope, Stream, Duration, Option } from 'effect';
|
|
2
2
|
import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
3
3
|
import { LedgerParametersCodec } from '@midnight-ntwrk/wallet-sdk-capabilities/codecs';
|
|
4
4
|
import { type DustSecretKey, type DustStateChanges, Event as LedgerEvent, LedgerParameters } from '@midnight-ntwrk/ledger-v8';
|
|
@@ -7,9 +7,96 @@ import { type WalletError } from './WalletError.js';
|
|
|
7
7
|
import { V8 } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
|
|
8
8
|
import { CoreWallet } from './CoreWallet.js';
|
|
9
9
|
import { type NetworkId } from './types/ledger.js';
|
|
10
|
+
/**
|
|
11
|
+
* How often background synchronization runs a sync service's `updates`.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Every service states this, because the answer cannot be inferred from the service: it follows from whether `updates`
|
|
15
|
+
* is finite, which only the service knows. A service whose `updates` is a long-lived subscription is
|
|
16
|
+
* {@link BackgroundRepeat.Once} — that single pass never ends, so there is nothing to repeat. A service that
|
|
17
|
+
* synchronizes in finite passes is {@link BackgroundRepeat.WithDelay}, or a wallet synchronizing in the background
|
|
18
|
+
* would converge once and then never observe anything again. The variant's background retry does not cover the
|
|
19
|
+
* latter: it re-runs a pass on failure, not on completion.
|
|
20
|
+
*
|
|
21
|
+
* Every service this variant has is {@link BackgroundRepeat.Once}: the finite-pass projections sync is a ledger-v9
|
|
22
|
+
* capability that no published ledger-v8 can support. The type is declared in both twins so that their background
|
|
23
|
+
* synchronization reads one contract, and so that a ledger-v8 service which one day synchronizes in finite passes has
|
|
24
|
+
* the vocabulary to say so.
|
|
25
|
+
*/
|
|
26
|
+
export type BackgroundRepeat = Data.TaggedEnum<{
|
|
27
|
+
/** Run `updates` once and leave it running. For a service whose `updates` never completes. */
|
|
28
|
+
Once: {};
|
|
29
|
+
/** Re-run `updates` this long after each pass completes. For a service that synchronizes in finite passes. */
|
|
30
|
+
WithDelay: {
|
|
31
|
+
readonly delay: Duration.DurationInput;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Constructors and matchers for {@link BackgroundRepeat}.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const service: SyncService<CoreWallet, DustSecretKey, WalletSyncUpdate> = {
|
|
40
|
+
* backgroundRepeat: BackgroundRepeat.Once(),
|
|
41
|
+
* updates: (state, secretKey) => subscribe(state, secretKey),
|
|
42
|
+
* blockData: () => latestBlock(),
|
|
43
|
+
* };
|
|
44
|
+
* ```;
|
|
45
|
+
*/
|
|
46
|
+
export declare const BackgroundRepeat: {
|
|
47
|
+
readonly Once: Data.Case.Constructor<{
|
|
48
|
+
readonly _tag: "Once";
|
|
49
|
+
}, "_tag">;
|
|
50
|
+
readonly WithDelay: Data.Case.Constructor<{
|
|
51
|
+
readonly _tag: "WithDelay";
|
|
52
|
+
readonly delay: Duration.DurationInput;
|
|
53
|
+
}, "_tag">;
|
|
54
|
+
readonly $is: <Tag extends "Once" | "WithDelay">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
55
|
+
readonly _tag: "Once";
|
|
56
|
+
}, {
|
|
57
|
+
readonly _tag: Tag;
|
|
58
|
+
}> | Extract<{
|
|
59
|
+
readonly _tag: "WithDelay";
|
|
60
|
+
readonly delay: Duration.DurationInput;
|
|
61
|
+
}, {
|
|
62
|
+
readonly _tag: Tag;
|
|
63
|
+
}>;
|
|
64
|
+
readonly $match: {
|
|
65
|
+
<const Cases extends {
|
|
66
|
+
readonly Once: (args: {
|
|
67
|
+
readonly _tag: "Once";
|
|
68
|
+
}) => any;
|
|
69
|
+
readonly WithDelay: (args: {
|
|
70
|
+
readonly _tag: "WithDelay";
|
|
71
|
+
readonly delay: Duration.DurationInput;
|
|
72
|
+
}) => any;
|
|
73
|
+
}>(cases: Cases & { [K in Exclude<keyof Cases, "Once" | "WithDelay">]: never; }): (value: {
|
|
74
|
+
readonly _tag: "Once";
|
|
75
|
+
} | {
|
|
76
|
+
readonly _tag: "WithDelay";
|
|
77
|
+
readonly delay: Duration.DurationInput;
|
|
78
|
+
}) => import("effect/Unify").Unify<ReturnType<Cases["Once" | "WithDelay"]>>;
|
|
79
|
+
<const Cases extends {
|
|
80
|
+
readonly Once: (args: {
|
|
81
|
+
readonly _tag: "Once";
|
|
82
|
+
}) => any;
|
|
83
|
+
readonly WithDelay: (args: {
|
|
84
|
+
readonly _tag: "WithDelay";
|
|
85
|
+
readonly delay: Duration.DurationInput;
|
|
86
|
+
}) => any;
|
|
87
|
+
}>(value: {
|
|
88
|
+
readonly _tag: "Once";
|
|
89
|
+
} | {
|
|
90
|
+
readonly _tag: "WithDelay";
|
|
91
|
+
readonly delay: Duration.DurationInput;
|
|
92
|
+
}, cases: Cases & { [K in Exclude<keyof Cases, "Once" | "WithDelay">]: never; }): import("effect/Unify").Unify<ReturnType<Cases["Once" | "WithDelay"]>>;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
10
95
|
export interface SyncService<TState, TStartAux, TUpdate> {
|
|
11
96
|
updates: (state: TState, auxData: TStartAux) => Stream.Stream<TUpdate, WalletError, Scope.Scope>;
|
|
12
97
|
blockData: () => Effect.Effect<BlockData, WalletError>;
|
|
98
|
+
/** How often background synchronization re-runs {@link SyncService.updates}. See {@link BackgroundRepeat}. */
|
|
99
|
+
readonly backgroundRepeat: BackgroundRepeat;
|
|
13
100
|
}
|
|
14
101
|
export interface BlockData {
|
|
15
102
|
hash: string;
|
package/dist/v1/Sync.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
|
-
import { Effect, Either, Layer, ParseResult, pipe, Schema, Stream, Duration, Chunk, Schedule, Option, identity, } from 'effect';
|
|
13
|
+
import { Array as Arr, Data, Effect, Either, Layer, ParseResult, pipe, Schema, Stream, Duration, Chunk, Schedule, Option, identity, } from 'effect';
|
|
14
14
|
import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
15
15
|
import { LedgerParametersCodec } from '@midnight-ntwrk/wallet-sdk-capabilities/codecs';
|
|
16
16
|
import { Event as LedgerEvent, LedgerParameters, } from '@midnight-ntwrk/ledger-v8';
|
|
@@ -18,10 +18,23 @@ import { BlockHash, DustLedgerEventTip, DustLedgerEvents } from '@midnight-ntwrk
|
|
|
18
18
|
import { WsSubscriptionClient, HttpQueryClient, ConnectionHelper, } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
|
|
19
19
|
import { EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
20
20
|
import { WsURL } from '@midnight-ntwrk/wallet-sdk-utilities/networking';
|
|
21
|
-
import { OtherWalletError, SyncWalletError } from './WalletError.js';
|
|
21
|
+
import { OtherWalletError, OutOfOrderSyncUpdateError, SyncWalletError } from './WalletError.js';
|
|
22
22
|
import { V8 } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
|
|
23
23
|
import { CoreWallet } from './CoreWallet.js';
|
|
24
24
|
import { Uint8ArraySchema } from './Serialization.js';
|
|
25
|
+
/**
|
|
26
|
+
* Constructors and matchers for {@link BackgroundRepeat}.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const service: SyncService<CoreWallet, DustSecretKey, WalletSyncUpdate> = {
|
|
31
|
+
* backgroundRepeat: BackgroundRepeat.Once(),
|
|
32
|
+
* updates: (state, secretKey) => subscribe(state, secretKey),
|
|
33
|
+
* blockData: () => latestBlock(),
|
|
34
|
+
* };
|
|
35
|
+
* ```;
|
|
36
|
+
*/
|
|
37
|
+
export const BackgroundRepeat = Data.taggedEnum();
|
|
25
38
|
/**
|
|
26
39
|
* The ledger parameters codecs this variant reads blocks with, unless it is told otherwise.
|
|
27
40
|
*
|
|
@@ -244,6 +257,8 @@ const versionWatch = (config, resumeFrom, knownVersion) => {
|
|
|
244
257
|
export const makeDefaultSyncService = (config) => {
|
|
245
258
|
const indexerSyncService = makeIndexerSyncService(config);
|
|
246
259
|
return {
|
|
260
|
+
// A long-lived indexer subscription: the one pass never ends, so there is nothing to repeat.
|
|
261
|
+
backgroundRepeat: BackgroundRepeat.Once(),
|
|
247
262
|
updates: (state, secretKey) => {
|
|
248
263
|
const batchSize = config.batchUpdates?.size ?? 10;
|
|
249
264
|
const batchTimeout = Duration.millis(config.batchUpdates?.timeout ?? 1);
|
|
@@ -358,6 +373,26 @@ const applyVersionSignal = (state, update) => [
|
|
|
358
373
|
: annotateVersion(state, Option.some(ProtocolVersion.ProtocolVersion(BigInt(update.version)))),
|
|
359
374
|
noChanges(state),
|
|
360
375
|
];
|
|
376
|
+
/**
|
|
377
|
+
* Checks that freshly delivered events arrive in strictly ascending id order.
|
|
378
|
+
*
|
|
379
|
+
* @remarks
|
|
380
|
+
* Order is all that can be checked, never contiguity: dust events share one id sequence with zswap and contract events
|
|
381
|
+
* in the indexer, so gaps in a dust stream are normal. A skipped commitment-inserting event is caught by the ledger's
|
|
382
|
+
* own insertion check instead. An empty batch is trivially ordered.
|
|
383
|
+
* @param fresh The events left after the cursor filter, in source order.
|
|
384
|
+
* @param appliedIndex The highest event id already applied; the first fresh id must exceed it.
|
|
385
|
+
* @returns The batch unchanged, or the first id that fails to exceed its predecessor as an
|
|
386
|
+
* {@link OutOfOrderSyncUpdateError}.
|
|
387
|
+
*/
|
|
388
|
+
const expectAscending = (fresh, appliedIndex) => pipe(Arr.zipWith(fresh, Arr.prepend(Arr.map(fresh, (item) => BigInt(item.id)), appliedIndex), (item, previous) => ({ item, previous })), Arr.findFirst(({ item, previous }) => BigInt(item.id) <= previous), Option.match({
|
|
389
|
+
onNone: () => Either.right(fresh),
|
|
390
|
+
onSome: ({ item, previous }) => Either.left(new OutOfOrderSyncUpdateError({
|
|
391
|
+
message: `Dust event ${item.id} delivered at or below its predecessor ${previous}; batch refused, applied index stays at ${appliedIndex}`,
|
|
392
|
+
expected: previous,
|
|
393
|
+
received: BigInt(item.id),
|
|
394
|
+
})),
|
|
395
|
+
}));
|
|
361
396
|
export const makeDefaultSyncCapability = () => {
|
|
362
397
|
return {
|
|
363
398
|
applyUpdate(state, wrappedUpdate, activeRange) {
|
|
@@ -374,7 +409,14 @@ export const makeDefaultSyncCapability = () => {
|
|
|
374
409
|
// The tip is a property of the source, not of what this variant chose to apply, so it is read from the batch
|
|
375
410
|
// tail even when the tail belongs to the next protocol version.
|
|
376
411
|
const highestRelevantWalletIndex = BigInt(updates.at(-1).maxId);
|
|
377
|
-
|
|
412
|
+
// Refuse the whole batch: applying the sound prefix would move the cursor and hide the fault behind
|
|
413
|
+
// `isConnected: true`. State and cursor stay untouched, so the retry re-fetches the same range; a transient
|
|
414
|
+
// reorder heals, a persistent one fails visibly.
|
|
415
|
+
//
|
|
416
|
+
// `SyncCapability.applyUpdate` has no typed error channel, so throwing preserves its public tuple-returning API;
|
|
417
|
+
// `RunningV1Variant` catches this at the capability boundary. See #572 for the planned `Either`-based API.
|
|
418
|
+
const orderedUpdates = Either.getOrThrowWith(expectAscending(freshUpdates, appliedIndex), identity);
|
|
419
|
+
const { applied, observedVersion } = splitAtVersionBoundary(orderedUpdates, (update) => update.protocolVersion, activeRange);
|
|
378
420
|
// Read here and nowhere earlier: `applied` is exactly the slice this variant owns, so nothing outside its
|
|
379
421
|
// activation range — nor anything below its cursor — is ever handed to this ledger version's deserializer.
|
|
380
422
|
const [newState, changes] = applied.length === 0
|
|
@@ -402,6 +444,8 @@ export const makeDefaultSyncCapability = () => {
|
|
|
402
444
|
};
|
|
403
445
|
export const makeSimulatorSyncService = (config) => {
|
|
404
446
|
return {
|
|
447
|
+
// A live feed of simulator states: the one pass never ends, so there is nothing to repeat.
|
|
448
|
+
backgroundRepeat: BackgroundRepeat.Once(),
|
|
405
449
|
updates: (_state, secretKey) => {
|
|
406
450
|
// Get the initial state immediately to ensure we process the genesis block.
|
|
407
451
|
// Then subscribe to state$ for subsequent changes, but deduplicate by block number
|
package/dist/v1/WalletError.d.ts
CHANGED
|
@@ -39,5 +39,30 @@ export declare class TransactionHistoryError extends TransactionHistoryError_bas
|
|
|
39
39
|
cause?: unknown;
|
|
40
40
|
}> {
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
declare const OutOfOrderSyncUpdateError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
43
|
+
readonly _tag: "Wallet.OutOfOrderSyncUpdate";
|
|
44
|
+
} & Readonly<A>;
|
|
45
|
+
/**
|
|
46
|
+
* A batch of dust ledger events was not in ascending id order: an event arrived at or below the one before it, or at or
|
|
47
|
+
* below the applied cursor.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Order is all the wallet can check: dust events share one id sequence with zswap and contract events in the indexer,
|
|
51
|
+
* so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
|
|
52
|
+
* whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
|
|
53
|
+
* place.
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare class OutOfOrderSyncUpdateError extends OutOfOrderSyncUpdateError_base<{
|
|
60
|
+
readonly message: string;
|
|
61
|
+
/** The id the delivered event had to exceed: its predecessor in the batch, or the applied cursor. */
|
|
62
|
+
readonly expected: bigint;
|
|
63
|
+
/** The event id the source delivered instead. */
|
|
64
|
+
readonly received: bigint;
|
|
65
|
+
}> {
|
|
66
|
+
}
|
|
67
|
+
export type WalletError = OtherWalletError | SyncWalletError | TransactingError | InsufficientFundsError | TransactionHistoryError | OutOfOrderSyncUpdateError | LedgerOps.LedgerError;
|
|
43
68
|
export {};
|
package/dist/v1/WalletError.js
CHANGED
|
@@ -21,3 +21,19 @@ export class InsufficientFundsError extends Data.TaggedError('Wallet.Insufficien
|
|
|
21
21
|
}
|
|
22
22
|
export class TransactionHistoryError extends Data.TaggedError('Wallet.TransactionHistory') {
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A batch of dust ledger events was not in ascending id order: an event arrived at or below the one before it, or at or
|
|
26
|
+
* below the applied cursor.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Order is all the wallet can check: dust events share one id sequence with zswap and contract events in the indexer,
|
|
30
|
+
* so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
|
|
31
|
+
* whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
|
|
32
|
+
* place.
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class OutOfOrderSyncUpdateError extends Data.TaggedError('Wallet.OutOfOrderSyncUpdate') {
|
|
39
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Stream, Scope } from 'effect';
|
|
1
|
+
import { Effect, Stream, Scope, Duration, Schedule } from 'effect';
|
|
2
2
|
import { type TransactionHistoryService } from './TransactionHistory.js';
|
|
3
3
|
import { type DustSecretKey, type Signature, type SignatureVerifyingKey, type FinalizedTransaction, type UnprovenTransaction } from '@midnightntwrk/ledger-v9';
|
|
4
4
|
import { type WalletError } from './WalletError.js';
|
|
@@ -28,6 +28,14 @@ export declare namespace RunningV2Variant {
|
|
|
28
28
|
type AnyContext = Context<any, any, any, any>;
|
|
29
29
|
}
|
|
30
30
|
export declare const V2Tag: unique symbol;
|
|
31
|
+
/**
|
|
32
|
+
* How a failing synchronization pass is retried.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Exported so the delays it actually produces can be asserted. A schedule reads as though its combinators apply to the
|
|
36
|
+
* delay, and for some of them they do not — which is worth pinning in a test rather than trusting by eye.
|
|
37
|
+
*/
|
|
38
|
+
export declare const syncRetrySchedule: Schedule.Schedule<[Duration.Duration, number], unknown, never>;
|
|
31
39
|
export type DefaultRunningV2 = RunningV2Variant<string, SimulatorState, FinalizedTransaction, DustSecretKey>;
|
|
32
40
|
export declare class RunningV2Variant<TSerialized, TSyncUpdate, TTransaction, TStartAux> implements Variant.RunningVariant<typeof V2Tag, CoreWallet> {
|
|
33
41
|
#private;
|
|
@@ -15,6 +15,7 @@ import { ProtocolVersion, Token } from '@midnight-ntwrk/wallet-sdk-abstractions'
|
|
|
15
15
|
import { OtherWalletError } from './WalletError.js';
|
|
16
16
|
import { ArrayOps, EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
17
17
|
import { StateChange, VersionChangeType, } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
|
|
18
|
+
import { BackgroundRepeat } from './Sync.js';
|
|
18
19
|
const progress = (state) => {
|
|
19
20
|
const appliedIndex = state.progress?.appliedIndex ?? 0n;
|
|
20
21
|
const highestRelevantWalletIndex = state.progress?.highestRelevantWalletIndex ?? 0n;
|
|
@@ -48,6 +49,21 @@ const protocolVersionChange = (previous, current, isInitial, activationRange) =>
|
|
|
48
49
|
: [];
|
|
49
50
|
};
|
|
50
51
|
export const V2Tag = Symbol('V2');
|
|
52
|
+
/**
|
|
53
|
+
* How a failing synchronization pass is retried.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Exported so the delays it actually produces can be asserted. A schedule reads as though its combinators apply to the
|
|
57
|
+
* delay, and for some of them they do not — which is worth pinning in a test rather than trusting by eye.
|
|
58
|
+
*/
|
|
59
|
+
export const syncRetrySchedule = pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.jittered,
|
|
60
|
+
// `union` retries as soon as either side would, so pairing the backoff with a fixed two-minute schedule takes the
|
|
61
|
+
// shorter of the two — which is the backoff until it grows past two minutes, and two minutes from then on. Written
|
|
62
|
+
// this way rather than as a `Schedule.map` over the delay: `map` rewrites a schedule's *output*, which it takes
|
|
63
|
+
// **after** the delay has already been read from it, so mapping an exponential leaves the timing untouched. The
|
|
64
|
+
// earlier form did exactly that, and backed off unboundedly — past an hour by the twelfth attempt — while reading
|
|
65
|
+
// as though it were capped.
|
|
66
|
+
Schedule.union(Schedule.spaced(Duration.minutes(2))));
|
|
51
67
|
export class RunningV2Variant {
|
|
52
68
|
__polyTag__ = V2Tag;
|
|
53
69
|
#scope;
|
|
@@ -59,11 +75,26 @@ export class RunningV2Variant {
|
|
|
59
75
|
#txHistoryPermits = Effect.makeSemaphore(8).pipe(Effect.runSync);
|
|
60
76
|
state;
|
|
61
77
|
#syncLock;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a background synchronization worker is already running.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* Distinct from {@link #syncLock}, which is released at the end of every pass and so says nothing about the worker
|
|
83
|
+
* that runs them. Held for the worker's whole lifetime — polling delays and retries included — and released as it
|
|
84
|
+
* exits, so a wallet started again while it is already synchronizing keeps the worker it has instead of gaining a
|
|
85
|
+
* second one that polls alongside it until the wallet stops.
|
|
86
|
+
*
|
|
87
|
+
* The guard asks only whether a worker is running, not what it was started with, so a second start is ignored even
|
|
88
|
+
* when it carries different key material. Starting a running wallet on a second key is not a supported way to
|
|
89
|
+
* change keys: stop it first. Two workers would be no better — they would apply two keys' updates to one state.
|
|
90
|
+
*/
|
|
91
|
+
#backgroundWorkerLock;
|
|
62
92
|
constructor(scope, context, v2Context) {
|
|
63
93
|
this.#scope = scope;
|
|
64
94
|
this.#context = context;
|
|
65
95
|
this.#v2Context = v2Context;
|
|
66
96
|
this.#syncLock = Effect.runSync(Ref.make(false));
|
|
97
|
+
this.#backgroundWorkerLock = Effect.runSync(Ref.make(false));
|
|
67
98
|
this.state = Stream.fromEffect(context.stateRef.get).pipe(Stream.flatMap((initialState) => context.stateRef.changes.pipe(
|
|
68
99
|
// The accumulator carries the "have we seen anything yet" flag alongside the previous state: the first
|
|
69
100
|
// observation is the only one that can be a restored state nobody has inspected against this variant's
|
|
@@ -78,12 +109,29 @@ export class RunningV2Variant {
|
|
|
78
109
|
}));
|
|
79
110
|
}
|
|
80
111
|
startSyncInBackground(startAux) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
// One pass, in a scope of its own. `Stream.runScoped` acquires into whichever scope it is handed, and the source's
|
|
113
|
+
// client layers take `Scope` as an input requirement rather than discharging one of their own — so a pass drained
|
|
114
|
+
// straight into the variant scope leaves its clients open there until the wallet stops, one more set per repeat.
|
|
115
|
+
// Scoping here closes them as the pass ends, on success, failure or interruption alike, and before any retry delay.
|
|
116
|
+
// The tx-history fan-out `startSync` forks is deliberately unaffected: it is pinned to the variant scope
|
|
117
|
+
// explicitly, and none of its work needs a client this scope holds.
|
|
118
|
+
const pass = this.startSync(startAux).pipe(Stream.runScoped(Sink.drain), Effect.scoped);
|
|
119
|
+
// Retrying the drained pass rather than the stream keeps the scope inside the retry: a failed attempt gives its
|
|
120
|
+
// resources back before the backoff, instead of holding them for the length of it.
|
|
121
|
+
const attempt = pass.pipe(Effect.retry(syncRetrySchedule));
|
|
122
|
+
// A service that synchronizes in finite passes would otherwise leave the wallet frozen at whatever its first pass
|
|
123
|
+
// saw. Repeating re-runs `startSync`, which re-reads the wallet state, so each pass resumes from what the previous
|
|
124
|
+
// one applied. The retry above cannot serve this purpose: it re-runs a pass on failure, not on completion.
|
|
125
|
+
// Repeating outside it keeps a transient failure to the pass it happened in, rather than restarting the cycle.
|
|
126
|
+
const worker = BackgroundRepeat.$match(this.#v2Context.syncService.backgroundRepeat, {
|
|
127
|
+
Once: () => attempt,
|
|
128
|
+
WithDelay: ({ delay }) => Effect.repeat(attempt, Schedule.spaced(delay)).pipe(Effect.asVoid),
|
|
129
|
+
});
|
|
130
|
+
// Taken before the worker is forked and given back as it exits, so the guard covers the gaps between passes that
|
|
131
|
+
// the sync lock cannot. A second start finds it held and leaves the running worker alone.
|
|
132
|
+
return Ref.modify(this.#backgroundWorkerLock, (isRunning) => [!isRunning, true]).pipe(Effect.flatMap((acquired) => acquired
|
|
133
|
+
? worker.pipe(Effect.ensuring(Ref.set(this.#backgroundWorkerLock, false)), Effect.forkScoped, Effect.asVoid)
|
|
134
|
+
: Effect.void), Effect.provideService(Scope.Scope, this.#scope));
|
|
87
135
|
}
|
|
88
136
|
sync(startAux) {
|
|
89
137
|
return this.startSync(startAux).pipe(Stream.runScoped(Sink.drain), Effect.scoped);
|
package/dist/v2/Sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Array as Arr, Effect, Layer, Option, type Scope, Stream } from 'effect';
|
|
1
|
+
import { Array as Arr, Data, Effect, Layer, Option, type Scope, Stream, Duration } from 'effect';
|
|
2
2
|
import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
3
3
|
import { type LedgerParametersCodec } from '@midnight-ntwrk/wallet-sdk-capabilities/codecs';
|
|
4
4
|
import { type DustNullifier, type DustSecretKey, DustStateChanges, type DustLocalState, type LedgerParameters } from '@midnightntwrk/ledger-v9';
|
|
@@ -9,9 +9,91 @@ import { CoreWallet } from './CoreWallet.js';
|
|
|
9
9
|
import { type NetworkId } from './types/ledger.js';
|
|
10
10
|
import { type CollapsedMerkleTree, type DustGenerationsSubscription, type DustNullifierTransactionsSubscription, type DustProjectionsUpdate, type DustSpendProcessedEvent, type DustUtxoUpdate, DustUtxoMap, type WalletSyncSubscription, WalletSyncUpdate, type BlockData, type DustGenerationDtimUpdate, type NullifierRegularTransaction } from './SyncSchema.js';
|
|
11
11
|
import { type Dust } from './types/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* How often background synchronization runs a sync service's `updates`.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Every service states this, because the answer cannot be inferred from the service: it follows from whether `updates`
|
|
17
|
+
* is finite, which only the service knows. A service whose `updates` is a long-lived subscription is
|
|
18
|
+
* {@link BackgroundRepeat.Once} — that single pass never ends, so there is nothing to repeat. A service that
|
|
19
|
+
* synchronizes in finite passes is {@link BackgroundRepeat.WithDelay}, or a wallet synchronizing in the background
|
|
20
|
+
* would converge once and then never observe anything again. The variant's background retry does not cover the
|
|
21
|
+
* latter: it re-runs a pass on failure, not on completion.
|
|
22
|
+
*/
|
|
23
|
+
export type BackgroundRepeat = Data.TaggedEnum<{
|
|
24
|
+
/** Run `updates` once and leave it running. For a service whose `updates` never completes. */
|
|
25
|
+
Once: {};
|
|
26
|
+
/** Re-run `updates` this long after each pass completes. For a service that synchronizes in finite passes. */
|
|
27
|
+
WithDelay: {
|
|
28
|
+
readonly delay: Duration.DurationInput;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Constructors and matchers for {@link BackgroundRepeat}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const service: SyncService<CoreWallet, DustSecretKey, WalletSyncUpdate> = {
|
|
37
|
+
* backgroundRepeat: BackgroundRepeat.Once(),
|
|
38
|
+
* updates: (state, secretKey) => subscribe(state, secretKey),
|
|
39
|
+
* blockData: () => latestBlock(),
|
|
40
|
+
* };
|
|
41
|
+
* ```;
|
|
42
|
+
*/
|
|
43
|
+
export declare const BackgroundRepeat: {
|
|
44
|
+
readonly Once: Data.Case.Constructor<{
|
|
45
|
+
readonly _tag: "Once";
|
|
46
|
+
}, "_tag">;
|
|
47
|
+
readonly WithDelay: Data.Case.Constructor<{
|
|
48
|
+
readonly _tag: "WithDelay";
|
|
49
|
+
readonly delay: Duration.DurationInput;
|
|
50
|
+
}, "_tag">;
|
|
51
|
+
readonly $is: <Tag extends "Once" | "WithDelay">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
52
|
+
readonly _tag: "Once";
|
|
53
|
+
}, {
|
|
54
|
+
readonly _tag: Tag;
|
|
55
|
+
}> | Extract<{
|
|
56
|
+
readonly _tag: "WithDelay";
|
|
57
|
+
readonly delay: Duration.DurationInput;
|
|
58
|
+
}, {
|
|
59
|
+
readonly _tag: Tag;
|
|
60
|
+
}>;
|
|
61
|
+
readonly $match: {
|
|
62
|
+
<const Cases extends {
|
|
63
|
+
readonly Once: (args: {
|
|
64
|
+
readonly _tag: "Once";
|
|
65
|
+
}) => any;
|
|
66
|
+
readonly WithDelay: (args: {
|
|
67
|
+
readonly _tag: "WithDelay";
|
|
68
|
+
readonly delay: Duration.DurationInput;
|
|
69
|
+
}) => any;
|
|
70
|
+
}>(cases: Cases & { [K in Exclude<keyof Cases, "Once" | "WithDelay">]: never; }): (value: {
|
|
71
|
+
readonly _tag: "Once";
|
|
72
|
+
} | {
|
|
73
|
+
readonly _tag: "WithDelay";
|
|
74
|
+
readonly delay: Duration.DurationInput;
|
|
75
|
+
}) => import("effect/Unify").Unify<ReturnType<Cases["Once" | "WithDelay"]>>;
|
|
76
|
+
<const Cases extends {
|
|
77
|
+
readonly Once: (args: {
|
|
78
|
+
readonly _tag: "Once";
|
|
79
|
+
}) => any;
|
|
80
|
+
readonly WithDelay: (args: {
|
|
81
|
+
readonly _tag: "WithDelay";
|
|
82
|
+
readonly delay: Duration.DurationInput;
|
|
83
|
+
}) => any;
|
|
84
|
+
}>(value: {
|
|
85
|
+
readonly _tag: "Once";
|
|
86
|
+
} | {
|
|
87
|
+
readonly _tag: "WithDelay";
|
|
88
|
+
readonly delay: Duration.DurationInput;
|
|
89
|
+
}, cases: Cases & { [K in Exclude<keyof Cases, "Once" | "WithDelay">]: never; }): import("effect/Unify").Unify<ReturnType<Cases["Once" | "WithDelay"]>>;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
12
92
|
export interface SyncService<TState, TStartAux, TUpdate> {
|
|
13
93
|
updates: (state: TState, auxData: TStartAux) => Stream.Stream<TUpdate, WalletError, Scope.Scope>;
|
|
14
94
|
blockData: (height?: number) => Effect.Effect<BlockData, WalletError>;
|
|
95
|
+
/** How often background synchronization re-runs {@link SyncService.updates}. See {@link BackgroundRepeat}. */
|
|
96
|
+
readonly backgroundRepeat: BackgroundRepeat;
|
|
15
97
|
}
|
|
16
98
|
export type ChangesResult = {
|
|
17
99
|
readonly changes: DustStateChanges[];
|
|
@@ -121,6 +203,19 @@ export type DefaultSyncConfiguration = {
|
|
|
121
203
|
versionWatch?: VersionWatchConfig;
|
|
122
204
|
/** The ledger parameters codecs blocks are read with; defaults to {@link defaultLedgerParametersCodecs}. */
|
|
123
205
|
ledgerParametersCodecs?: LedgerParametersCodec.LedgerParametersCodecs<LedgerParameters>;
|
|
206
|
+
/**
|
|
207
|
+
* Delay in milliseconds between background synchronization passes, for a sync service that synchronizes in finite
|
|
208
|
+
* passes rather than over a live subscription — currently only the projections service. Ignored by the event-based
|
|
209
|
+
* service, whose subscription never completes.
|
|
210
|
+
*
|
|
211
|
+
* Lower values keep the wallet closer to the chain tip at the cost of more idle queries; a pass with nothing to do
|
|
212
|
+
* costs one block query, because the generation subscription and the commitment load both short-circuit on an
|
|
213
|
+
* unchanged tip. Passes never overlap regardless of this value: a pass that cannot take the sync lock yields to the
|
|
214
|
+
* one already running and waits for the next interval.
|
|
215
|
+
*
|
|
216
|
+
* @default 5000
|
|
217
|
+
*/
|
|
218
|
+
backgroundSyncInterval?: number;
|
|
124
219
|
};
|
|
125
220
|
export type SimulatorSyncConfiguration = {
|
|
126
221
|
simulator: Simulator;
|
|
@@ -136,6 +231,23 @@ export declare const SecretKeysResource: {
|
|
|
136
231
|
};
|
|
137
232
|
export declare const makeDefaultSyncService: (config: DefaultSyncConfiguration) => SyncService<CoreWallet, DustSecretKey, WalletSyncUpdate>;
|
|
138
233
|
export declare const nullifierPhaseProgress: (mtIndicesSum: bigint, totalNullifiers: number, freshCount: number, maxCommitmentEndIndex: number) => number;
|
|
234
|
+
/**
|
|
235
|
+
* The applied index a mid-phase nullifier report carries, on the same basis as every other report in the pass.
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* The nullifier term is the larger of two true statements about the same work. {@link nullifierPhaseProgress} measures
|
|
239
|
+
* this phase from zero, while the report that opened the phase already credited the nullifiers the wallet arrived
|
|
240
|
+
* holding; early in the phase the credit is the further along of the two, and reporting the measurement alone would
|
|
241
|
+
* take progress backwards.
|
|
242
|
+
* @param basis The two terms this phase does not advance: commitment work, and nullifier work already credited.
|
|
243
|
+
* @param generationWork The generation term, which is complete by the time this phase runs.
|
|
244
|
+
* @param phaseProgress What {@link nullifierPhaseProgress} has measured so far.
|
|
245
|
+
* @returns The applied index to report.
|
|
246
|
+
*/
|
|
247
|
+
export declare const nullifierPhaseAppliedIndex: (basis: {
|
|
248
|
+
readonly commitmentWork: number;
|
|
249
|
+
readonly creditedNullifierWork: number;
|
|
250
|
+
}, generationWork: number, phaseProgress: number) => number;
|
|
139
251
|
export declare const doEventlessSync: (state: CoreWallet, secretKey: DustSecretKey, anonymityLevel: number, indexerSyncService: IndexerSyncService, ledgerParametersCodecs?: LedgerParametersCodec.LedgerParametersCodecs<LedgerParameters>) => Stream.Stream<DustProjectionsUpdate, WalletError, Scope.Scope | QueryClient | SubscriptionClient>;
|
|
140
252
|
export declare const makeEventLessSyncService: (config: DefaultSyncConfiguration) => SyncService<CoreWallet, DustSecretKey, DustProjectionsUpdate>;
|
|
141
253
|
export type IndexerSyncService = {
|
package/dist/v2/Sync.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
|
-
import { Array as Arr, Effect, Either, HashMap, HashSet, Layer, Option, pipe, Schema, Stream, Duration, Chunk, Schedule, identity, } from 'effect';
|
|
13
|
+
import { Array as Arr, Data, Effect, Either, HashMap, HashSet, Layer, Option, pipe, Schema, Stream, Duration, Chunk, Schedule, identity, } from 'effect';
|
|
14
14
|
import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
15
15
|
import { dustNullifier, successorDustUtxo, DustStateChanges, } from '@midnightntwrk/ledger-v9';
|
|
16
16
|
import { DustAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
|
|
@@ -18,11 +18,24 @@ import { DustGenerationEvents, BlockHash, DustLedgerEventTip, DustLedgerEvents,
|
|
|
18
18
|
import { WsSubscriptionClient, HttpQueryClient, ConnectionHelper, } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
|
|
19
19
|
import { EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
20
20
|
import { WsURL } from '@midnight-ntwrk/wallet-sdk-utilities/networking';
|
|
21
|
-
import { OtherWalletError, SyncWalletError } from './WalletError.js';
|
|
21
|
+
import { OtherWalletError, OutOfOrderSyncUpdateError, SyncWalletError } from './WalletError.js';
|
|
22
22
|
import { getLastBlock } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
|
|
23
23
|
import { CoreWallet } from './CoreWallet.js';
|
|
24
24
|
import { CollapsedMerkleTreeSchema, DustGenerationsSubscriptionSchema, DustGenerationsSyncUpdate, DustNullifierTransactionSubscriptionSchema, DustUtxoMap, SyncEventsUpdateSchema, VersionSignalSyncUpdate, WalletSyncUpdate, defaultLedgerParametersCodecs, makeBlockDataSchema, ProgressUpdate, StateUpdate, isProgressUpdate, readEvent, readNullifierBlockParameters, tryReadEvent, } from './SyncSchema.js';
|
|
25
25
|
import { bigintToLeHex, calculatePrefixLength, gapRanges, hashMapGroupBy, leBigintToHex, uniqueArray, } from './Utils.js';
|
|
26
|
+
/**
|
|
27
|
+
* Constructors and matchers for {@link BackgroundRepeat}.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const service: SyncService<CoreWallet, DustSecretKey, WalletSyncUpdate> = {
|
|
32
|
+
* backgroundRepeat: BackgroundRepeat.Once(),
|
|
33
|
+
* updates: (state, secretKey) => subscribe(state, secretKey),
|
|
34
|
+
* blockData: () => latestBlock(),
|
|
35
|
+
* };
|
|
36
|
+
* ```;
|
|
37
|
+
*/
|
|
38
|
+
export const BackgroundRepeat = Data.taggedEnum();
|
|
26
39
|
/**
|
|
27
40
|
* Splits a batch of version-tagged items at the end of a variant's activation range.
|
|
28
41
|
*
|
|
@@ -155,6 +168,8 @@ export const SecretKeysResource = {
|
|
|
155
168
|
export const makeDefaultSyncService = (config) => {
|
|
156
169
|
const indexerSyncService = makeIndexerSyncService(config);
|
|
157
170
|
return {
|
|
171
|
+
// A long-lived indexer subscription: the one pass never ends, so there is nothing to repeat.
|
|
172
|
+
backgroundRepeat: BackgroundRepeat.Once(),
|
|
158
173
|
updates: (state, secretKey) => {
|
|
159
174
|
const batchSize = config.batchUpdates?.size ?? 10;
|
|
160
175
|
const batchTimeout = Duration.millis(config.batchUpdates?.timeout ?? 1);
|
|
@@ -205,7 +220,26 @@ const NULLIFIER_LENGTH = NULLIFIER_BYTE_LENGTH * 2;
|
|
|
205
220
|
// counts as a full scan of the commitment space; a still-live chain counts up to the merkle index of its freshest
|
|
206
221
|
// successor. Bounded by totalNullifiers * maxCommitmentEndIndex (the phase ceiling).
|
|
207
222
|
export const nullifierPhaseProgress = (mtIndicesSum, totalNullifiers, freshCount, maxCommitmentEndIndex) => Math.min(Number(mtIndicesSum) + (totalNullifiers - freshCount) * maxCommitmentEndIndex, totalNullifiers * maxCommitmentEndIndex);
|
|
208
|
-
|
|
223
|
+
/**
|
|
224
|
+
* The applied index a mid-phase nullifier report carries, on the same basis as every other report in the pass.
|
|
225
|
+
*
|
|
226
|
+
* @remarks
|
|
227
|
+
* The nullifier term is the larger of two true statements about the same work. {@link nullifierPhaseProgress} measures
|
|
228
|
+
* this phase from zero, while the report that opened the phase already credited the nullifiers the wallet arrived
|
|
229
|
+
* holding; early in the phase the credit is the further along of the two, and reporting the measurement alone would
|
|
230
|
+
* take progress backwards.
|
|
231
|
+
* @param basis The two terms this phase does not advance: commitment work, and nullifier work already credited.
|
|
232
|
+
* @param generationWork The generation term, which is complete by the time this phase runs.
|
|
233
|
+
* @param phaseProgress What {@link nullifierPhaseProgress} has measured so far.
|
|
234
|
+
* @returns The applied index to report.
|
|
235
|
+
*/
|
|
236
|
+
export const nullifierPhaseAppliedIndex = (basis, generationWork, phaseProgress) => generationWork + basis.commitmentWork + Math.max(basis.creditedNullifierWork, phaseProgress);
|
|
237
|
+
const resolveNullifierSpends = (initialNullifiers, initialNewUtxos, pendingDust, dustState, secretKey, latestBlock, dustGenerationUpdates, indexerSyncService, anonymityLevel, ledgerParametersCodecs,
|
|
238
|
+
/**
|
|
239
|
+
* The two terms of the pass's progress basis this phase does not itself advance, so that the progress it reports is a
|
|
240
|
+
* sum on the same basis as every other report rather than a figure of its own.
|
|
241
|
+
*/
|
|
242
|
+
progressBasis, emit) => {
|
|
209
243
|
const maxCommitmentEndIndex = latestBlock.dustCommitmentEndIndex - 1;
|
|
210
244
|
const initialSpentUtxos = HashMap.empty();
|
|
211
245
|
return pipe(Stream.unfoldEffect([initialNullifiers, initialNewUtxos, initialSpentUtxos], ([nullifiersToCheck, newUtxos, spentUtxos]) => {
|
|
@@ -222,8 +256,13 @@ const resolveNullifierSpends = (initialNullifiers, initialNewUtxos, pendingDust,
|
|
|
222
256
|
const mtIndicesSum = freshUtxos.reduce((partialSum, a) => partialSum + a.qdo.mtIndex, 0n);
|
|
223
257
|
const generationEndIndex = latestBlock.dustGenerationEndIndex - 1;
|
|
224
258
|
const progress = nullifierPhaseProgress(mtIndicesSum, initialNullifiers.length, freshUtxos.length, maxCommitmentEndIndex);
|
|
225
|
-
//
|
|
226
|
-
|
|
259
|
+
// This phase runs after the generation updates, so its term sits on top of a completed generation tree and
|
|
260
|
+
// the commitment work the wallet already had. `nullifierPhaseProgress` counts this phase from zero, so on
|
|
261
|
+
// an early iteration it can still be behind the credit report 2 gave for nullifiers the wallet arrived
|
|
262
|
+
// holding; the larger of the two is the one that is true.
|
|
263
|
+
yield* Effect.promise(() => emit.single(ProgressUpdate({
|
|
264
|
+
appliedIndex: nullifierPhaseAppliedIndex(progressBasis, generationEndIndex, progress),
|
|
265
|
+
})));
|
|
227
266
|
}
|
|
228
267
|
const { nextUtxos, nextSpentUtxos } = accumulateUtxoUpdates(dustUtxoUpdates, newUtxos, spentUtxos);
|
|
229
268
|
const nextNullifiersToCheck = dustUtxoUpdates.filter((u) => !u.isSpent).map((u) => u.dustNullifier);
|
|
@@ -242,10 +281,15 @@ export const doEventlessSync = (state, secretKey, anonymityLevel, indexerSyncSer
|
|
|
242
281
|
const lastSyncedCommitmentIndex = state.state.commitmentTreeFirstFree - 1n;
|
|
243
282
|
const lastSyncedGenerationIndex = state.state.generatingTreeFirstFree - 1n;
|
|
244
283
|
const lastSyncedBlockHeight = state.progress.highestIndex;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
284
|
+
// Every report this pass makes is a sum of the same three terms — generation work, commitment work, nullifier
|
|
285
|
+
// work — against a total on that same basis. A report that leaves one of them out does not read as "less far
|
|
286
|
+
// along"; it reads as progress going *backwards*, because the terms it drops were already counted. That
|
|
287
|
+
// matters beyond cosmetics: `isSynced` is exact equality between the applied and relevant indexes, so a
|
|
288
|
+
// settled wallet whose pass undercounts flips to "not synced" and back on every background repeat.
|
|
289
|
+
const commitmentWork = Number(lastSyncedCommitmentIndex);
|
|
290
|
+
const creditedNullifierWork = state.state.nullifiers.size * maxCommitmentTreeIndex;
|
|
291
|
+
const highestInitialIndex = maxGeneratingTreeIndex + maxCommitmentTreeIndex + creditedNullifierWork;
|
|
292
|
+
const initialAppliedIndex = Number(lastSyncedGenerationIndex) + commitmentWork + creditedNullifierWork;
|
|
249
293
|
// What this pass resumed from, in one line. A projections pass is a single snapshot fetch, so when it comes back
|
|
250
294
|
// with less than expected the only question worth asking is where it started — and the three cursors that decide
|
|
251
295
|
// that are otherwise invisible to anything outside this function, including to a wallet that arrived here by
|
|
@@ -261,12 +305,20 @@ export const doEventlessSync = (state, secretKey, anonymityLevel, indexerSyncSer
|
|
|
261
305
|
.map((n) => n.dustNullifier)
|
|
262
306
|
.concat([...state.state.nullifiers.keys(), ...state.pendingDust.map((d) => d.nullifier)]);
|
|
263
307
|
const highestRelevantIndex = maxGeneratingTreeIndex + maxCommitmentTreeIndex + allNullifiers.length * maxCommitmentTreeIndex;
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
308
|
+
// The generation tree is now synced to the chain's end index, so its term becomes the full
|
|
309
|
+
// `maxGeneratingTreeIndex`. The commitment and nullifier terms are carried across untouched — this phase did
|
|
310
|
+
// not undo them. `highestRelevantIndex` rises here because new generations added nullifiers to check.
|
|
311
|
+
yield* Effect.promise(() => emit.single(ProgressUpdate({
|
|
312
|
+
highestRelevantIndex,
|
|
313
|
+
appliedIndex: maxGeneratingTreeIndex + commitmentWork + creditedNullifierWork,
|
|
314
|
+
})));
|
|
267
315
|
const newUtxos = DustUtxoMap.create(dustGenerationUpdates.newGenerations);
|
|
268
|
-
const { nextUtxos: finalUtxos, nextSpentUtxos: finalSpentUtxos } = yield* resolveNullifierSpends(allNullifiers, newUtxos, CoreWallet.pendingDustToMap(state.pendingDust), state.state, secretKey, latestBlock, dustGenerationUpdates, indexerSyncService, anonymityLevel, ledgerParametersCodecs, emit);
|
|
269
|
-
|
|
316
|
+
const { nextUtxos: finalUtxos, nextSpentUtxos: finalSpentUtxos } = yield* resolveNullifierSpends(allNullifiers, newUtxos, CoreWallet.pendingDustToMap(state.pendingDust), state.state, secretKey, latestBlock, dustGenerationUpdates, indexerSyncService, anonymityLevel, ledgerParametersCodecs, { commitmentWork, creditedNullifierWork }, emit);
|
|
317
|
+
// Every nullifier has been resolved, so the nullifier term reaches its full width. The commitment phase has
|
|
318
|
+
// not run yet, so its term is still what the wallet arrived holding.
|
|
319
|
+
yield* Effect.promise(() => emit.single(ProgressUpdate({
|
|
320
|
+
appliedIndex: maxGeneratingTreeIndex + commitmentWork + allNullifiers.length * maxCommitmentTreeIndex,
|
|
321
|
+
})));
|
|
270
322
|
// lastSyncedCommitmentIndex out of maxCommitmentTreeIndex
|
|
271
323
|
const collapsedCommitments = yield* loadCollapsedCommitments(Number(lastSyncedCommitmentIndex), maxCommitmentTreeIndex, finalUtxos, indexerSyncService);
|
|
272
324
|
yield* Effect.promise(() => emit.single(StateUpdate({
|
|
@@ -279,13 +331,21 @@ export const doEventlessSync = (state, secretKey, anonymityLevel, indexerSyncSer
|
|
|
279
331
|
// NOTE: appliedIndex = highestRelevantIndex means we're fully synced
|
|
280
332
|
yield* Effect.promise(() => emit.single(ProgressUpdate({ appliedIndex: highestRelevantIndex })));
|
|
281
333
|
yield* Effect.promise(() => emit.end());
|
|
282
|
-
})
|
|
334
|
+
}),
|
|
335
|
+
// The whole pass runs inside this register effect, and Effect only starts draining the stream once that effect
|
|
336
|
+
// returns — so every update a pass emits has to sit in the buffer simultaneously. The default buffer is bounded
|
|
337
|
+
// (16), which caps the sync at a handful of Dust spends: the emission that overflows it suspends waiting for
|
|
338
|
+
// capacity no consumer is running to free, and the pass deadlocks silently rather than failing. A pass emits one
|
|
339
|
+
// update per resolved spend, so the ceiling is reached by any wallet with a modest spend history.
|
|
340
|
+
'unbounded');
|
|
283
341
|
};
|
|
284
342
|
export const makeEventLessSyncService = (config) => {
|
|
285
343
|
const defaultSyncService = makeDefaultSyncService(config);
|
|
286
344
|
const indexerSyncService = makeIndexerSyncService(config);
|
|
287
345
|
const anonymityLevel = config.anonymityLevel ?? 7;
|
|
288
346
|
return {
|
|
347
|
+
// Each pass ends its own stream, so background synchronization has to be told to run another one.
|
|
348
|
+
backgroundRepeat: BackgroundRepeat.WithDelay({ delay: Duration.millis(config.backgroundSyncInterval ?? 5_000) }),
|
|
289
349
|
updates: (state, secretKey) => pipe(doEventlessSync(state, secretKey, anonymityLevel, indexerSyncService, config.ledgerParametersCodecs ?? defaultLedgerParametersCodecs), Stream.provideSomeLayer(Layer.merge(indexerSyncService.connectionLayer(), indexerSyncService.queryClient()))),
|
|
290
350
|
blockData: defaultSyncService.blockData,
|
|
291
351
|
};
|
|
@@ -401,6 +461,26 @@ const applyVersionSignal = (state, update) => [
|
|
|
401
461
|
: annotateVersion(state, Option.some(ProtocolVersion.ProtocolVersion(BigInt(update.version)))),
|
|
402
462
|
noChanges(state),
|
|
403
463
|
];
|
|
464
|
+
/**
|
|
465
|
+
* Checks that freshly delivered events arrive in strictly ascending id order.
|
|
466
|
+
*
|
|
467
|
+
* @remarks
|
|
468
|
+
* Order is all that can be checked, never contiguity: dust events share one id sequence with zswap and contract events
|
|
469
|
+
* in the indexer, so gaps in a dust stream are normal. A skipped commitment-inserting event is caught by the ledger's
|
|
470
|
+
* own insertion check instead. An empty batch is trivially ordered.
|
|
471
|
+
* @param fresh The events left after the cursor filter, in source order.
|
|
472
|
+
* @param appliedIndex The highest event id already applied; the first fresh id must exceed it.
|
|
473
|
+
* @returns The batch unchanged, or the first id that fails to exceed its predecessor as an
|
|
474
|
+
* {@link OutOfOrderSyncUpdateError}.
|
|
475
|
+
*/
|
|
476
|
+
const expectAscending = (fresh, appliedIndex) => pipe(Arr.zipWith(fresh, Arr.prepend(Arr.map(fresh, (item) => BigInt(item.id)), appliedIndex), (item, previous) => ({ item, previous })), Arr.findFirst(({ item, previous }) => BigInt(item.id) <= previous), Option.match({
|
|
477
|
+
onNone: () => Either.right(fresh),
|
|
478
|
+
onSome: ({ item, previous }) => Either.left(new OutOfOrderSyncUpdateError({
|
|
479
|
+
message: `Dust event ${item.id} delivered at or below its predecessor ${previous}; batch refused, applied index stays at ${appliedIndex}`,
|
|
480
|
+
expected: previous,
|
|
481
|
+
received: BigInt(item.id),
|
|
482
|
+
})),
|
|
483
|
+
}));
|
|
404
484
|
export const makeDefaultSyncCapability = () => {
|
|
405
485
|
return {
|
|
406
486
|
applyUpdate(state, wrappedUpdate, activeRange) {
|
|
@@ -417,7 +497,14 @@ export const makeDefaultSyncCapability = () => {
|
|
|
417
497
|
// The tip is a property of the source, not of what this variant chose to apply, so it is read from the batch
|
|
418
498
|
// tail even when the tail belongs to the next protocol version.
|
|
419
499
|
const highestRelevantWalletIndex = BigInt(updates.at(-1).maxId);
|
|
420
|
-
|
|
500
|
+
// Refuse the whole batch: applying the sound prefix would move the cursor and hide the fault behind
|
|
501
|
+
// `isConnected: true`. State and cursor stay untouched, so the retry re-fetches the same range; a transient
|
|
502
|
+
// reorder heals, a persistent one fails visibly.
|
|
503
|
+
//
|
|
504
|
+
// `SyncCapability.applyUpdate` has no typed error channel, so throwing preserves its public tuple-returning API;
|
|
505
|
+
// `RunningV2Variant` catches this at the capability boundary. See #572 for the planned `Either`-based API.
|
|
506
|
+
const orderedUpdates = Either.getOrThrowWith(expectAscending(freshUpdates, appliedIndex), identity);
|
|
507
|
+
const { applied, observedVersion } = splitAtVersionBoundary(orderedUpdates, (update) => update.protocolVersion, activeRange);
|
|
421
508
|
// Read here and nowhere earlier: `applied` is exactly the slice this variant owns, so nothing outside its
|
|
422
509
|
// activation range — nor anything below its cursor — is ever handed to this ledger version's deserializer.
|
|
423
510
|
const [newState, changes] = applied.length === 0
|
|
@@ -569,6 +656,8 @@ export const matchedDustSpends = (nullifierTransactions) => pipe(nullifierTransa
|
|
|
569
656
|
export const createDustUtxoUpdates = (dustState, spends, secretKey, knownUtxos, pendingDust, generationDtimeUpdates, ledgerParametersCodecs = defaultLedgerParametersCodecs) => pipe(spends, Arr.map(({ transaction, dustSpend }) => createUtxoUpdatesFromSpend(dustState, secretKey, knownUtxos, pendingDust, generationDtimeUpdates, transaction, dustSpend, ledgerParametersCodecs)), Effect.all, Effect.map(Arr.getSomes), Effect.map(Arr.flatten));
|
|
570
657
|
export const makeSimulatorSyncService = (config) => {
|
|
571
658
|
return {
|
|
659
|
+
// A live feed of simulator states: the one pass never ends, so there is nothing to repeat.
|
|
660
|
+
backgroundRepeat: BackgroundRepeat.Once(),
|
|
572
661
|
updates: (_state, secretKey) => {
|
|
573
662
|
// Get the initial state immediately to ensure we process the genesis block.
|
|
574
663
|
// Then subscribe to state$ for subsequent changes, but deduplicate by block number
|
package/dist/v2/WalletError.d.ts
CHANGED
|
@@ -39,5 +39,30 @@ export declare class TransactionHistoryError extends TransactionHistoryError_bas
|
|
|
39
39
|
cause?: unknown;
|
|
40
40
|
}> {
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
declare const OutOfOrderSyncUpdateError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
43
|
+
readonly _tag: "Wallet.OutOfOrderSyncUpdate";
|
|
44
|
+
} & Readonly<A>;
|
|
45
|
+
/**
|
|
46
|
+
* A batch of dust ledger events was not in ascending id order: an event arrived at or below the one before it, or at or
|
|
47
|
+
* below the applied cursor.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Order is all the wallet can check: dust events share one id sequence with zswap and contract events in the indexer,
|
|
51
|
+
* so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
|
|
52
|
+
* whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
|
|
53
|
+
* place.
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare class OutOfOrderSyncUpdateError extends OutOfOrderSyncUpdateError_base<{
|
|
60
|
+
readonly message: string;
|
|
61
|
+
/** The id the delivered event had to exceed: its predecessor in the batch, or the applied cursor. */
|
|
62
|
+
readonly expected: bigint;
|
|
63
|
+
/** The event id the source delivered instead. */
|
|
64
|
+
readonly received: bigint;
|
|
65
|
+
}> {
|
|
66
|
+
}
|
|
67
|
+
export type WalletError = OtherWalletError | SyncWalletError | TransactingError | InsufficientFundsError | TransactionHistoryError | OutOfOrderSyncUpdateError | LedgerOps.LedgerError;
|
|
43
68
|
export {};
|
package/dist/v2/WalletError.js
CHANGED
|
@@ -21,3 +21,19 @@ export class InsufficientFundsError extends Data.TaggedError('Wallet.Insufficien
|
|
|
21
21
|
}
|
|
22
22
|
export class TransactionHistoryError extends Data.TaggedError('Wallet.TransactionHistory') {
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A batch of dust ledger events was not in ascending id order: an event arrived at or below the one before it, or at or
|
|
26
|
+
* below the applied cursor.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Order is all the wallet can check: dust events share one id sequence with zswap and contract events in the indexer,
|
|
30
|
+
* so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
|
|
31
|
+
* whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
|
|
32
|
+
* place.
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class OutOfOrderSyncUpdateError extends Data.TaggedError('Wallet.OutOfOrderSyncUpdate') {
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midnight-ntwrk/wallet-sdk-dust-wallet",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@midnight-ntwrk/ledger-v8": "^8.1.0",
|
|
38
|
-
"@midnightntwrk/ledger-v9": "1.0.0-rc.
|
|
39
|
-
"@midnight-ntwrk/wallet-sdk-abstractions": "3.0.0-beta.
|
|
40
|
-
"@midnight-ntwrk/wallet-sdk-address-format": "4.0.0-beta.
|
|
41
|
-
"@midnight-ntwrk/wallet-sdk-capabilities": "4.0.0-beta.
|
|
38
|
+
"@midnightntwrk/ledger-v9": "1.0.0-rc.5",
|
|
39
|
+
"@midnight-ntwrk/wallet-sdk-abstractions": "3.0.0-beta.2",
|
|
40
|
+
"@midnight-ntwrk/wallet-sdk-address-format": "4.0.0-beta.4",
|
|
41
|
+
"@midnight-ntwrk/wallet-sdk-capabilities": "4.0.0-beta.4",
|
|
42
42
|
"@midnight-ntwrk/wallet-sdk-indexer-client": "2.0.0-beta.2",
|
|
43
|
-
"@midnight-ntwrk/wallet-sdk-runtime": "1.1.0-beta.
|
|
43
|
+
"@midnight-ntwrk/wallet-sdk-runtime": "1.1.0-beta.2",
|
|
44
44
|
"@midnight-ntwrk/wallet-sdk-utilities": "1.2.2-beta.0",
|
|
45
45
|
"effect": "^3.19.19",
|
|
46
46
|
"rxjs": "^7.8.2"
|
|
@@ -59,6 +59,6 @@
|
|
|
59
59
|
"publint": "publint --strict"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@midnight-ntwrk/wallet-sdk-hd": "^3.1.0-beta.
|
|
62
|
+
"@midnight-ntwrk/wallet-sdk-hd": "^3.1.0-beta.3"
|
|
63
63
|
}
|
|
64
64
|
}
|