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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,8 +89,10 @@ wallet.state.subscribe((state) => {
89
89
  ### Creating Transfer Transactions
90
90
 
91
91
  ```typescript
92
+ // tokenType: the shielded token type (raw hex) of a token you hold,
93
+ // e.g. one minted by a contract — NIGHT is unshielded and cannot appear here
92
94
  const tx = await wallet.transferTransaction(shieldedSecretKeys, [
93
- { type: 'NIGHT', receiverAddress: 'mn_shield-addr1...', amount: 1000n },
95
+ { type: tokenType, receiverAddress: 'mn_shield-addr1...', amount: 1000n },
94
96
  ]);
95
97
  ```
96
98
 
@@ -103,10 +105,12 @@ const balancingTx = await wallet.balanceTransaction(shieldedSecretKeys, transact
103
105
  ### Creating Swap Offers
104
106
 
105
107
  ```typescript
108
+ // tokenTypeA / tokenTypeB: shielded token types (raw hex) — both sides of a
109
+ // shielded swap are shielded tokens; NIGHT is unshielded and cannot appear here
106
110
  const swapTx = await wallet.initSwap(
107
111
  shieldedSecretKeys,
108
- { NIGHT: 500n }, // inputs
109
- [{ type: 'TOKEN_A', receiverAddress: shieldedAddress, amount: 100n }], // outputs
112
+ { [tokenTypeA]: 500n }, // inputs
113
+ [{ type: tokenTypeB, receiverAddress: shieldedAddress, amount: 100n }], // outputs
110
114
  );
111
115
  ```
112
116
 
package/dist/v1/Sync.js CHANGED
@@ -11,13 +11,13 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  import * as ledger from '@midnight-ntwrk/ledger-v8';
14
- import { Chunk, Duration, Effect, Either, identity, Option, ParseResult, pipe, Schedule, Schema, Stream, } from 'effect';
14
+ import { Array as Arr, Chunk, Duration, Effect, Either, identity, Option, ParseResult, pipe, Schedule, Schema, Stream, } from 'effect';
15
15
  import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
16
16
  import { CoreWallet } from './CoreWallet.js';
17
17
  import { V8 } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
18
18
  import { BlockHash, ZswapEventTip, ZswapEvents } from '@midnight-ntwrk/wallet-sdk-indexer-client';
19
19
  import { ConnectionHelper, HttpQueryClient, WsSubscriptionClient, } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
20
- import { SyncWalletError } from './WalletError.js';
20
+ import { OutOfOrderSyncUpdateError, SyncWalletError } from './WalletError.js';
21
21
  import { WsURL } from '@midnight-ntwrk/wallet-sdk-utilities/networking';
22
22
  import { EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
23
23
  /**
@@ -48,6 +48,26 @@ export const splitAtVersionBoundary = (items, versionOf, activeRange) => {
48
48
  : Option.some(ProtocolVersion.ProtocolVersion(BigInt(versionOf(signalling)))),
49
49
  };
50
50
  };
51
+ /**
52
+ * Checks that freshly delivered events arrive in strictly ascending id order.
53
+ *
54
+ * @remarks
55
+ * Order is all that can be checked, never contiguity: zswap events share one id sequence with dust and contract events
56
+ * in the indexer, so gaps in a zswap stream are normal. A skipped commitment-inserting event is caught by the
57
+ * ledger's own insertion check instead. An empty batch is trivially ordered.
58
+ * @param fresh The events left after the cursor filter, in source order.
59
+ * @param appliedIndex The highest event id already applied; the first fresh id must exceed it.
60
+ * @returns The batch unchanged, or the first id that fails to exceed its predecessor as an
61
+ * {@link OutOfOrderSyncUpdateError}.
62
+ */
63
+ const expectAscending = (fresh, appliedIndex) => pipe(Arr.zipWith(fresh, Arr.prepend(Arr.map(fresh, (item) => BigInt(item.id)), appliedIndex), (item, previous) => ({ item, previous })), Arr.findFirst(({ item, previous }) => BigInt(item.id) <= previous), Option.match({
64
+ onNone: () => Either.right(fresh),
65
+ onSome: ({ item, previous }) => Either.left(new OutOfOrderSyncUpdateError({
66
+ message: `Zswap event ${item.id} delivered at or below its predecessor ${previous}; batch refused, applied index stays at ${appliedIndex}`,
67
+ expected: previous,
68
+ received: BigInt(item.id),
69
+ })),
70
+ }));
51
71
  /** Records a batch's observed protocol version on the state, monotonically. A batch that observed none is a no-op. */
52
72
  export const annotateVersion = (state, observedVersion) => Option.match(observedVersion, {
53
73
  onNone: () => state,
@@ -334,7 +354,14 @@ export const makeEventsSyncCapability = () => {
334
354
  // the batch tail even when the tail belongs to the next protocol version.
335
355
  const lastUpdate = wrappedUpdate.updates.at(-1);
336
356
  const highestRelevantWalletIndex = BigInt(lastUpdate.maxId);
337
- const { applied, observedVersion } = splitAtVersionBoundary(freshUpdates, (update) => update.protocolVersion, activeRange);
357
+ // Refuse the whole batch: applying the sound prefix would move the cursor and hide the fault behind
358
+ // `isConnected: true`. State and cursor stay untouched, so the retry re-fetches the same range; a transient
359
+ // reorder heals, a persistent one fails visibly.
360
+ //
361
+ // `SyncCapability.applyUpdate` has no typed error channel, so throwing preserves its public tuple-returning API;
362
+ // `RunningV1Variant` catches this at the capability boundary. See #572 for the planned `Either`-based API.
363
+ const orderedUpdates = Either.getOrThrowWith(expectAscending(freshUpdates, appliedIndex), identity);
364
+ const { applied, observedVersion } = splitAtVersionBoundary(orderedUpdates, (update) => update.protocolVersion, activeRange);
338
365
  // Read here and nowhere earlier: `applied` is exactly the slice this variant owns, so nothing outside its
339
366
  // activation range — nor anything below its cursor — is ever handed to this ledger version's deserializer.
340
367
  const [newState, newChanges] = applied.length === 0
@@ -132,7 +132,8 @@ export class TransactingCapabilityImplementation {
132
132
  feeTokenType: '',
133
133
  coinSelection,
134
134
  createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
135
- isCoinEqual: (a, b) => a.type === b.type && a.value === b.value,
135
+ // Distinct coins can share a type and a value; only the nonce tells them apart.
136
+ isCoinEqual: (a, b) => a.nonce === b.nonce,
136
137
  });
137
138
  return this.#prepareOffer(secretKeys, newState, fallibleBalanceRecipe, segment);
138
139
  },
@@ -4,7 +4,7 @@ export declare const WalletError: {
4
4
  other(err: unknown): WalletError;
5
5
  submission(err: unknown): WalletError;
6
6
  };
7
- export type WalletError = OtherWalletError | InsufficientFundsError | SubmissionError | AddressError | SyncWalletError | InvalidCoinHashesError | TransactingError | LedgerOps.LedgerError;
7
+ export type WalletError = OtherWalletError | InsufficientFundsError | SubmissionError | AddressError | SyncWalletError | InvalidCoinHashesError | TransactingError | OutOfOrderSyncUpdateError | LedgerOps.LedgerError;
8
8
  declare const OtherWalletError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
9
9
  readonly _tag: "Wallet.Other";
10
10
  } & Readonly<A>;
@@ -71,4 +71,29 @@ export declare class TransactionHistoryError extends TransactionHistoryError_bas
71
71
  cause?: unknown;
72
72
  }> {
73
73
  }
74
+ declare const OutOfOrderSyncUpdateError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
75
+ readonly _tag: "Wallet.OutOfOrderSyncUpdate";
76
+ } & Readonly<A>;
77
+ /**
78
+ * A batch of zswap ledger events was not in ascending id order: an event arrived at or below the one before it, or at
79
+ * or below the applied cursor.
80
+ *
81
+ * @remarks
82
+ * Order is all the wallet can check: zswap events share one id sequence with dust and contract events in the indexer,
83
+ * so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
84
+ * whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
85
+ * place.
86
+ * @example
87
+ * ```ts
88
+ * if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
89
+ * ```
90
+ */
91
+ export declare class OutOfOrderSyncUpdateError extends OutOfOrderSyncUpdateError_base<{
92
+ readonly message: string;
93
+ /** The id the delivered event had to exceed: its predecessor in the batch, or the applied cursor. */
94
+ readonly expected: bigint;
95
+ /** The event id the source delivered instead. */
96
+ readonly received: bigint;
97
+ }> {
98
+ }
74
99
  export {};
@@ -51,3 +51,19 @@ export class TransactingError extends Data.TaggedError('Wallet.Transacting') {
51
51
  }
52
52
  export class TransactionHistoryError extends Data.TaggedError('Wallet.TransactionHistory') {
53
53
  }
54
+ /**
55
+ * A batch of zswap ledger events was not in ascending id order: an event arrived at or below the one before it, or at
56
+ * or below the applied cursor.
57
+ *
58
+ * @remarks
59
+ * Order is all the wallet can check: zswap events share one id sequence with dust and contract events in the indexer,
60
+ * so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
61
+ * whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
62
+ * place.
63
+ * @example
64
+ * ```ts
65
+ * if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
66
+ * ```
67
+ */
68
+ export class OutOfOrderSyncUpdateError extends Data.TaggedError('Wallet.OutOfOrderSyncUpdate') {
69
+ }
package/dist/v2/Sync.js CHANGED
@@ -11,13 +11,13 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  import * as ledger from '@midnightntwrk/ledger-v9';
14
- import { Chunk, Duration, Effect, Either, identity, Option, ParseResult, pipe, Schedule, Schema, Stream, } from 'effect';
14
+ import { Array as Arr, Chunk, Duration, Effect, Either, identity, Option, ParseResult, pipe, Schedule, Schema, Stream, } from 'effect';
15
15
  import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
16
16
  import { CoreWallet } from './CoreWallet.js';
17
17
  import { getLastBlock } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
18
18
  import { BlockHash, ZswapEventTip, ZswapEvents } from '@midnight-ntwrk/wallet-sdk-indexer-client';
19
19
  import { ConnectionHelper, HttpQueryClient, WsSubscriptionClient, } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
20
- import { SyncWalletError } from './WalletError.js';
20
+ import { OutOfOrderSyncUpdateError, SyncWalletError } from './WalletError.js';
21
21
  import { WsURL } from '@midnight-ntwrk/wallet-sdk-utilities/networking';
22
22
  import { EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
23
23
  /**
@@ -48,6 +48,26 @@ export const splitAtVersionBoundary = (items, versionOf, activeRange) => {
48
48
  : Option.some(ProtocolVersion.ProtocolVersion(BigInt(versionOf(signalling)))),
49
49
  };
50
50
  };
51
+ /**
52
+ * Checks that freshly delivered events arrive in strictly ascending id order.
53
+ *
54
+ * @remarks
55
+ * Order is all that can be checked, never contiguity: zswap events share one id sequence with dust and contract events
56
+ * in the indexer, so gaps in a zswap stream are normal. A skipped commitment-inserting event is caught by the
57
+ * ledger's own insertion check instead. An empty batch is trivially ordered.
58
+ * @param fresh The events left after the cursor filter, in source order.
59
+ * @param appliedIndex The highest event id already applied; the first fresh id must exceed it.
60
+ * @returns The batch unchanged, or the first id that fails to exceed its predecessor as an
61
+ * {@link OutOfOrderSyncUpdateError}.
62
+ */
63
+ const expectAscending = (fresh, appliedIndex) => pipe(Arr.zipWith(fresh, Arr.prepend(Arr.map(fresh, (item) => BigInt(item.id)), appliedIndex), (item, previous) => ({ item, previous })), Arr.findFirst(({ item, previous }) => BigInt(item.id) <= previous), Option.match({
64
+ onNone: () => Either.right(fresh),
65
+ onSome: ({ item, previous }) => Either.left(new OutOfOrderSyncUpdateError({
66
+ message: `Zswap event ${item.id} delivered at or below its predecessor ${previous}; batch refused, applied index stays at ${appliedIndex}`,
67
+ expected: previous,
68
+ received: BigInt(item.id),
69
+ })),
70
+ }));
51
71
  /** Records a batch's observed protocol version on the state, monotonically. A batch that observed none is a no-op. */
52
72
  export const annotateVersion = (state, observedVersion) => Option.match(observedVersion, {
53
73
  onNone: () => state,
@@ -343,7 +363,14 @@ export const makeEventsSyncCapability = () => {
343
363
  // the batch tail even when the tail belongs to the next protocol version.
344
364
  const lastUpdate = wrappedUpdate.updates.at(-1);
345
365
  const highestRelevantWalletIndex = BigInt(lastUpdate.maxId);
346
- const { applied, observedVersion } = splitAtVersionBoundary(freshUpdates, (update) => update.protocolVersion, activeRange);
366
+ // Refuse the whole batch: applying the sound prefix would move the cursor and hide the fault behind
367
+ // `isConnected: true`. State and cursor stay untouched, so the retry re-fetches the same range; a transient
368
+ // reorder heals, a persistent one fails visibly.
369
+ //
370
+ // `SyncCapability.applyUpdate` has no typed error channel, so throwing preserves its public tuple-returning API;
371
+ // `RunningV2Variant` catches this at the capability boundary. See #572 for the planned `Either`-based API.
372
+ const orderedUpdates = Either.getOrThrowWith(expectAscending(freshUpdates, appliedIndex), identity);
373
+ const { applied, observedVersion } = splitAtVersionBoundary(orderedUpdates, (update) => update.protocolVersion, activeRange);
347
374
  // Read here and nowhere earlier: `applied` is exactly the slice this variant owns, so nothing outside its
348
375
  // activation range — nor anything below its cursor — is ever handed to this ledger version's deserializer.
349
376
  const [newState, newChanges] = applied.length === 0
@@ -132,7 +132,8 @@ export class TransactingCapabilityImplementation {
132
132
  feeTokenType: '',
133
133
  coinSelection,
134
134
  createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
135
- isCoinEqual: (a, b) => a.type === b.type && a.value === b.value,
135
+ // Distinct coins can share a type and a value; only the nonce tells them apart.
136
+ isCoinEqual: (a, b) => a.nonce === b.nonce,
136
137
  });
137
138
  return this.#prepareOffer(secretKeys, newState, fallibleBalanceRecipe, segment);
138
139
  },
@@ -4,7 +4,7 @@ export declare const WalletError: {
4
4
  other(err: unknown): WalletError;
5
5
  submission(err: unknown): WalletError;
6
6
  };
7
- export type WalletError = OtherWalletError | InsufficientFundsError | SubmissionError | AddressError | SyncWalletError | InvalidCoinHashesError | TransactingError | LedgerOps.LedgerError;
7
+ export type WalletError = OtherWalletError | InsufficientFundsError | SubmissionError | AddressError | SyncWalletError | InvalidCoinHashesError | TransactingError | OutOfOrderSyncUpdateError | LedgerOps.LedgerError;
8
8
  declare const OtherWalletError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
9
9
  readonly _tag: "Wallet.Other";
10
10
  } & Readonly<A>;
@@ -71,4 +71,29 @@ export declare class TransactionHistoryError extends TransactionHistoryError_bas
71
71
  cause?: unknown;
72
72
  }> {
73
73
  }
74
+ declare const OutOfOrderSyncUpdateError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
75
+ readonly _tag: "Wallet.OutOfOrderSyncUpdate";
76
+ } & Readonly<A>;
77
+ /**
78
+ * A batch of zswap ledger events was not in ascending id order: an event arrived at or below the one before it, or at
79
+ * or below the applied cursor.
80
+ *
81
+ * @remarks
82
+ * Order is all the wallet can check: zswap events share one id sequence with dust and contract events in the indexer,
83
+ * so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
84
+ * whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
85
+ * place.
86
+ * @example
87
+ * ```ts
88
+ * if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
89
+ * ```
90
+ */
91
+ export declare class OutOfOrderSyncUpdateError extends OutOfOrderSyncUpdateError_base<{
92
+ readonly message: string;
93
+ /** The id the delivered event had to exceed: its predecessor in the batch, or the applied cursor. */
94
+ readonly expected: bigint;
95
+ /** The event id the source delivered instead. */
96
+ readonly received: bigint;
97
+ }> {
98
+ }
74
99
  export {};
@@ -51,3 +51,19 @@ export class TransactingError extends Data.TaggedError('Wallet.Transacting') {
51
51
  }
52
52
  export class TransactionHistoryError extends Data.TaggedError('Wallet.TransactionHistory') {
53
53
  }
54
+ /**
55
+ * A batch of zswap ledger events was not in ascending id order: an event arrived at or below the one before it, or at
56
+ * or below the applied cursor.
57
+ *
58
+ * @remarks
59
+ * Order is all the wallet can check: zswap events share one id sequence with dust and contract events in the indexer,
60
+ * so gaps in the stream are normal and a skipped event is only ever caught by the ledger's own insertion check. The
61
+ * whole batch is refused, nothing is applied and the cursor stays, so the running variant retries from the same
62
+ * place.
63
+ * @example
64
+ * ```ts
65
+ * if (error._tag === 'Wallet.OutOfOrderSyncUpdate') console.warn(`expected above ${error.expected}, got ${error.received}`);
66
+ * ```
67
+ */
68
+ export class OutOfOrderSyncUpdateError extends Data.TaggedError('Wallet.OutOfOrderSyncUpdate') {
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midnight-ntwrk/wallet-sdk-shielded",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0-beta.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -35,12 +35,12 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@midnight-ntwrk/ledger-v8": "^8.1.0",
38
- "@midnightntwrk/ledger-v9": "1.0.0-rc.4",
39
- "@midnight-ntwrk/wallet-sdk-abstractions": "3.0.0-beta.1",
40
- "@midnight-ntwrk/wallet-sdk-address-format": "4.0.0-beta.3",
41
- "@midnight-ntwrk/wallet-sdk-capabilities": "4.0.0-beta.3",
38
+ "@midnightntwrk/ledger-v9": "1.0.0-rc.5",
39
+ "@midnight-ntwrk/wallet-sdk-abstractions": "3.0.0-beta.2",
40
+ "@midnight-ntwrk/wallet-sdk-address-format": "4.0.0-beta.4",
41
+ "@midnight-ntwrk/wallet-sdk-capabilities": "4.0.0-beta.4",
42
42
  "@midnight-ntwrk/wallet-sdk-indexer-client": "2.0.0-beta.2",
43
- "@midnight-ntwrk/wallet-sdk-runtime": "1.1.0-beta.1",
43
+ "@midnight-ntwrk/wallet-sdk-runtime": "1.1.0-beta.2",
44
44
  "@midnight-ntwrk/wallet-sdk-utilities": "1.2.2-beta.0",
45
45
  "effect": "^3.19.19",
46
46
  "rxjs": "^7.8.2"