@midnight-ntwrk/wallet-sdk-shielded 2.1.0-rc.0 → 3.0.0

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.
@@ -1,28 +1,31 @@
1
- import { type ProtocolState, ProtocolVersion, SyncProgress } from '@midnight-ntwrk/wallet-sdk-abstractions';
1
+ import { type ProtocolState, ProtocolVersion, type SyncProgress } from '@midnight-ntwrk/wallet-sdk-abstractions';
2
2
  import { type BaseV1Configuration, type DefaultV1Configuration, type V1Variant, CoreWallet } from './v1/index.js';
3
3
  import * as ledger from '@midnight-ntwrk/ledger-v8';
4
4
  import * as rx from 'rxjs';
5
5
  import { type BalancingResult } from './v1/Transacting.js';
6
6
  import { type SerializationCapability } from './v1/Serialization.js';
7
- import { type TransactionHistoryCapability } from './v1/TransactionHistory.js';
7
+ import { type TransactionHistoryService } from './v1/TransactionHistory.js';
8
8
  import { type AvailableCoin, type CoinsAndBalancesCapability, type PendingCoin } from './v1/CoinsAndBalances.js';
9
9
  import { type KeysCapability } from './v1/Keys.js';
10
10
  import { type ShieldedAddress, type ShieldedCoinPublicKey, type ShieldedEncryptionPublicKey } from '@midnight-ntwrk/wallet-sdk-address-format';
11
11
  import { type TokenTransfer } from './v1/Transacting.js';
12
12
  import { type WalletSyncUpdate } from './v1/Sync.js';
13
13
  import { type Variant, type VariantBuilder, type WalletLike } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
14
- export type ShieldedWalletCapabilities<TSerialized = string, TTransaction = ledger.FinalizedTransaction> = {
14
+ export type ShieldedWalletCapabilities<TSerialized = string> = {
15
15
  serialization: SerializationCapability<CoreWallet, null, TSerialized>;
16
16
  coinsAndBalances: CoinsAndBalancesCapability<CoreWallet>;
17
17
  keys: KeysCapability<CoreWallet>;
18
- transactionHistory: TransactionHistoryCapability<CoreWallet, TTransaction>;
18
+ };
19
+ export type ShieldedWalletServices = {
20
+ transactionHistory: TransactionHistoryService;
19
21
  };
20
22
  export type UnboundTransaction = ledger.Transaction<ledger.SignatureEnabled, ledger.Proof, ledger.PreBinding>;
21
- export declare class ShieldedWalletState<TSerialized = string, TTransaction = ledger.FinalizedTransaction> {
22
- static readonly mapState: <TSerialized_1 = string, TTransaction_1 = ledger.FinalizedTransaction>(capabilities: ShieldedWalletCapabilities<TSerialized_1, TTransaction_1>) => (state: ProtocolState.ProtocolState<CoreWallet>) => ShieldedWalletState<TSerialized_1, TTransaction_1>;
23
+ export declare class ShieldedWalletState<TSerialized = string, _TTransaction = ledger.FinalizedTransaction> {
24
+ static readonly mapState: <TSerialized_1 = string>(variant: ShieldedWalletCapabilities<TSerialized_1> & ShieldedWalletServices) => (state: ProtocolState.ProtocolState<CoreWallet>) => ShieldedWalletState<TSerialized_1>;
23
25
  readonly protocolVersion: ProtocolVersion.ProtocolVersion;
24
26
  readonly state: CoreWallet;
25
- readonly capabilities: ShieldedWalletCapabilities<TSerialized, TTransaction>;
27
+ readonly capabilities: ShieldedWalletCapabilities<TSerialized>;
28
+ readonly services: ShieldedWalletServices;
26
29
  get balances(): Record<ledger.RawTokenType, bigint>;
27
30
  get totalCoins(): readonly (AvailableCoin | PendingCoin)[];
28
31
  get availableCoins(): readonly AvailableCoin[];
@@ -31,12 +34,7 @@ export declare class ShieldedWalletState<TSerialized = string, TTransaction = le
31
34
  get encryptionPublicKey(): ShieldedEncryptionPublicKey;
32
35
  get address(): ShieldedAddress;
33
36
  get progress(): SyncProgress.SyncProgress;
34
- /**
35
- * Transaction history for the wallet.
36
- * @throws Error - Not yet implemented
37
- */
38
- get transactionHistory(): never;
39
- constructor(state: ProtocolState.ProtocolState<CoreWallet>, capabilities: ShieldedWalletCapabilities<TSerialized, TTransaction>);
37
+ constructor(state: ProtocolState.ProtocolState<CoreWallet>, capabilities: ShieldedWalletCapabilities<TSerialized>, services: ShieldedWalletServices);
40
38
  serialize(): TSerialized;
41
39
  }
42
40
  export type ShieldedWallet = CustomizedShieldedWallet<ledger.ZswapSecretKeys, ledger.FinalizedTransaction, WalletSyncUpdate, string>;
@@ -17,12 +17,15 @@ import { Effect, Either } from 'effect';
17
17
  import * as rx from 'rxjs';
18
18
  import { WalletBuilder } from '@midnight-ntwrk/wallet-sdk-runtime';
19
19
  export class ShieldedWalletState {
20
- static mapState = (capabilities) => (state) => {
21
- return new ShieldedWalletState(state, capabilities);
20
+ static mapState = (variant) => (state) => {
21
+ const { serialization, coinsAndBalances, keys } = variant;
22
+ const { transactionHistory } = variant;
23
+ return new ShieldedWalletState(state, { serialization, coinsAndBalances, keys }, { transactionHistory });
22
24
  };
23
25
  protocolVersion;
24
26
  state;
25
27
  capabilities;
28
+ services;
26
29
  get balances() {
27
30
  return this.capabilities.coinsAndBalances.getAvailableBalances(this.state);
28
31
  }
@@ -47,17 +50,11 @@ export class ShieldedWalletState {
47
50
  get progress() {
48
51
  return this.state.progress;
49
52
  }
50
- /**
51
- * Transaction history for the wallet.
52
- * @throws Error - Not yet implemented
53
- */
54
- get transactionHistory() {
55
- throw new Error('Transaction history is not yet implemented for ShieldedWallet');
56
- }
57
- constructor(state, capabilities) {
53
+ constructor(state, capabilities, services) {
58
54
  this.protocolVersion = state.version;
59
55
  this.state = state.state;
60
56
  this.capabilities = capabilities;
57
+ this.services = services;
61
58
  }
62
59
  serialize() {
63
60
  return this.capabilities.serialization.serialize(this.state);
@@ -122,8 +119,8 @@ export function CustomShieldedWallet(configuration, builder) {
122
119
  return rx.firstValueFrom(this.state.pipe(rx.filter((state) => state.state.progress.isCompleteWithin(allowedGap))));
123
120
  }
124
121
  /**
125
- * Serializes the most recent state
126
- * It's preferable to use [[ShieldedWalletState.serialize]] instead, to know exactly, which state is serialized
122
+ * Serializes the most recent state It's preferable to use [[ShieldedWalletState.serialize]] instead, to know
123
+ * exactly, which state is serialized
127
124
  */
128
125
  serializeState() {
129
126
  return rx.firstValueFrom(this.state).then((state) => state.serialize());
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './ShieldedWallet.js';
2
+ export { type ShieldedTransactionHistoryEntry, ShieldedSectionSchema, mergeShieldedSections, } from './v1/TransactionHistory.js';
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  export * from './ShieldedWallet.js';
14
+ export { ShieldedSectionSchema, mergeShieldedSections, } from './v1/TransactionHistory.js';
@@ -1,5 +1,5 @@
1
- import { CoreWallet } from './CoreWallet.js';
2
- import * as ledger from '@midnight-ntwrk/ledger-v8';
1
+ import { type CoreWallet } from './CoreWallet.js';
2
+ import type * as ledger from '@midnight-ntwrk/ledger-v8';
3
3
  export type AvailableCoin = {
4
4
  coin: ledger.QualifiedShieldedCoinInfo;
5
5
  commitment: ledger.CoinCommitment;
@@ -1,7 +1,7 @@
1
1
  import * as ledger from '@midnight-ntwrk/ledger-v8';
2
2
  import { ProtocolVersion, SyncProgress } from '@midnight-ntwrk/wallet-sdk-abstractions';
3
3
  import { Either } from 'effect';
4
- import { WalletError } from './WalletError.js';
4
+ import { type WalletError } from './WalletError.js';
5
5
  export type PublicKeys = {
6
6
  coinPublicKey: ledger.CoinPublicKey;
7
7
  encryptionPublicKey: ledger.EncPublicKey;
@@ -37,7 +37,7 @@ export declare const CoreWallet: {
37
37
  initEmpty(keys: ledger.ZswapSecretKeys, networkId: string): CoreWallet;
38
38
  applyCollapsedUpdate(wallet: CoreWallet, collapsed: ledger.MerkleTreeCollapsedUpdate): CoreWallet;
39
39
  apply<TOffer extends ledger.ZswapOffer<ledger.Proofish>>(wallet: CoreWallet, secretKeys: ledger.ZswapSecretKeys, offer: TOffer): CoreWallet;
40
- replayEvents(wallet: CoreWallet, secretKeys: ledger.ZswapSecretKeys, events: ledger.Event[]): CoreWallet;
40
+ replayEventsWithChanges(wallet: CoreWallet, secretKeys: ledger.ZswapSecretKeys, events: ledger.Event[]): [CoreWallet, ledger.ZswapStateChanges[]];
41
41
  updateProgress(wallet: CoreWallet, { appliedIndex, highestRelevantWalletIndex, highestIndex, highestRelevantIndex, isConnected, }: Partial<SyncProgress.SyncProgressData>): CoreWallet;
42
42
  addTransaction(wallet: CoreWallet, _tx: ledger.FinalizedTransaction): CoreWallet;
43
43
  revertTransaction<TTx extends ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>>(wallet: CoreWallet, tx: TTx): CoreWallet;
@@ -106,10 +106,12 @@ export const CoreWallet = {
106
106
  const newCoinHashes = CoinHashesMap.updateWithCoins(secretKeys, wallet.coinHashes, CoinHashesMap.pickAllCoins(newState));
107
107
  return { ...wallet, state: newState, coinHashes: newCoinHashes };
108
108
  },
109
- replayEvents(wallet, secretKeys, events) {
110
- const newState = wallet.state.replayEvents(secretKeys, events);
109
+ replayEventsWithChanges(wallet, secretKeys, events) {
110
+ const stateWithChanges = wallet.state.replayEventsWithChanges(secretKeys, events);
111
+ const newState = stateWithChanges.state;
111
112
  const newCoinHashes = CoinHashesMap.updateWithCoins(secretKeys, wallet.coinHashes, CoinHashesMap.pickAllCoins(newState));
112
- return { ...wallet, state: newState, coinHashes: newCoinHashes };
113
+ const updatedWallet = { ...wallet, state: newState, coinHashes: newCoinHashes };
114
+ return [updatedWallet, stateWithChanges.changes];
113
115
  },
114
116
  updateProgress(wallet, { appliedIndex, highestRelevantWalletIndex, highestIndex, highestRelevantIndex, isConnected, }) {
115
117
  const updatedProgress = SyncProgress.createSyncProgress({
@@ -2,24 +2,24 @@ 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';
5
- import { type EventsSyncUpdate, type SyncCapability, type SyncService } from './Sync.js';
5
+ import { type ChangesResult, type EventsSyncUpdate, type SyncCapability, type SyncService } from './Sync.js';
6
6
  import { type TransactingCapability, type TokenTransfer, type BalancingResult } from './Transacting.js';
7
7
  import { type WalletError } from './WalletError.js';
8
8
  import { type CoinsAndBalancesCapability } from './CoinsAndBalances.js';
9
9
  import { type KeysCapability } from './Keys.js';
10
10
  import { type CoinSelection } from '@midnight-ntwrk/wallet-sdk-capabilities';
11
11
  import { type CoreWallet } from './CoreWallet.js';
12
- import { type TransactionHistoryCapability } from './TransactionHistory.js';
12
+ import { type TransactionHistoryService } from './TransactionHistory.js';
13
13
  export declare namespace RunningV1Variant {
14
14
  type Context<TSerialized, TSyncUpdate, TTransaction, TStartAux> = {
15
15
  serializationCapability: SerializationCapability<CoreWallet, null, TSerialized>;
16
16
  syncService: SyncService<CoreWallet, TStartAux, TSyncUpdate>;
17
- syncCapability: SyncCapability<CoreWallet, TSyncUpdate>;
17
+ syncCapability: SyncCapability<CoreWallet, TSyncUpdate, ChangesResult>;
18
18
  transactingCapability: TransactingCapability<ledger.ZswapSecretKeys, CoreWallet, TTransaction>;
19
19
  coinsAndBalancesCapability: CoinsAndBalancesCapability<CoreWallet>;
20
20
  keysCapability: KeysCapability<CoreWallet>;
21
21
  coinSelection: CoinSelection<ledger.QualifiedShieldedCoinInfo>;
22
- transactionHistoryCapability: TransactionHistoryCapability<CoreWallet, TTransaction>;
22
+ transactionHistoryService: TransactionHistoryService;
23
23
  };
24
24
  type AnyContext = Context<any, any, any, any>;
25
25
  }
@@ -49,15 +49,16 @@ export class RunningV1Variant {
49
49
  return this.startSync(startAux).pipe(Stream.runScoped(Sink.drain), Effect.forkScoped, Effect.provideService(Scope.Scope, this.#scope));
50
50
  }
51
51
  startSync(startAux) {
52
- return pipe(SubscriptionRef.get(this.#context.stateRef), Stream.fromEffect, Stream.flatMap((state) => this.#v1Context.syncService.updates(state, startAux)), Stream.mapEffect((update) => {
53
- return SubscriptionRef.updateEffect(this.#context.stateRef, (state) => Effect.try({
54
- try: () => this.#v1Context.syncCapability.applyUpdate(state, update),
55
- catch: (err) => new OtherWalletError({
56
- message: 'Error while applying sync update',
57
- cause: err,
58
- }),
59
- }));
60
- }), Stream.tapError((error) => Console.error(error)), Stream.retry(pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.map((delay) => {
52
+ 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
+ try: () => {
54
+ const [newState, changesResult] = this.#v1Context.syncCapability.applyUpdate(state, update);
55
+ return [changesResult, newState];
56
+ },
57
+ catch: (err) => new OtherWalletError({
58
+ message: 'Error while applying sync update',
59
+ cause: err,
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
62
  const maxDelay = Duration.minutes(2);
62
63
  const jitter = Duration.millis(Math.floor(Math.random() * 1000));
63
64
  const delayWithJitter = Duration.toMillis(delay) + Duration.toMillis(jitter);
@@ -1,7 +1,7 @@
1
1
  import { Either } from 'effect';
2
2
  import { WalletError } from './WalletError.js';
3
3
  import { CoreWallet } from './CoreWallet.js';
4
- import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
4
+ import { type NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
5
5
  export type SerializationCapability<TWallet, TAux, TSerialized> = {
6
6
  serialize(wallet: TWallet): TSerialized;
7
7
  deserialize(aux: TAux, data: TSerialized): Either.Either<TWallet, WalletError>;
package/dist/v1/Sync.d.ts CHANGED
@@ -1,46 +1,73 @@
1
1
  import * as ledger from '@midnight-ntwrk/ledger-v8';
2
- import { Schema, Scope, Stream } from 'effect';
2
+ import { Schema, type Scope, Stream } from 'effect';
3
3
  import { CoreWallet } from './CoreWallet.js';
4
- import { Simulator, SimulatorState } from './Simulator.js';
5
- import { WalletError } from './WalletError.js';
6
- import { TransactionHistoryCapability } from './TransactionHistory.js';
4
+ import { type Simulator, type SimulatorState } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
5
+ import { type WalletError } from './WalletError.js';
6
+ import { type TransactionHistoryService } from './TransactionHistory.js';
7
7
  export interface SyncService<TState, TStartAux, TUpdate> {
8
8
  updates: (state: TState, auxData: TStartAux) => Stream.Stream<TUpdate, WalletError, Scope.Scope>;
9
9
  }
10
- export interface SyncCapability<TState, TUpdate> {
11
- applyUpdate: (state: TState, update: TUpdate) => TState;
10
+ export type ChangesResult = {
11
+ readonly changes: ledger.ZswapStateChanges[];
12
+ readonly protocolVersion: number;
13
+ };
14
+ export interface SyncCapability<TState, TUpdate, TResult> {
15
+ applyUpdate: (state: TState, update: TUpdate) => [TState, TResult];
12
16
  }
13
17
  export type IndexerClientConnection = {
14
18
  indexerHttpUrl: string;
15
19
  indexerWsUrl?: string;
16
20
  keepAlive?: number;
17
21
  };
22
+ export type BatchUpdatesConfig = {
23
+ /**
24
+ * Maximum number of events to collect into a single batch before emitting.
25
+ *
26
+ * @default 10
27
+ */
28
+ readonly size?: number;
29
+ /**
30
+ * Maximum time in milliseconds to wait for a full batch before emitting a partial one. Controls the `groupedWithin`
31
+ * timeout — lower values mean more responsive (but smaller) batches when events arrive slowly.
32
+ *
33
+ * @default 1
34
+ */
35
+ readonly timeout?: number;
36
+ /**
37
+ * Minimum delay in milliseconds injected between consecutive batches. Prevents the sync stream from saturating
38
+ * downstream consumers when many events are available at once. Set to 0 to disable spacing entirely.
39
+ *
40
+ * @default 4
41
+ */
42
+ readonly spacing?: number;
43
+ };
18
44
  export type DefaultSyncConfiguration = {
19
45
  indexerClientConnection: IndexerClientConnection;
20
- batchSize?: number;
46
+ batchUpdates?: BatchUpdatesConfig;
21
47
  };
22
48
  export type DefaultSyncContext = {
23
- transactionHistoryCapability: TransactionHistoryCapability<CoreWallet, ledger.FinalizedTransaction>;
49
+ transactionHistoryService: TransactionHistoryService;
24
50
  };
25
- type SecretKeysResource = <A>(cb: (keys: ledger.ZswapSecretKeys) => A) => A;
51
+ export type SecretKeysResource = <A>(cb: (keys: ledger.ZswapSecretKeys) => A) => A;
26
52
  export declare const SecretKeysResource: {
27
53
  create: (secretKeys: ledger.ZswapSecretKeys) => SecretKeysResource;
28
54
  };
29
55
  export type WalletSyncUpdate = {
30
56
  updates: EventsSyncUpdate[];
31
- secretKeys: SecretKeysResource;
57
+ secretKeys: ledger.ZswapSecretKeys;
32
58
  };
33
59
  export declare const WalletSyncUpdate: {
34
60
  create: (updates: EventsSyncUpdate[], secretKeys: ledger.ZswapSecretKeys) => WalletSyncUpdate;
35
61
  };
36
62
  export declare const EventsSyncUpdate: Schema.TaggedStruct<"EventsSyncUpdate", {
37
63
  id: typeof Schema.Number;
64
+ protocolVersion: typeof Schema.Number;
38
65
  maxId: typeof Schema.Number;
39
66
  event: Schema.declare<ledger.Event, ledger.Event, readonly [], never>;
40
67
  }>;
41
68
  export type EventsSyncUpdate = Schema.Schema.Type<typeof EventsSyncUpdate>;
42
69
  export declare const makeEventsSyncService: (config: DefaultSyncConfiguration) => SyncService<CoreWallet, ledger.ZswapSecretKeys, WalletSyncUpdate>;
43
- export declare const makeEventsSyncCapability: () => SyncCapability<CoreWallet, WalletSyncUpdate>;
70
+ export declare const makeEventsSyncCapability: () => SyncCapability<CoreWallet, WalletSyncUpdate, ChangesResult>;
44
71
  export type SimulatorSyncConfiguration = {
45
72
  simulator: Simulator;
46
73
  };
@@ -49,5 +76,4 @@ export type SimulatorSyncUpdate = {
49
76
  secretKeys: ledger.ZswapSecretKeys;
50
77
  };
51
78
  export declare const makeSimulatorSyncService: (config: SimulatorSyncConfiguration) => SyncService<CoreWallet, ledger.ZswapSecretKeys, SimulatorSyncUpdate>;
52
- export declare const makeSimulatorSyncCapability: () => SyncCapability<CoreWallet, SimulatorSyncUpdate>;
53
- export {};
79
+ export declare const makeSimulatorSyncCapability: () => SyncCapability<CoreWallet, SimulatorSyncUpdate, ChangesResult>;
package/dist/v1/Sync.js CHANGED
@@ -13,6 +13,7 @@
13
13
  import * as ledger from '@midnight-ntwrk/ledger-v8';
14
14
  import { Chunk, Duration, Effect, Either, ParseResult, pipe, Schedule, Schema, Stream } from 'effect';
15
15
  import { CoreWallet } from './CoreWallet.js';
16
+ import { getLastBlock, getBlockEventsFrom, } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
16
17
  import { ZswapEvents } from '@midnight-ntwrk/wallet-sdk-indexer-client';
17
18
  import { ConnectionHelper, WsSubscriptionClient } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
18
19
  import { SyncWalletError } from './WalletError.js';
@@ -23,17 +24,9 @@ const Uint8ArraySchema = Schema.declare((input) => input instanceof Uint8Array).
23
24
  });
24
25
  export const SecretKeysResource = {
25
26
  create: (secretKeys) => {
26
- /**
27
- * TODO: future Ledger version will include `clear` function to clear the secret keys,
28
- * it is intentend to be used here instead of `null`
29
- */
30
- let sk = secretKeys;
31
27
  return (cb) => {
32
- if (sk === null) {
33
- throw new Error('Secret keys have been consumed');
34
- }
35
- const result = cb(sk);
36
- sk = null;
28
+ const result = cb(secretKeys);
29
+ secretKeys.clear();
37
30
  return result;
38
31
  };
39
32
  },
@@ -42,7 +35,7 @@ export const WalletSyncUpdate = {
42
35
  create: (updates, secretKeys) => {
43
36
  return {
44
37
  updates,
45
- secretKeys: SecretKeysResource.create(secretKeys),
38
+ secretKeys,
46
39
  };
47
40
  },
48
41
  };
@@ -63,10 +56,12 @@ const HexedLedgerEvent = pipe(Schema.Uint8ArrayFromHex, Schema.compose(LedgerEve
63
56
  const EventsSyncUpdatePayload = Schema.Struct({
64
57
  id: Schema.Number,
65
58
  raw: Schema.String,
59
+ protocolVersion: Schema.Number,
66
60
  maxId: Schema.Number,
67
61
  });
68
62
  export const EventsSyncUpdate = Schema.TaggedStruct('EventsSyncUpdate', {
69
63
  id: Schema.Number,
64
+ protocolVersion: Schema.Number,
70
65
  maxId: Schema.Number,
71
66
  event: LedgerEventSchema,
72
67
  });
@@ -74,12 +69,14 @@ const EventsSyncUpdateFromPayload = Schema.transformOrFail(EventsSyncUpdatePaylo
74
69
  decode: (input) => pipe(Schema.decodeUnknownEither(HexedLedgerEvent)(input.raw), Either.map((event) => ({
75
70
  _tag: 'EventsSyncUpdate',
76
71
  id: input.id,
72
+ protocolVersion: input.protocolVersion,
77
73
  maxId: input.maxId,
78
74
  event,
79
75
  })), Either.mapLeft((error) => new ParseResult.Unexpected(error, 'Failed to decode ledger event payload')), EitherOps.toEffect),
80
76
  encode: (output) => pipe(Schema.encodeEither(HexedLedgerEvent)(output.event), Either.map((raw) => ({
81
77
  id: output.id,
82
78
  raw,
79
+ protocolVersion: output.protocolVersion,
83
80
  maxId: output.maxId,
84
81
  })), Either.mapLeft((error) => new ParseResult.Unexpected(error, 'Failed to encode ledger event payload')), EitherOps.toEffect),
85
82
  });
@@ -97,8 +94,13 @@ export const makeEventsSyncService = (config) => {
97
94
  }
98
95
  const indexerWsUrl = indexerWsUrlResult.right;
99
96
  const appliedIndex = state.progress?.appliedIndex ?? 0n;
100
- const batchSize = config.batchSize ?? 10;
101
- return 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, Duration.millis(1)), Stream.map(Chunk.toArray), Stream.map((data) => WalletSyncUpdate.create(data, secretKeys)), Stream.schedule(Schedule.spaced(Duration.millis(4))));
97
+ const batchSize = config.batchUpdates?.size ?? 10;
98
+ const batchTimeout = Duration.millis(config.batchUpdates?.timeout ?? 1);
99
+ 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)));
101
+ return batchSpacing > 0
102
+ ? Stream.schedule(eventsStream, Schedule.spaced(Duration.millis(batchSpacing)))
103
+ : eventsStream;
102
104
  },
103
105
  };
104
106
  };
@@ -106,7 +108,7 @@ export const makeEventsSyncCapability = () => {
106
108
  return {
107
109
  applyUpdate: (state, wrappedUpdate) => {
108
110
  if (wrappedUpdate.updates.length === 0) {
109
- return state;
111
+ return [state, { changes: [], protocolVersion: Number(state.protocolVersion) }];
110
112
  }
111
113
  const lastUpdate = wrappedUpdate.updates.at(-1);
112
114
  const nextIndex = BigInt(lastUpdate.id);
@@ -114,31 +116,67 @@ export const makeEventsSyncCapability = () => {
114
116
  // in case the nextIndex is less than or equal to the appliedIndex
115
117
  // just update highestRelevantWalletIndex
116
118
  if (nextIndex <= state.progress.appliedIndex) {
117
- return CoreWallet.updateProgress(state, {
118
- highestRelevantWalletIndex,
119
- isConnected: true,
120
- });
119
+ return [
120
+ CoreWallet.updateProgress(state, {
121
+ highestRelevantWalletIndex,
122
+ isConnected: true,
123
+ }),
124
+ { changes: [], protocolVersion: lastUpdate.protocolVersion },
125
+ ];
121
126
  }
122
- return wrappedUpdate.secretKeys((keys) => {
123
- return CoreWallet.updateProgress(CoreWallet.replayEvents(state, keys, wrappedUpdate.updates.map((u) => u.event)), {
124
- highestRelevantWalletIndex,
125
- appliedIndex: nextIndex,
126
- isConnected: true,
127
- });
127
+ const [newState, newChanges] = CoreWallet.replayEventsWithChanges(state, wrappedUpdate.secretKeys, wrappedUpdate.updates.map((u) => u.event));
128
+ const updatedState = CoreWallet.updateProgress(newState, {
129
+ highestRelevantWalletIndex,
130
+ appliedIndex: nextIndex,
131
+ isConnected: true,
128
132
  });
133
+ return [updatedState, { changes: newChanges, protocolVersion: lastUpdate.protocolVersion }];
129
134
  },
130
135
  };
131
136
  };
132
137
  export const makeSimulatorSyncService = (config) => {
133
138
  return {
134
- updates: (_state, secretKeys) => config.simulator.state$.pipe(Stream.map((state) => ({ update: state, secretKeys: secretKeys }))),
139
+ updates: (_state, secretKeys) => {
140
+ // Get the initial state immediately to ensure we process the genesis block.
141
+ // Then subscribe to state$ for subsequent changes, but deduplicate by block number
142
+ // to avoid processing the same block twice.
143
+ let lastSeenBlockNumber;
144
+ return pipe(Stream.fromEffect(config.simulator.getLatestState()), Stream.concat(config.simulator.state$), Stream.filter((state) => {
145
+ const lastBlock = getLastBlock(state);
146
+ if (lastBlock === undefined) {
147
+ return false; // Skip blank state
148
+ }
149
+ const blockNumber = lastBlock.number;
150
+ // Skip if we've already seen this block (deduplication)
151
+ if (lastSeenBlockNumber !== undefined && blockNumber <= lastSeenBlockNumber) {
152
+ return false;
153
+ }
154
+ lastSeenBlockNumber = blockNumber;
155
+ return true;
156
+ }), Stream.map((state) => ({ update: state, secretKeys })));
157
+ },
135
158
  };
136
159
  };
137
160
  export const makeSimulatorSyncCapability = () => {
138
161
  return {
139
162
  applyUpdate: (state, update) => {
140
- const { update: { lastTxResult: { events }, }, secretKeys, } = update;
141
- return CoreWallet.replayEvents(state, secretKeys, events);
163
+ const { update: simulatorState, secretKeys } = update;
164
+ const lastBlock = getLastBlock(simulatorState);
165
+ if (lastBlock === undefined) {
166
+ return [state, { changes: [], protocolVersion: Number(state.protocolVersion) }];
167
+ }
168
+ // Get all events from blocks starting at appliedIndex (the next block to process).
169
+ // appliedIndex semantics: the first block number we haven't processed yet.
170
+ // Initial: appliedIndex = 0 (haven't processed any blocks)
171
+ // After processing block N: appliedIndex = N + 1 (next block to process)
172
+ const events = [...getBlockEventsFrom(simulatorState, state.progress.appliedIndex)];
173
+ const [newState, newChanges] = CoreWallet.replayEventsWithChanges(state, secretKeys, events);
174
+ return [
175
+ CoreWallet.updateProgress(newState, {
176
+ appliedIndex: lastBlock.number + 1n,
177
+ }),
178
+ { changes: newChanges, protocolVersion: Number(state.protocolVersion) },
179
+ ];
142
180
  },
143
181
  };
144
182
  };
@@ -1,14 +1,14 @@
1
1
  import * as ledger from '@midnight-ntwrk/ledger-v8';
2
- import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
2
+ import { type NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
3
3
  import { Array as Arr, Either } from 'effect';
4
4
  import { CoreWallet } from './CoreWallet.js';
5
- import { WalletError } from './WalletError.js';
6
- import { CoinSelection } from '@midnight-ntwrk/wallet-sdk-capabilities';
7
- import { ShieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
5
+ import { type WalletError } from './WalletError.js';
6
+ import { type CoinSelection } from '@midnight-ntwrk/wallet-sdk-capabilities';
7
+ import { type ShieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
8
8
  import { TransactionImbalances } from './TransactionImbalances.js';
9
9
  import { TransactionOps } from './TransactionOps.js';
10
- import { CoinsAndBalancesCapability } from './CoinsAndBalances.js';
11
- import { KeysCapability } from './Keys.js';
10
+ import { type CoinsAndBalancesCapability } from './CoinsAndBalances.js';
11
+ import { type KeysCapability } from './Keys.js';
12
12
  export interface TokenTransfer {
13
13
  readonly amount: bigint;
14
14
  readonly type: ledger.RawTokenType;
@@ -44,7 +44,7 @@ export declare class TransactingCapabilityImplementation<TTransaction extends le
44
44
  initSwap(secretKeys: ledger.ZswapSecretKeys, state: CoreWallet, desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>): Either.Either<[ledger.UnprovenTransaction, CoreWallet], WalletError>;
45
45
  revertTransaction(state: CoreWallet, transaction: TTransaction | ledger.UnprovenTransaction): Either.Either<CoreWallet, WalletError>;
46
46
  balanceFallibleSection(secretKeys: ledger.ZswapSecretKeys, state: CoreWallet, imbalances: TransactionImbalances, coinSelection: CoinSelection<ledger.QualifiedShieldedCoinInfo>): Either.Either<{
47
- offer: ledger.ZswapOffer<ledger.PreProof> | undefined;
47
+ fallibleOffers: Map<number, ledger.ZswapOffer<ledger.PreProof>>;
48
48
  newState: CoreWallet;
49
49
  }, WalletError>;
50
50
  }
@@ -44,9 +44,12 @@ export class TransactingCapabilityImplementation {
44
44
  if (TransactionImbalances.areBalanced(initialImbalances)) {
45
45
  return [undefined, state];
46
46
  }
47
- const { newState: afterFallible, offer: maybeFallible } = yield* this.balanceFallibleSection(secretKeys, state, initialImbalances, coinSelection);
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
- return [ledger.Transaction.fromParts(this.networkId, guaranteed, maybeFallible), afterGuaranteed];
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;
52
+ return [balancedTx, afterGuaranteed];
50
53
  });
51
54
  }
52
55
  makeTransfer(secretKeys, state, transfers) {
@@ -120,43 +123,44 @@ export class TransactingCapabilityImplementation {
120
123
  }), Option.map((offer) => ({ offer, newState: stateAfterWatches })));
121
124
  }
122
125
  balanceFallibleSection(secretKeys, state, imbalances, coinSelection) {
123
- return Either.try({
126
+ return Array.from(imbalances.fallible.entries()).reduce((acc, [segment, segmentImbalances]) => Either.flatMap(acc, ({ fallibleOffers, newState }) => pipe(Either.try({
124
127
  try: () => {
125
128
  const fallibleBalanceRecipe = getBalanceRecipe({
126
129
  coins: this.getCoins()
127
- .getAvailableCoins(state)
130
+ .getAvailableCoins(newState)
128
131
  .map((c) => c.coin),
129
- initialImbalances: imbalances.fallible,
132
+ initialImbalances: segmentImbalances,
130
133
  transactionCostModel: ShieldedCostModel,
131
134
  feeTokenType: '',
132
135
  coinSelection,
133
136
  createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
134
137
  isCoinEqual: (a, b) => a.type === b.type && a.value === b.value,
135
138
  });
136
- return pipe(this.#prepareOffer(secretKeys, state, fallibleBalanceRecipe, 1), Option.match({
137
- onNone: () => ({
138
- newState: state,
139
- offer: undefined,
140
- }),
141
- onSome: (res) => res,
142
- }));
139
+ return this.#prepareOffer(secretKeys, newState, fallibleBalanceRecipe, segment);
143
140
  },
144
141
  catch: (err) => {
145
142
  if (err instanceof BalancingInsufficientFundsError) {
146
143
  return new InsufficientFundsError({
147
- message: 'Insufficient funds',
144
+ message: `Insufficient funds for fallible segment ${segment}`,
148
145
  tokenType: err.tokenType,
149
- amount: imbalances.fallible.get(err.tokenType) ?? 0n,
150
- });
151
- }
152
- else {
153
- return new OtherWalletError({
154
- message: 'Balancing fallible section failed',
155
- cause: err,
146
+ amount: segmentImbalances.get(err.tokenType) ?? 0n,
156
147
  });
157
148
  }
149
+ return new OtherWalletError({
150
+ message: `Balancing fallible segment ${segment} failed`,
151
+ cause: err,
152
+ });
158
153
  },
159
- });
154
+ }), Either.map(Option.match({
155
+ onNone: () => ({ fallibleOffers, newState }),
156
+ onSome: (res) => ({
157
+ fallibleOffers: new Map([...fallibleOffers, [segment, res.offer]]),
158
+ newState: res.newState,
159
+ }),
160
+ })))), Either.right({
161
+ fallibleOffers: new Map(),
162
+ newState: state,
163
+ }));
160
164
  }
161
165
  #balanceGuaranteedSection(secretKeys, state, imbalances, coinSelection, targetImbalances) {
162
166
  return Either.gen(this, function* () {