@midnight-ntwrk/wallet-sdk-shielded 4.0.0-beta.2 → 4.0.0-beta.3

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.
Files changed (60) hide show
  1. package/README.md +11 -6
  2. package/dist/Restore.d.ts +40 -0
  3. package/dist/Restore.js +63 -0
  4. package/dist/ShieldedWallet.d.ts +248 -62
  5. package/dist/ShieldedWallet.js +366 -76
  6. package/dist/ShieldedWalletAPI.d.ts +179 -0
  7. package/dist/ShieldedWalletAPI.js +75 -0
  8. package/dist/SingleVariantShieldedWallet.d.ts +35 -0
  9. package/dist/SingleVariantShieldedWallet.js +240 -0
  10. package/dist/index.d.ts +4 -1
  11. package/dist/index.js +4 -1
  12. package/dist/v1/CoinsAndBalances.d.ts +1 -1
  13. package/dist/v1/CoreWallet.d.ts +48 -1
  14. package/dist/v1/CoreWallet.js +54 -1
  15. package/dist/v1/Migration.d.ts +95 -0
  16. package/dist/v1/Migration.js +82 -0
  17. package/dist/v1/RunningV1Variant.d.ts +1 -1
  18. package/dist/v1/RunningV1Variant.js +37 -8
  19. package/dist/v1/Serialization.js +1 -1
  20. package/dist/v1/Sync.d.ts +132 -9
  21. package/dist/v1/Sync.js +244 -35
  22. package/dist/v1/Transacting.d.ts +1 -1
  23. package/dist/v1/Transacting.js +1 -1
  24. package/dist/v1/TransactionHistory.d.ts +9 -2
  25. package/dist/v1/TransactionHistory.js +26 -5
  26. package/dist/v1/TransactionOps.d.ts +1 -1
  27. package/dist/v1/V1Builder.d.ts +87 -26
  28. package/dist/v1/V1Builder.js +109 -8
  29. package/dist/v1/WalletError.d.ts +1 -1
  30. package/dist/v1/index.d.ts +2 -1
  31. package/dist/v1/index.js +5 -1
  32. package/dist/v2/CoinsAndBalances.d.ts +24 -0
  33. package/dist/v2/CoinsAndBalances.js +41 -0
  34. package/dist/v2/CoreWallet.d.ts +162 -0
  35. package/dist/v2/CoreWallet.js +305 -0
  36. package/dist/v2/Keys.d.ts +8 -0
  37. package/dist/v2/Keys.js +16 -0
  38. package/dist/v2/Migration.d.ts +112 -0
  39. package/dist/v2/Migration.js +96 -0
  40. package/dist/v2/RunningV2Variant.d.ts +40 -0
  41. package/dist/v2/RunningV2Variant.js +127 -0
  42. package/dist/v2/Serialization.d.ts +12 -0
  43. package/dist/v2/Serialization.js +93 -0
  44. package/dist/v2/Sync.d.ts +208 -0
  45. package/dist/v2/Sync.js +436 -0
  46. package/dist/v2/Transacting.d.ts +50 -0
  47. package/dist/v2/Transacting.js +258 -0
  48. package/dist/v2/TransactionHistory.d.ts +100 -0
  49. package/dist/v2/TransactionHistory.js +132 -0
  50. package/dist/v2/TransactionImbalances.d.ts +10 -0
  51. package/dist/v2/TransactionImbalances.js +30 -0
  52. package/dist/v2/TransactionOps.d.ts +17 -0
  53. package/dist/v2/TransactionOps.js +73 -0
  54. package/dist/v2/V2Builder.d.ts +154 -0
  55. package/dist/v2/V2Builder.js +273 -0
  56. package/dist/v2/WalletError.d.ts +74 -0
  57. package/dist/v2/WalletError.js +53 -0
  58. package/dist/v2/index.d.ts +13 -0
  59. package/dist/v2/index.js +26 -0
  60. package/package.json +16 -8
@@ -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 * as ledger from '@midnightntwrk/ledger-v9';
13
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
14
14
  import { ProtocolVersion, SyncProgress } from '@midnight-ntwrk/wallet-sdk-abstractions';
15
15
  import { Either, Iterable, pipe, Record, Array as Arr } from 'effect';
16
16
  import { InvalidCoinHashesError } from './WalletError.js';
@@ -97,6 +97,59 @@ export const CoreWallet = {
97
97
  initEmpty(keys, networkId) {
98
98
  return this.empty(PublicKeys.fromSecretKeys(keys), networkId);
99
99
  },
100
+ /**
101
+ * Records an observed protocol version on the wallet, never going backwards.
102
+ *
103
+ * @remarks
104
+ * The version is a signal, not a measurement: writing one outside the running variant's activation range is what
105
+ * makes it hand over to the next variant. A source that briefly reports an older version — a reconnect replaying
106
+ * from an earlier cursor, say — must therefore not be able to drag the wallet back below a boundary it has already
107
+ * crossed, which would ask the runtime to migrate backwards. Taking the maximum makes that unrepresentable.
108
+ * @param wallet The wallet to annotate.
109
+ * @param version The protocol version just observed.
110
+ * @returns `wallet` unchanged if it already records `version` or a later one, otherwise a copy recording `version`.
111
+ */
112
+ withProtocolVersion(wallet, version) {
113
+ return version > wallet.protocolVersion ? { ...wallet, protocolVersion: version } : wallet;
114
+ },
115
+ /**
116
+ * Projects a wallet inherited from the previous ledger version onto a fresh state of this one.
117
+ *
118
+ * @remarks
119
+ * No coin data crosses the boundary here, and that is a decision rather than an oversight. This is the oldest variant
120
+ * the wallet registers and no ledger version below it exists, so nothing ever hands a state to this projection: it
121
+ * is shape parity with the twin at `src/v2`, which is the variant a real crossing lands in. That twin adopts the
122
+ * previous wallet's local state whole, by deserializing its bytes — the two ledger majors share the
123
+ * `zswap-local-state` codec — because the chain's state translation continues the commitment tree across the fork
124
+ * and the indexer re-emits none of the ledger-v8 timeline (see `src/v2/Migration.ts`). Mirroring that into a seam
125
+ * no chain can reach would buy nothing, so this side stays as it is.
126
+ *
127
+ * What crosses is therefore identity and position: the public keys — which decide whose coins the far side can
128
+ * decrypt — the network, the protocol version that triggered the hand-over, kept so the new variant starts inside
129
+ * its own activation range rather than immediately signalling backwards, and the cursor the previous variant
130
+ * stopped at.
131
+ *
132
+ * **Sync progress is parked at the fork, not rewound**: the previous wallet's cursor crosses unchanged. A fork does
133
+ * not restart the timeline — event ids run onwards from whatever the indexer had reached when it happened, never
134
+ * from zero — so resuming from the inherited cursor is what puts this wallet in front of what comes next. Rewinding
135
+ * to zero would park it on a stretch of history that this ledger version's events do not occupy. What does not
136
+ * cross is `isConnected`: no sync is running behind this state yet.
137
+ *
138
+ * Coin hashes start empty for the same reason the tree does: they are commitments and nullifiers computed under the
139
+ * previous ledger's codec, and this version recomputes its own from the keys and whatever state it ends up with.
140
+ * @param previous The plain data read off the previous ledger version's wallet.
141
+ * @returns A wallet of this ledger version holding no coins, positioned at the fork.
142
+ */
143
+ fromPreviousVersion(previous) {
144
+ return {
145
+ state: new ledger.ZswapLocalState(),
146
+ publicKeys: previous.publicKeys,
147
+ networkId: previous.networkId,
148
+ coinHashes: CoinHashesMap.empty,
149
+ progress: SyncProgress.createSyncProgress({ ...previous.progress, isConnected: false }),
150
+ protocolVersion: ProtocolVersion.ProtocolVersion(previous.protocolVersion),
151
+ };
152
+ },
100
153
  applyCollapsedUpdate(wallet, collapsed) {
101
154
  const newState = wallet.state.applyCollapsedUpdate(collapsed);
102
155
  return { ...wallet, state: newState };
@@ -0,0 +1,95 @@
1
+ import { type NetworkId, type SyncProgress } from '@midnight-ntwrk/wallet-sdk-abstractions';
2
+ import { Effect } from 'effect';
3
+ import { CoreWallet } from './CoreWallet.js';
4
+ import { type WalletError } from './WalletError.js';
5
+ /**
6
+ * How this variant produces its first state from whatever came before it.
7
+ *
8
+ * @remarks
9
+ * The strategy is injected through the builder rather than hard-coded into the variant, because "whatever came before"
10
+ * is not one thing: an empty wallet at first start, a wallet of this same ledger version, or a wallet of the ledger
11
+ * version this one replaced. Those are different functions with different input types, and the runtime only ever
12
+ * needs one of them per registration — so which one is a build-time decision, expressed as a dictionary in the
13
+ * repository's usual dictionary-passing style.
14
+ * @typeParam TPreviousState The state this migration consumes.
15
+ */
16
+ export type StateMigration<TPreviousState> = {
17
+ migrate: (previousState: TPreviousState) => Effect.Effect<CoreWallet, WalletError>;
18
+ };
19
+ export type EmptyWalletMigrationConfiguration = {
20
+ networkId: NetworkId.NetworkId;
21
+ };
22
+ /**
23
+ * The migration used when there is no previous wallet at all.
24
+ *
25
+ * @remarks
26
+ * It backs `WalletLike.startEmpty`, which builds a wallet before any key material exists. The resulting state has no
27
+ * coins and public keys derived from a fixed placeholder seed; it is a scaffold to be replaced by a real start, not a
28
+ * wallet anybody can transact with. Preserved verbatim from the ledger-v8 implementation so that `startEmpty` behaves
29
+ * exactly as it did.
30
+ * @example
31
+ * ```typescript
32
+ * const builder = new V2Builder().withDefaults().withMigration(makeEmptyWalletMigration({ networkId }));
33
+ * ```;
34
+ *
35
+ * @param configuration Supplies the network the scaffold state claims to be on.
36
+ * @returns A migration from `null`.
37
+ */
38
+ export declare const makeEmptyWalletMigration: (configuration: EmptyWalletMigrationConfiguration) => StateMigration<null>;
39
+ /**
40
+ * The migration between two variants that share this ledger version.
41
+ *
42
+ * @remarks
43
+ * Both sides speak the same state type, so the carry is the identity: local Merkle tree, coins, coin hashes and sync
44
+ * progress all remain valid, and nothing has to be re-read. This is the right strategy for a protocol bump that does
45
+ * not change serialization.
46
+ * @returns A migration from a {@link CoreWallet} of this ledger version.
47
+ */
48
+ export declare const makeCarryOverMigration: () => StateMigration<CoreWallet>;
49
+ /**
50
+ * The shape a wallet of the _previous_ ledger version must expose to be projected across a hard fork.
51
+ *
52
+ * @remarks
53
+ * Structural on purpose. The previous variant's `CoreWallet` is built on a different ledger module, and naming that
54
+ * module here would drag a second multi-megabyte WASM binary into this variant for the sake of a type. Everything the
55
+ * projection reads is plain data — the ledger's key and network types are all string aliases — so a structural
56
+ * description is both sufficient and the only version-agnostic option.
57
+ *
58
+ * `progress` is where a migrated wallet resumes from: event ids continue across a fork rather than restarting, so the
59
+ * previous variant's cursor is the position the next one has to start at. Nothing ever fills this shape in practice —
60
+ * see {@link makeCrossLedgerMigration} for why this variant is never on the receiving end of a crossing.
61
+ */
62
+ export type PreviousLedgerWallet = Readonly<{
63
+ publicKeys: {
64
+ readonly coinPublicKey: string;
65
+ readonly encryptionPublicKey: string;
66
+ };
67
+ networkId: string;
68
+ protocolVersion: bigint;
69
+ progress: SyncProgress.SyncProgressData;
70
+ }>;
71
+ /**
72
+ * The migration across a ledger-version boundary: for this variant, shape parity and nothing else.
73
+ *
74
+ * @remarks
75
+ * This is the oldest variant the wallet registers, and no ledger version below the one it is built on exists to be
76
+ * registered under it. Nothing can therefore ever hand a state _to_ here: this seam is never exercised by a real
77
+ * chain. It exists because both twins declare the same builder surface, and a builder that could not name a
78
+ * cross-ledger migration would be a different type on each side of the fork.
79
+ *
80
+ * The twin at `src/v2` is where a crossing actually happens, and it does something this one deliberately does not
81
+ * mirror. The chain's state translation carries every commitment across the fork in place — the ledger-v9 tree
82
+ * continues at the index the ledger-v8 tree reached, and the indexer re-emits none of the ledger-v8 timeline — so a
83
+ * wallet that started coinless there would simply lose its coins. That migration therefore reads the previous
84
+ * wallet's local state across whole, by handing its serialization to this ledger version's deserializer: the two
85
+ * majors either side of that boundary share the `zswap-local-state` codec (see `src/v2/Migration.ts` and
86
+ * `src/v2/test/byteCrossing.test.ts`). Porting that back here would be machinery for a case that cannot arise, so it
87
+ * stays out: a deliberate, permanent exclusion of the kind the twin convention allows for, not a gap left to close.
88
+ *
89
+ * What crosses here is public keys, the network, the protocol version that triggered the hand-over, and the cursor.
90
+ * Sync progress is **parked at the fork** rather than rewound, on the rule that outlives the design change: event ids
91
+ * continue across a fork rather than restarting from zero, so the inherited cursor is exactly where reading resumes.
92
+ * A wallet that rewound to zero would wait on a stretch of the timeline this ledger version's events do not occupy.
93
+ * @returns A migration from a previous-ledger-version wallet.
94
+ */
95
+ export declare const makeCrossLedgerMigration: () => StateMigration<PreviousLedgerWallet>;
@@ -0,0 +1,82 @@
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 * as ledger from '@midnight-ntwrk/ledger-v8';
14
+ import { WalletSeed } from '@midnight-ntwrk/wallet-sdk-abstractions';
15
+ import { Effect } from 'effect';
16
+ import { CoreWallet, PublicKeys } from './CoreWallet.js';
17
+ /**
18
+ * The migration used when there is no previous wallet at all.
19
+ *
20
+ * @remarks
21
+ * It backs `WalletLike.startEmpty`, which builds a wallet before any key material exists. The resulting state has no
22
+ * coins and public keys derived from a fixed placeholder seed; it is a scaffold to be replaced by a real start, not a
23
+ * wallet anybody can transact with. Preserved verbatim from the ledger-v8 implementation so that `startEmpty` behaves
24
+ * exactly as it did.
25
+ * @example
26
+ * ```typescript
27
+ * const builder = new V2Builder().withDefaults().withMigration(makeEmptyWalletMigration({ networkId }));
28
+ * ```;
29
+ *
30
+ * @param configuration Supplies the network the scaffold state claims to be on.
31
+ * @returns A migration from `null`.
32
+ */
33
+ export const makeEmptyWalletMigration = (configuration) => ({
34
+ migrate: () => {
35
+ const seed = WalletSeed.fromString('0000000000000000000000000000000000000000000000000000000000000001');
36
+ return Effect.succeed(CoreWallet.empty(PublicKeys.fromSecretKeys(ledger.ZswapSecretKeys.fromSeed(seed)), configuration.networkId));
37
+ },
38
+ });
39
+ /**
40
+ * The migration between two variants that share this ledger version.
41
+ *
42
+ * @remarks
43
+ * Both sides speak the same state type, so the carry is the identity: local Merkle tree, coins, coin hashes and sync
44
+ * progress all remain valid, and nothing has to be re-read. This is the right strategy for a protocol bump that does
45
+ * not change serialization.
46
+ * @returns A migration from a {@link CoreWallet} of this ledger version.
47
+ */
48
+ export const makeCarryOverMigration = () => ({
49
+ migrate: (previousState) => Effect.succeed(previousState),
50
+ });
51
+ /**
52
+ * The migration across a ledger-version boundary: for this variant, shape parity and nothing else.
53
+ *
54
+ * @remarks
55
+ * This is the oldest variant the wallet registers, and no ledger version below the one it is built on exists to be
56
+ * registered under it. Nothing can therefore ever hand a state _to_ here: this seam is never exercised by a real
57
+ * chain. It exists because both twins declare the same builder surface, and a builder that could not name a
58
+ * cross-ledger migration would be a different type on each side of the fork.
59
+ *
60
+ * The twin at `src/v2` is where a crossing actually happens, and it does something this one deliberately does not
61
+ * mirror. The chain's state translation carries every commitment across the fork in place — the ledger-v9 tree
62
+ * continues at the index the ledger-v8 tree reached, and the indexer re-emits none of the ledger-v8 timeline — so a
63
+ * wallet that started coinless there would simply lose its coins. That migration therefore reads the previous
64
+ * wallet's local state across whole, by handing its serialization to this ledger version's deserializer: the two
65
+ * majors either side of that boundary share the `zswap-local-state` codec (see `src/v2/Migration.ts` and
66
+ * `src/v2/test/byteCrossing.test.ts`). Porting that back here would be machinery for a case that cannot arise, so it
67
+ * stays out: a deliberate, permanent exclusion of the kind the twin convention allows for, not a gap left to close.
68
+ *
69
+ * What crosses here is public keys, the network, the protocol version that triggered the hand-over, and the cursor.
70
+ * Sync progress is **parked at the fork** rather than rewound, on the rule that outlives the design change: event ids
71
+ * continue across a fork rather than restarting from zero, so the inherited cursor is exactly where reading resumes.
72
+ * A wallet that rewound to zero would wait on a stretch of the timeline this ledger version's events do not occupy.
73
+ * @returns A migration from a previous-ledger-version wallet.
74
+ */
75
+ export const makeCrossLedgerMigration = () => ({
76
+ migrate: (previousState) => Effect.succeed(CoreWallet.fromPreviousVersion({
77
+ publicKeys: previousState.publicKeys,
78
+ networkId: previousState.networkId,
79
+ protocolVersion: previousState.protocolVersion,
80
+ progress: previousState.progress,
81
+ })),
82
+ });
@@ -1,4 +1,4 @@
1
- import type * as ledger from '@midnightntwrk/ledger-v9';
1
+ import type * as ledger from '@midnight-ntwrk/ledger-v8';
2
2
  import { Effect, Scope, Stream } from 'effect';
3
3
  import { type WalletRuntimeError, type Variant, StateChange } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
4
4
  import { type SerializationCapability } from './Serialization.js';
@@ -12,8 +12,23 @@ const progress = (state) => {
12
12
  const applyGap = highestRelevantWalletIndex - appliedIndex;
13
13
  return [StateChange.ProgressUpdate({ sourceGap, applyGap })];
14
14
  };
15
- const protocolVersionChange = (previous, current) => {
16
- return previous.protocolVersion != current.protocolVersion
15
+ /**
16
+ * Reports the protocol version a state observation implies, if any.
17
+ *
18
+ * @remarks
19
+ * Two situations call for a signal. The ordinary one is a transition: sync annotated the state with a version it did
20
+ * not have before, and the runtime decides whether that is a re-annotation inside this variant's range or a hand-over
21
+ * to the next one.
22
+ *
23
+ * The other is a state that arrives already outside this variant's range — a snapshot serialized in the window between
24
+ * the annotation and the migration it should have caused, or one written by an older protocol version entirely.
25
+ * Nothing further will change that value, so waiting for a transition would strand the wallet on a version it does
26
+ * not own. Emitting on sight heals it.
27
+ */
28
+ const protocolVersionChange = (previous, current, isInitial, activationRange) => {
29
+ const transitioned = previous.protocolVersion != current.protocolVersion;
30
+ const strandedOutsideRange = isInitial && !ProtocolVersion.withinRange(current.protocolVersion, activationRange);
31
+ return transitioned || strandedOutsideRange
17
32
  ? [
18
33
  StateChange.VersionChange({
19
34
  change: VersionChangeType.Version({
@@ -29,19 +44,25 @@ export class RunningV1Variant {
29
44
  #scope;
30
45
  #context;
31
46
  #v1Context;
47
+ // Variant-wide cap on concurrent tx-history lookups. Each sync batch forks its own fan-out fiber, so a per-batch
48
+ // `concurrency` limit alone would let in-flight indexer queries grow with the number of live batches while their
49
+ // lookups retry through the indexer-lag window. Every lookup acquires a permit here, making the cap global.
50
+ #txHistoryPermits = Effect.makeSemaphore(8).pipe(Effect.runSync);
32
51
  state;
33
52
  constructor(scope, context, v1Context) {
34
53
  this.#scope = scope;
35
54
  this.#context = context;
36
55
  this.#v1Context = v1Context;
37
- this.state = Stream.fromEffect(context.stateRef.get).pipe(Stream.flatMap((initialState) => context.stateRef.changes.pipe(Stream.mapAccum(initialState, (previous, current) => {
38
- return [current, [previous, current]];
39
- }))), Stream.mapConcat(([previous, current]) => {
56
+ this.state = Stream.fromEffect(context.stateRef.get).pipe(Stream.flatMap((initialState) => context.stateRef.changes.pipe(
57
+ // The accumulator carries the "have we seen anything yet" flag alongside the previous state: the first
58
+ // observation is the only one that can be a restored state nobody has inspected against this variant's
59
+ // range yet, and `SubscriptionRef.changes` replays the current value, so it is exactly this element.
60
+ Stream.mapAccum({ previous: initialState, isInitial: true }, (seen, current) => [{ previous: current, isInitial: false }, [seen.previous, current, seen.isInitial]]))), Stream.mapConcat(([previous, current, isInitial]) => {
40
61
  // TODO: emit progress only upon actual change
41
62
  return [
42
63
  StateChange.State({ state: current }),
43
64
  ...progress(current),
44
- ...protocolVersionChange(previous, current),
65
+ ...protocolVersionChange(previous, current, isInitial, context.activationRange),
45
66
  ];
46
67
  }));
47
68
  }
@@ -51,7 +72,7 @@ export class RunningV1Variant {
51
72
  startSync(startAux) {
52
73
  return pipe(SubscriptionRef.get(this.#context.stateRef), Stream.fromEffect, Stream.flatMap((state) => this.#v1Context.syncService.updates(state, startAux)), Stream.mapEffect((update) => SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => Effect.try({
53
74
  try: () => {
54
- const [newState, changesResult] = this.#v1Context.syncCapability.applyUpdate(state, update);
75
+ const [newState, changesResult] = this.#v1Context.syncCapability.applyUpdate(state, update, this.#context.activationRange);
55
76
  return [changesResult, newState];
56
77
  },
57
78
  catch: (err) => new OtherWalletError({
@@ -65,7 +86,15 @@ export class RunningV1Variant {
65
86
  // which adds up fast during catch-up.
66
87
  changes.length === 0
67
88
  ? Effect.void
68
- : pipe(Effect.forEach(changes, (change) => pipe(this.#v1Context.transactionHistoryService.getTransactionDetails(change.source), Effect.flatMap((metadata) => this.#v1Context.transactionHistoryService.put(change, metadata, protocolVersion)), Effect.catchAllCause((cause) => Console.error('Error processing tx history metadata', cause))), { discard: true, concurrency: 'unbounded' }), Effect.forkScoped)), Effect.provideService(Scope.Scope, this.#scope))), Stream.tapError((error) => Console.error(error)), Stream.retry(pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.map((delay) => {
89
+ : pipe(Effect.forEach(changes, (change) => pipe(this.#txHistoryPermits.withPermits(1)(pipe(this.#v1Context.transactionHistoryService.getTransactionDetails(change.source), Effect.flatMap((metadata) => this.#v1Context.transactionHistoryService.put(change, metadata, protocolVersion)))), Effect.catchAllCause((cause) =>
90
+ // A sustained indexer outage (longer than getTransactionDetails' retry window) still lands
91
+ // here. applyUpdate has already advanced appliedIndex, so this change.source won't be
92
+ // re-processed — the shielded section is permanently lost. Surface that as a structured
93
+ // error carrying the tx hash, not a silent Console.error defect.
94
+ Effect.logError(cause, `Failed to record shielded tx-history section for ${change.source}`))),
95
+ // `concurrency` only bounds fiber creation within this one batch; the real cap on simultaneous
96
+ // indexer queries is the variant-wide semaphore acquired around each lookup above.
97
+ { discard: true, concurrency: 8 }), Effect.forkScoped)), Effect.provideService(Scope.Scope, this.#scope))), Stream.tapError((error) => Console.error(error)), Stream.retry(pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.map((delay) => {
69
98
  const maxDelay = Duration.minutes(2);
70
99
  const jitter = Duration.millis(Math.floor(Math.random() * 1000));
71
100
  const delayWithJitter = Duration.toMillis(delay) + Duration.toMillis(jitter);
@@ -12,7 +12,7 @@
12
12
  // limitations under the License.
13
13
  import { Effect, ParseResult, Either, pipe, Schema } from 'effect';
14
14
  import { WalletError } from './WalletError.js';
15
- import * as ledger from '@midnightntwrk/ledger-v9';
15
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
16
16
  import { CoreWallet } from './CoreWallet.js';
17
17
  const StateSchema = Schema.declare((input) => input instanceof ledger.ZswapLocalState).annotations({
18
18
  identifier: 'ledger.ZswapLocalState',
package/dist/v1/Sync.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import * as ledger from '@midnightntwrk/ledger-v9';
2
- import { Schema, type Scope, Stream } from 'effect';
1
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
2
+ import { Option, Schema, type Scope, Stream } from 'effect';
3
+ import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
3
4
  import { CoreWallet } from './CoreWallet.js';
4
- import { type Simulator, type SimulatorState } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
5
+ import { V8 } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
5
6
  import { type WalletError } from './WalletError.js';
6
7
  import { type TransactionHistoryService } from './TransactionHistory.js';
7
8
  export interface SyncService<TState, TStartAux, TUpdate> {
@@ -12,8 +13,46 @@ export type ChangesResult = {
12
13
  readonly protocolVersion: number;
13
14
  };
14
15
  export interface SyncCapability<TState, TUpdate, TResult> {
15
- applyUpdate: (state: TState, update: TUpdate) => [TState, TResult];
16
+ /**
17
+ * Folds a sync update into the wallet state.
18
+ *
19
+ * @param state The state to fold into.
20
+ * @param update The update to apply.
21
+ * @param activeRange The half-open protocol version range the running variant owns. Anything the source reports at or
22
+ * beyond its end belongs to a later variant and must be left unapplied for that variant to fetch.
23
+ */
24
+ applyUpdate: (state: TState, update: TUpdate, activeRange: ProtocolVersion.ProtocolVersion.Range) => [TState, TResult];
16
25
  }
26
+ /** The result of splitting a batch of version-tagged items at a variant's activation boundary. */
27
+ export type BoundarySplit<T> = {
28
+ /** The leading items this variant owns, in source order. */
29
+ readonly applied: readonly T[];
30
+ /** The trailing items belonging to a later protocol version, left for the next variant to re-fetch. */
31
+ readonly deferred: readonly T[];
32
+ /**
33
+ * The protocol version to record on the state: the first deferred item's version when there is one — that is the
34
+ * signal that triggers migration — otherwise the last applied item's version. `None` for an empty batch.
35
+ */
36
+ readonly observedVersion: Option.Option<ProtocolVersion.ProtocolVersion>;
37
+ };
38
+ /**
39
+ * Splits a batch of version-tagged items at the end of a variant's activation range.
40
+ *
41
+ * @remarks
42
+ * The split is positional, not a filter: everything from the first out-of-range item onwards is deferred, even if a
43
+ * later item reports an in-range version again. A batch is a contiguous slice of one timeline, so applying past a
44
+ * boundary and then resuming behind it would leave a hole no cursor can describe.
45
+ *
46
+ * This is the one place the boundary rule lives; both the indexer capability (per event) and the simulator capability
47
+ * (per block) go through it, so they cannot drift apart.
48
+ * @param items The batch, in source order.
49
+ * @param versionOf Reads the protocol version an item was reported at.
50
+ * @param activeRange The variant's half-open activation range.
51
+ * @returns The applied/deferred split and the version to record.
52
+ */
53
+ export declare const splitAtVersionBoundary: <T>(items: readonly T[], versionOf: (item: T) => number, activeRange: ProtocolVersion.ProtocolVersion.Range) => BoundarySplit<T>;
54
+ /** Records a batch's observed protocol version on the state, monotonically. A batch that observed none is a no-op. */
55
+ export declare const annotateVersion: (state: CoreWallet, observedVersion: Option.Option<ProtocolVersion.ProtocolVersion>) => CoreWallet;
17
56
  export type IndexerClientConnection = {
18
57
  indexerHttpUrl: string;
19
58
  indexerWsUrl?: string;
@@ -45,9 +84,32 @@ export type BatchUpdatesConfig = {
45
84
  */
46
85
  readonly spacing?: number;
47
86
  };
87
+ /**
88
+ * How often the sync source re-asks the chain which protocol version it is on.
89
+ *
90
+ * @remarks
91
+ * The event subscription this source reads has no progress arm: it says nothing at all when there is nothing to say. A
92
+ * wallet therefore observes the chain's protocol version only through events, and on a chain that crosses a protocol
93
+ * boundary and then produces no shielded traffic it observes no version at all — it stays on the variant it was
94
+ * running, and everything built through it stays routed to that variant's ledger. Asking on a timer is what closes
95
+ * that, and the cost of asking is one small query per interval on a chain that has not moved.
96
+ *
97
+ * This variant is the ledger-v8 one, so it is the one that actually crosses: a wallet stranded here is a wallet still
98
+ * building transactions for a ledger version the chain has left behind.
99
+ */
100
+ export type VersionWatchConfig = {
101
+ /**
102
+ * Milliseconds between checks. Zero or less turns the watcher off entirely, which is what a source driving a wallet
103
+ * from something other than a live chain wants.
104
+ *
105
+ * @default 30000
106
+ */
107
+ readonly intervalMs?: number;
108
+ };
48
109
  export type DefaultSyncConfiguration = {
49
110
  indexerClientConnection: IndexerClientConnection;
50
111
  batchUpdates?: BatchUpdatesConfig;
112
+ versionWatch?: VersionWatchConfig;
51
113
  };
52
114
  export type DefaultSyncContext = {
53
115
  transactionHistoryService: TransactionHistoryService;
@@ -56,27 +118,88 @@ export type SecretKeysResource = <A>(cb: (keys: ledger.ZswapSecretKeys) => A) =>
56
118
  export declare const SecretKeysResource: {
57
119
  create: (secretKeys: ledger.ZswapSecretKeys) => SecretKeysResource;
58
120
  };
59
- export type WalletSyncUpdate = {
121
+ /** The ordinary arm of {@link WalletSyncUpdate}: a batch of the indexer's event timeline, still encoded. */
122
+ export type EventsWalletSyncUpdate = {
123
+ _tag: 'Events';
60
124
  updates: EventsSyncUpdate[];
61
125
  secretKeys: ledger.ZswapSecretKeys;
62
126
  };
127
+ /**
128
+ * What the chain says about itself when its timeline says nothing.
129
+ *
130
+ * @remarks
131
+ * An observation, not a piece of the chain: it moves no cursor, inserts nothing and produces no changes. All it can do
132
+ * is record a protocol version, which is enough, because recording one outside the running variant's activation range
133
+ * is exactly what makes the runtime hand over.
134
+ *
135
+ * `highestEventId` is what makes the record safe to make. Handing over parks the sync cursor where it stands, and the
136
+ * variant that takes over re-fetches from there — so an event still unread below the source's tip would reach it as
137
+ * bytes of the version that preceded it, which its ledger cannot deserialize, and which may in any case be carrying a
138
+ * coin that would then never reach the far side. The signal therefore travels with the far end of the source's event
139
+ * timeline, so the capability can refuse it while anything remains unread. `null` means the source provably holds no
140
+ * zswap event at all, which is the one case where nothing can be unread.
141
+ */
142
+ export type VersionSignalSyncUpdate = Readonly<{
143
+ _tag: 'VersionSignal';
144
+ /** The protocol version the source's tip was reported under. */
145
+ version: number;
146
+ /** The highest event id the source holds, or `null` when it provably holds no event. */
147
+ highestEventId: number | null;
148
+ }>;
149
+ export declare const VersionSignalSyncUpdate: {
150
+ create: (version: number, highestEventId: number | null) => VersionSignalSyncUpdate;
151
+ };
152
+ /**
153
+ * What the indexer-backed sync source emits.
154
+ *
155
+ * @remarks
156
+ * Ordinarily a batch of events; and, on a timer, what the chain says about its own version when its timeline says
157
+ * nothing.
158
+ */
159
+ export type WalletSyncUpdate = EventsWalletSyncUpdate | VersionSignalSyncUpdate;
63
160
  export declare const WalletSyncUpdate: {
64
- create: (updates: EventsSyncUpdate[], secretKeys: ledger.ZswapSecretKeys) => WalletSyncUpdate;
161
+ create: (updates: EventsSyncUpdate[], secretKeys: ledger.ZswapSecretKeys) => EventsWalletSyncUpdate;
65
162
  };
163
+ /**
164
+ * One event of the source's timeline, still encoded.
165
+ *
166
+ * @remarks
167
+ * The event is carried as the source served it rather than as a `ledger.Event`, because whether this ledger version may
168
+ * read it at all is not the source's question to answer. A batch spanning a protocol boundary carries events of the
169
+ * version that follows this one, which this ledger version cannot deserialize — the serialization header names a
170
+ * different version — and after a hand-over the inclusive cursor re-delivers an event of the version that preceded
171
+ * it, which it equally cannot. Both are ordinary, and neither is an error: they belong to the variant either side.
172
+ *
173
+ * So the bytes travel undecoded and only the capability, which knows the activation range, reads the ones it is about
174
+ * to apply — see {@link readEvent}. Decoding here would fail the whole batch on an event nobody intended to apply.
175
+ */
66
176
  export declare const EventsSyncUpdate: Schema.TaggedStruct<"EventsSyncUpdate", {
67
177
  id: typeof Schema.Number;
68
178
  protocolVersion: typeof Schema.Number;
69
179
  maxId: typeof Schema.Number;
70
- event: Schema.declare<ledger.Event, ledger.Event, readonly [], never>;
180
+ raw: typeof Schema.String;
71
181
  }>;
72
182
  export type EventsSyncUpdate = Schema.Schema.Type<typeof EventsSyncUpdate>;
183
+ /**
184
+ * Reads an event this variant is going to apply.
185
+ *
186
+ * @remarks
187
+ * The counterpart of {@link EventsSyncUpdate} carrying its event encoded: a capability calls this on the batch prefix it
188
+ * owns, and never on what it defers. Failure here is a genuine one — an event this variant claimed and cannot read —
189
+ * and is raised rather than returned, because `SyncCapability.applyUpdate` is total in its own domain and the variant
190
+ * already turns a throw from it into a typed synchronization error.
191
+ * @param update The update to read.
192
+ * @returns The event it carries.
193
+ * @throws ParseError if the bytes are not an event this ledger version can deserialize.
194
+ */
195
+ export declare const readEvent: (update: EventsSyncUpdate) => ledger.Event;
73
196
  export declare const makeEventsSyncService: (config: DefaultSyncConfiguration) => SyncService<CoreWallet, ledger.ZswapSecretKeys, WalletSyncUpdate>;
74
197
  export declare const makeEventsSyncCapability: () => SyncCapability<CoreWallet, WalletSyncUpdate, ChangesResult>;
75
198
  export type SimulatorSyncConfiguration = {
76
- simulator: Simulator;
199
+ simulator: V8.Simulator;
77
200
  };
78
201
  export type SimulatorSyncUpdate = {
79
- update: SimulatorState;
202
+ update: V8.SimulatorState;
80
203
  secretKeys: ledger.ZswapSecretKeys;
81
204
  };
82
205
  export declare const makeSimulatorSyncService: (config: SimulatorSyncConfiguration) => SyncService<CoreWallet, ledger.ZswapSecretKeys, SimulatorSyncUpdate>;