@midnight-ntwrk/wallet-sdk-shielded 2.1.0 → 3.0.1

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,26 +1,52 @@
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
51
  export type SecretKeysResource = <A>(cb: (keys: ledger.ZswapSecretKeys) => A) => A;
26
52
  export declare const SecretKeysResource: {
@@ -35,12 +61,13 @@ export declare const 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,4 +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>;
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';
@@ -55,10 +56,12 @@ const HexedLedgerEvent = pipe(Schema.Uint8ArrayFromHex, Schema.compose(LedgerEve
55
56
  const EventsSyncUpdatePayload = Schema.Struct({
56
57
  id: Schema.Number,
57
58
  raw: Schema.String,
59
+ protocolVersion: Schema.Number,
58
60
  maxId: Schema.Number,
59
61
  });
60
62
  export const EventsSyncUpdate = Schema.TaggedStruct('EventsSyncUpdate', {
61
63
  id: Schema.Number,
64
+ protocolVersion: Schema.Number,
62
65
  maxId: Schema.Number,
63
66
  event: LedgerEventSchema,
64
67
  });
@@ -66,12 +69,14 @@ const EventsSyncUpdateFromPayload = Schema.transformOrFail(EventsSyncUpdatePaylo
66
69
  decode: (input) => pipe(Schema.decodeUnknownEither(HexedLedgerEvent)(input.raw), Either.map((event) => ({
67
70
  _tag: 'EventsSyncUpdate',
68
71
  id: input.id,
72
+ protocolVersion: input.protocolVersion,
69
73
  maxId: input.maxId,
70
74
  event,
71
75
  })), Either.mapLeft((error) => new ParseResult.Unexpected(error, 'Failed to decode ledger event payload')), EitherOps.toEffect),
72
76
  encode: (output) => pipe(Schema.encodeEither(HexedLedgerEvent)(output.event), Either.map((raw) => ({
73
77
  id: output.id,
74
78
  raw,
79
+ protocolVersion: output.protocolVersion,
75
80
  maxId: output.maxId,
76
81
  })), Either.mapLeft((error) => new ParseResult.Unexpected(error, 'Failed to encode ledger event payload')), EitherOps.toEffect),
77
82
  });
@@ -89,8 +94,13 @@ export const makeEventsSyncService = (config) => {
89
94
  }
90
95
  const indexerWsUrl = indexerWsUrlResult.right;
91
96
  const appliedIndex = state.progress?.appliedIndex ?? 0n;
92
- const batchSize = config.batchSize ?? 10;
93
- 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;
94
104
  },
95
105
  };
96
106
  };
@@ -98,7 +108,7 @@ export const makeEventsSyncCapability = () => {
98
108
  return {
99
109
  applyUpdate: (state, wrappedUpdate) => {
100
110
  if (wrappedUpdate.updates.length === 0) {
101
- return state;
111
+ return [state, { changes: [], protocolVersion: Number(state.protocolVersion) }];
102
112
  }
103
113
  const lastUpdate = wrappedUpdate.updates.at(-1);
104
114
  const nextIndex = BigInt(lastUpdate.id);
@@ -106,29 +116,67 @@ export const makeEventsSyncCapability = () => {
106
116
  // in case the nextIndex is less than or equal to the appliedIndex
107
117
  // just update highestRelevantWalletIndex
108
118
  if (nextIndex <= state.progress.appliedIndex) {
109
- return CoreWallet.updateProgress(state, {
110
- highestRelevantWalletIndex,
111
- isConnected: true,
112
- });
119
+ return [
120
+ CoreWallet.updateProgress(state, {
121
+ highestRelevantWalletIndex,
122
+ isConnected: true,
123
+ }),
124
+ { changes: [], protocolVersion: lastUpdate.protocolVersion },
125
+ ];
113
126
  }
114
- return CoreWallet.updateProgress(CoreWallet.replayEvents(state, wrappedUpdate.secretKeys, wrappedUpdate.updates.map((u) => u.event)), {
127
+ const [newState, newChanges] = CoreWallet.replayEventsWithChanges(state, wrappedUpdate.secretKeys, wrappedUpdate.updates.map((u) => u.event));
128
+ const updatedState = CoreWallet.updateProgress(newState, {
115
129
  highestRelevantWalletIndex,
116
130
  appliedIndex: nextIndex,
117
131
  isConnected: true,
118
132
  });
133
+ return [updatedState, { changes: newChanges, protocolVersion: lastUpdate.protocolVersion }];
119
134
  },
120
135
  };
121
136
  };
122
137
  export const makeSimulatorSyncService = (config) => {
123
138
  return {
124
- 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
+ },
125
158
  };
126
159
  };
127
160
  export const makeSimulatorSyncCapability = () => {
128
161
  return {
129
162
  applyUpdate: (state, update) => {
130
- const { update: { lastTxResult: { events }, }, secretKeys, } = update;
131
- 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
+ ];
132
180
  },
133
181
  };
134
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,10 @@ 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 = Array.from(fallibleOffers.entries()).reduce((tx, [segment, offer]) => tx.addZswapOffer({ tag: 'specific', value: segment }, offer), ledger.Transaction.fromParts(this.networkId, guaranteed));
50
+ return [balancedTx, afterGuaranteed];
50
51
  });
51
52
  }
52
53
  makeTransfer(secretKeys, state, transfers) {
@@ -120,43 +121,44 @@ export class TransactingCapabilityImplementation {
120
121
  }), Option.map((offer) => ({ offer, newState: stateAfterWatches })));
121
122
  }
122
123
  balanceFallibleSection(secretKeys, state, imbalances, coinSelection) {
123
- return Either.try({
124
+ return Array.from(imbalances.fallible.entries()).reduce((acc, [segment, segmentImbalances]) => Either.flatMap(acc, ({ fallibleOffers, newState }) => pipe(Either.try({
124
125
  try: () => {
125
126
  const fallibleBalanceRecipe = getBalanceRecipe({
126
127
  coins: this.getCoins()
127
- .getAvailableCoins(state)
128
+ .getAvailableCoins(newState)
128
129
  .map((c) => c.coin),
129
- initialImbalances: imbalances.fallible,
130
+ initialImbalances: segmentImbalances,
130
131
  transactionCostModel: ShieldedCostModel,
131
132
  feeTokenType: '',
132
133
  coinSelection,
133
134
  createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
134
135
  isCoinEqual: (a, b) => a.type === b.type && a.value === b.value,
135
136
  });
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
- }));
137
+ return this.#prepareOffer(secretKeys, newState, fallibleBalanceRecipe, segment);
143
138
  },
144
139
  catch: (err) => {
145
140
  if (err instanceof BalancingInsufficientFundsError) {
146
141
  return new InsufficientFundsError({
147
- message: 'Insufficient funds',
142
+ message: `Insufficient funds for fallible segment ${segment}`,
148
143
  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,
144
+ amount: segmentImbalances.get(err.tokenType) ?? 0n,
156
145
  });
157
146
  }
147
+ return new OtherWalletError({
148
+ message: `Balancing fallible segment ${segment} failed`,
149
+ cause: err,
150
+ });
158
151
  },
159
- });
152
+ }), Either.map(Option.match({
153
+ onNone: () => ({ fallibleOffers, newState }),
154
+ onSome: (res) => ({
155
+ fallibleOffers: new Map([...fallibleOffers, [segment, res.offer]]),
156
+ newState: res.newState,
157
+ }),
158
+ })))), Either.right({
159
+ fallibleOffers: new Map(),
160
+ newState: state,
161
+ }));
160
162
  }
161
163
  #balanceGuaranteedSection(secretKeys, state, imbalances, coinSelection, targetImbalances) {
162
164
  return Either.gen(this, function* () {
@@ -1,9 +1,64 @@
1
- import * as ledger from '@midnight-ntwrk/ledger-v8';
2
- import { CoreWallet } from './CoreWallet.js';
3
- export type TransactionHistoryCapability<TState, TTransaction> = {
4
- updateTxHistory(state: TState, newTxs: TTransaction[]): TState;
5
- transactionHistory(state: TState): readonly TTransaction[];
1
+ import { TransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-abstractions';
2
+ import type * as ledger from '@midnight-ntwrk/ledger-v8';
3
+ import { Effect, Schema } from 'effect';
4
+ import { TransactionHistoryError } from './WalletError.js';
5
+ export declare const QualifiedShieldedCoinInfoSchema: Schema.Struct<{
6
+ type: typeof Schema.String;
7
+ nonce: typeof Schema.String;
8
+ value: typeof Schema.BigInt;
9
+ mtIndex: typeof Schema.BigInt;
10
+ }>;
11
+ export declare const ShieldedSectionSchema: Schema.Struct<{
12
+ receivedCoins: Schema.Array$<Schema.Struct<{
13
+ type: typeof Schema.String;
14
+ nonce: typeof Schema.String;
15
+ value: typeof Schema.BigInt;
16
+ mtIndex: typeof Schema.BigInt;
17
+ }>>;
18
+ spentCoins: Schema.Array$<Schema.Struct<{
19
+ type: typeof Schema.String;
20
+ nonce: typeof Schema.String;
21
+ value: typeof Schema.BigInt;
22
+ mtIndex: typeof Schema.BigInt;
23
+ }>>;
24
+ }>;
25
+ type ShieldedSection = Schema.Schema.Type<typeof ShieldedSectionSchema>;
26
+ export declare const ShieldedTransactionHistoryEntrySchema: Schema.Struct<{
27
+ hash: typeof Schema.String;
28
+ protocolVersion: typeof Schema.Number;
29
+ status: Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>;
30
+ shielded: Schema.Struct<{
31
+ receivedCoins: Schema.Array$<Schema.Struct<{
32
+ type: typeof Schema.String;
33
+ nonce: typeof Schema.String;
34
+ value: typeof Schema.BigInt;
35
+ mtIndex: typeof Schema.BigInt;
36
+ }>>;
37
+ spentCoins: Schema.Array$<Schema.Struct<{
38
+ type: typeof Schema.String;
39
+ nonce: typeof Schema.String;
40
+ value: typeof Schema.BigInt;
41
+ mtIndex: typeof Schema.BigInt;
42
+ }>>;
43
+ }>;
44
+ }>;
45
+ export type ShieldedTransactionHistoryEntry = Schema.Schema.Type<typeof ShieldedTransactionHistoryEntrySchema>;
46
+ export type DefaultTransactionHistoryConfiguration = {
47
+ txHistoryStorage: TransactionHistoryStorage.TransactionHistoryStorage<TransactionHistoryStorage.TransactionHistoryEntryWithHash>;
48
+ indexerClientConnection: {
49
+ indexerHttpUrl: string;
50
+ };
6
51
  };
7
- export declare const makeDefaultTransactionHistoryCapability: () => TransactionHistoryCapability<CoreWallet, ledger.FinalizedTransaction>;
8
- export declare const makeSimulatorTransactionHistoryCapability: () => TransactionHistoryCapability<CoreWallet, ledger.ProofErasedTransaction>;
9
- export declare const makeDiscardTransactionHistoryCapability: () => TransactionHistoryCapability<CoreWallet, ledger.FinalizedTransaction>;
52
+ export type TransactionDetails = {
53
+ hash: string;
54
+ timestamp: number;
55
+ status: 'SUCCESS' | 'FAILURE' | 'PARTIAL_SUCCESS';
56
+ };
57
+ export type TransactionHistoryService = {
58
+ put(changes: ledger.ZswapStateChanges, metadata: TransactionDetails, protocolVersion: number): Effect.Effect<void, TransactionHistoryError>;
59
+ getTransactionDetails(hash: TransactionHistoryStorage.TransactionHash): Effect.Effect<TransactionDetails, TransactionHistoryError>;
60
+ };
61
+ export declare const mergeShieldedSections: (existing: ShieldedSection, incoming: ShieldedSection) => ShieldedSection;
62
+ export declare const makeDefaultTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService;
63
+ export declare const makeSimulatorTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService;
64
+ export {};