@midnight-ntwrk/wallet-sdk-shielded 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,10 @@
1
+ > [!IMPORTANT]
2
+ > **This package has moved.** The `@midnight-ntwrk` scope is published only
3
+ > during the migration window and will stop receiving updates. Please migrate to
4
+ > [`@midnightntwrk/wallet-sdk-shielded`](https://www.npmjs.com/package/@midnightntwrk/wallet-sdk-shielded).
5
+
6
+ ---
7
+
1
8
  # @midnight-ntwrk/wallet-sdk-shielded
2
9
 
3
10
  Manages shielded tokens on the Midnight network using zero-knowledge proofs.
@@ -58,7 +58,14 @@ export class RunningV1Variant {
58
58
  message: 'Error while applying sync update',
59
59
  cause: err,
60
60
  }),
61
- })).pipe(Effect.flatMap(({ changes, protocolVersion }) => 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) => {
61
+ })).pipe(Effect.flatMap(({ changes, protocolVersion }) =>
62
+ // Skip the tx-history fork entirely when there are no changes.
63
+ // Forking unconditionally allocates a fiber per apply call (one
64
+ // for every batch the sync emits, even the all-progress ones),
65
+ // which adds up fast during catch-up.
66
+ changes.length === 0
67
+ ? 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) => {
62
69
  const maxDelay = Duration.minutes(2);
63
70
  const jitter = Duration.millis(Math.floor(Math.random() * 1000));
64
71
  const delayWithJitter = Duration.toMillis(delay) + Duration.toMillis(jitter);
package/dist/v1/Sync.d.ts CHANGED
@@ -18,6 +18,10 @@ export type IndexerClientConnection = {
18
18
  indexerHttpUrl: string;
19
19
  indexerWsUrl?: string;
20
20
  keepAlive?: number;
21
+ /** Cap on the in-flight event queue between the WebSocket push and the apply loop. Default: 10000. */
22
+ bufferSize?: number;
23
+ /** In-flight count at which the disposed WS subscription is reopened. Default: 100. */
24
+ resumeThreshold?: number;
21
25
  };
22
26
  export type BatchUpdatesConfig = {
23
27
  /**
package/dist/v1/Sync.js CHANGED
@@ -94,10 +94,36 @@ export const makeEventsSyncService = (config) => {
94
94
  }
95
95
  const indexerWsUrl = indexerWsUrlResult.right;
96
96
  const appliedIndex = state.progress?.appliedIndex ?? 0n;
97
+ // The boundary is load-bearing, not waste: this subscription emits only events (no tip/progress
98
+ // sentinel), and `isConnected`/the tip (`maxId`) are set only when an event is received. So the
99
+ // cursor must stay `<= appliedIndex` — never `appliedIndex + 1`. Requesting one event later would
100
+ // deliver nothing to a wallet already at the tip, so `applyUpdate` would never run and sync would
101
+ // hang.
102
+ //
103
+ // A fresh wallet has `appliedIndex === 0n` (the "nothing applied yet" sentinel), so `resumeFrom`
104
+ // is `-1n` and the `variables` mapping below opens the subscription with no `id` — the indexer
105
+ // streams from the very start. A restored wallet has `appliedIndex >= 1`, so `resumeFrom` is
106
+ // `appliedIndex - 1` and the inclusive cursor re-delivers the boundary event.
107
+ const resumeFrom = appliedIndex - 1n;
97
108
  const batchSize = config.batchUpdates?.size ?? 10;
98
109
  const batchTimeout = Duration.millis(config.batchUpdates?.timeout ?? 1);
99
110
  const batchSpacing = config.batchUpdates?.spacing ?? 4;
100
- const eventsStream = pipe(ZswapEvents.run({ id: Number(appliedIndex) }), Stream.provideLayer(WsSubscriptionClient.layer({ url: indexerWsUrl, keepAlive: config.indexerClientConnection.keepAlive })), Stream.mapError((error) => new SyncWalletError(error)), Stream.mapEffect((subscription) => pipe(subscription.zswapLedgerEvents, Schema.decodeUnknownEither(EventsSyncUpdateFromPayload), Either.mapLeft((err) => new SyncWalletError(err)), EitherOps.toEffect)), Stream.groupedWithin(batchSize, batchTimeout), Stream.map(Chunk.toArray), Stream.map((data) => WalletSyncUpdate.create(data, secretKeys)));
111
+ const bufferSize = config.indexerClientConnection.bufferSize ?? 10000;
112
+ const resumeThreshold = config.indexerClientConnection.resumeThreshold ?? 100;
113
+ const eventsStream = pipe(
114
+ // Backpressure caps the in-flight queue between the WS push and the
115
+ // apply loop. Without it the JS heap grows linearly with catch-up
116
+ // depth, since `Stream.asyncPush({ bufferSize: 'unbounded' })`
117
+ // buffers every event the indexer pushes regardless of apply rate.
118
+ ZswapEvents.runWithBackpressure({
119
+ bufferSize,
120
+ resumeThreshold,
121
+ from: resumeFrom,
122
+ // `resumeFrom < 0n` means a fresh wallet: send no `id` so the indexer streams from the very
123
+ // start, rather than relying on `id: 0` sorting below the first real event id.
124
+ variables: (cursor) => ({ id: cursor < 0n ? null : Number(cursor) }),
125
+ key: (r) => BigInt(r.zswapLedgerEvents.id),
126
+ }), Stream.provideLayer(WsSubscriptionClient.layer({ url: indexerWsUrl, keepAlive: config.indexerClientConnection.keepAlive })), Stream.mapError((error) => new SyncWalletError(error)), Stream.mapEffect((subscription) => pipe(subscription.zswapLedgerEvents, Schema.decodeUnknownEither(EventsSyncUpdateFromPayload), Either.mapLeft((err) => new SyncWalletError(err)), EitherOps.toEffect)), Stream.groupedWithin(batchSize, batchTimeout), Stream.map(Chunk.toArray), Stream.map((data) => WalletSyncUpdate.create(data, secretKeys)));
101
127
  return batchSpacing > 0
102
128
  ? Stream.schedule(eventsStream, Schedule.spaced(Duration.millis(batchSpacing)))
103
129
  : eventsStream;
@@ -110,24 +136,22 @@ export const makeEventsSyncCapability = () => {
110
136
  if (wrappedUpdate.updates.length === 0) {
111
137
  return [state, { changes: [], protocolVersion: Number(state.protocolVersion) }];
112
138
  }
139
+ // The subscription resumes at the last-applied index and its cursor is inclusive, so the
140
+ // boundary event (id === appliedIndex) is re-delivered. Replaying it would re-insert
141
+ // commitments already in the zswap state ("non-linear insertion" error), so only events
142
+ // strictly after the applied index are replayed. The boundary is still used (below) to
143
+ // refresh the tip and mark the wallet connected — that is how an already-caught-up wallet
144
+ // learns it is synced when no new events exist.
145
+ const appliedIndex = state.progress?.appliedIndex ?? 0n;
146
+ const freshUpdates = wrappedUpdate.updates.filter((u) => BigInt(u.id) > appliedIndex);
113
147
  const lastUpdate = wrappedUpdate.updates.at(-1);
114
- const nextIndex = BigInt(lastUpdate.id);
115
148
  const highestRelevantWalletIndex = BigInt(lastUpdate.maxId);
116
- // in case the nextIndex is less than or equal to the appliedIndex
117
- // just update highestRelevantWalletIndex
118
- if (nextIndex <= state.progress.appliedIndex) {
119
- return [
120
- CoreWallet.updateProgress(state, {
121
- highestRelevantWalletIndex,
122
- isConnected: true,
123
- }),
124
- { changes: [], protocolVersion: lastUpdate.protocolVersion },
125
- ];
126
- }
127
- const [newState, newChanges] = CoreWallet.replayEventsWithChanges(state, wrappedUpdate.secretKeys, wrappedUpdate.updates.map((u) => u.event));
149
+ const [newState, newChanges] = freshUpdates.length === 0
150
+ ? [state, []]
151
+ : CoreWallet.replayEventsWithChanges(state, wrappedUpdate.secretKeys, freshUpdates.map((u) => u.event));
128
152
  const updatedState = CoreWallet.updateProgress(newState, {
129
153
  highestRelevantWalletIndex,
130
- appliedIndex: nextIndex,
154
+ appliedIndex: freshUpdates.length === 0 ? appliedIndex : BigInt(freshUpdates.at(-1).id),
131
155
  isConnected: true,
132
156
  });
133
157
  return [updatedState, { changes: newChanges, protocolVersion: lastUpdate.protocolVersion }];
@@ -46,9 +46,7 @@ export class TransactingCapabilityImplementation {
46
46
  }
47
47
  const { newState: afterFallible, fallibleOffers } = yield* this.balanceFallibleSection(secretKeys, state, initialImbalances, coinSelection);
48
48
  const { newState: afterGuaranteed, offer: guaranteed } = yield* this.#balanceGuaranteedSection(secretKeys, afterFallible, initialImbalances, coinSelection, Imbalances.empty());
49
- const balancedTx = ledger.Transaction.fromParts(this.networkId, guaranteed);
50
- // workaround as Transaction.fromParts does not accept fallible offers with specific segments
51
- balancedTx.fallibleOffer = fallibleOffers.size > 0 ? fallibleOffers : undefined;
49
+ const balancedTx = Array.from(fallibleOffers.entries()).reduce((tx, [segment, offer]) => tx.addZswapOffer({ tag: 'specific', value: segment }, offer), ledger.Transaction.fromParts(this.networkId, guaranteed));
52
50
  return [balancedTx, afterGuaranteed];
53
51
  });
54
52
  }
@@ -193,10 +191,11 @@ export class TransactingCapabilityImplementation {
193
191
  }
194
192
  },
195
193
  });
196
- return yield* pipe(this.#prepareOffer(secretKeys, state, balanceRecipe, 0), Either.fromOption(() => {
197
- return new OtherWalletError({
198
- message: 'Could not create a valid guaranteed offer',
199
- });
194
+ // An empty recipe (e.g. the guaranteed section is already balanced) yields no offer.
195
+ // Treat that as "nothing to add" rather than a failure, mirroring the fallible section.
196
+ return pipe(this.#prepareOffer(secretKeys, state, balanceRecipe, 0), Option.match({
197
+ onNone: () => ({ offer: undefined, newState: state }),
198
+ onSome: (result) => result,
200
199
  }));
201
200
  });
202
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midnight-ntwrk/wallet-sdk-shielded",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -8,14 +8,16 @@
8
8
  "author": "Midnight Foundation",
9
9
  "license": "Apache-2.0",
10
10
  "publishConfig": {
11
- "registry": "https://npm.pkg.github.com/"
11
+ "registry": "https://registry.npmjs.org/",
12
+ "access": "public"
12
13
  },
13
14
  "files": [
14
15
  "dist/"
15
16
  ],
16
17
  "repository": {
17
18
  "type": "git",
18
- "url": "git+https://github.com/midnight-ntwrk/artifacts.git"
19
+ "url": "git+https://github.com/midnightntwrk/midnight-wallet.git",
20
+ "directory": "packages/shielded-wallet"
19
21
  },
20
22
  "exports": {
21
23
  ".": {
@@ -28,13 +30,13 @@
28
30
  }
29
31
  },
30
32
  "dependencies": {
31
- "@midnight-ntwrk/ledger-v8": "^8.0.3",
32
- "@midnight-ntwrk/wallet-sdk-abstractions": "2.1.0",
33
- "@midnight-ntwrk/wallet-sdk-address-format": "3.1.1",
34
- "@midnight-ntwrk/wallet-sdk-capabilities": "3.3.0",
35
- "@midnight-ntwrk/wallet-sdk-indexer-client": "1.2.1",
36
- "@midnight-ntwrk/wallet-sdk-runtime": "1.0.3",
37
- "@midnight-ntwrk/wallet-sdk-utilities": "1.1.1",
33
+ "@midnight-ntwrk/ledger-v8": "^8.1.0",
34
+ "@midnight-ntwrk/wallet-sdk-abstractions": "^2.1.0",
35
+ "@midnight-ntwrk/wallet-sdk-address-format": "^3.1.2",
36
+ "@midnight-ntwrk/wallet-sdk-capabilities": "^3.3.1",
37
+ "@midnight-ntwrk/wallet-sdk-indexer-client": "^1.2.3",
38
+ "@midnight-ntwrk/wallet-sdk-runtime": "^1.0.5",
39
+ "@midnight-ntwrk/wallet-sdk-utilities": "^1.2.0",
38
40
  "effect": "^3.19.19",
39
41
  "rxjs": "^7.8.2"
40
42
  },