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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +11 -6
  2. package/dist/Restore.d.ts +40 -0
  3. package/dist/Restore.js +63 -0
  4. package/dist/ShieldedWallet.d.ts +248 -62
  5. package/dist/ShieldedWallet.js +366 -76
  6. package/dist/ShieldedWalletAPI.d.ts +179 -0
  7. package/dist/ShieldedWalletAPI.js +75 -0
  8. package/dist/SingleVariantShieldedWallet.d.ts +35 -0
  9. package/dist/SingleVariantShieldedWallet.js +240 -0
  10. package/dist/index.d.ts +4 -1
  11. package/dist/index.js +4 -1
  12. package/dist/v1/CoinsAndBalances.d.ts +1 -1
  13. package/dist/v1/CoreWallet.d.ts +48 -1
  14. package/dist/v1/CoreWallet.js +54 -1
  15. package/dist/v1/Migration.d.ts +95 -0
  16. package/dist/v1/Migration.js +82 -0
  17. package/dist/v1/RunningV1Variant.d.ts +1 -1
  18. package/dist/v1/RunningV1Variant.js +37 -8
  19. package/dist/v1/Serialization.js +1 -1
  20. package/dist/v1/Sync.d.ts +132 -9
  21. package/dist/v1/Sync.js +244 -35
  22. package/dist/v1/Transacting.d.ts +1 -1
  23. package/dist/v1/Transacting.js +1 -1
  24. package/dist/v1/TransactionHistory.d.ts +9 -2
  25. package/dist/v1/TransactionHistory.js +26 -5
  26. package/dist/v1/TransactionOps.d.ts +1 -1
  27. package/dist/v1/V1Builder.d.ts +87 -26
  28. package/dist/v1/V1Builder.js +109 -8
  29. package/dist/v1/WalletError.d.ts +1 -1
  30. package/dist/v1/index.d.ts +2 -1
  31. package/dist/v1/index.js +5 -1
  32. package/dist/v2/CoinsAndBalances.d.ts +24 -0
  33. package/dist/v2/CoinsAndBalances.js +41 -0
  34. package/dist/v2/CoreWallet.d.ts +162 -0
  35. package/dist/v2/CoreWallet.js +305 -0
  36. package/dist/v2/Keys.d.ts +8 -0
  37. package/dist/v2/Keys.js +16 -0
  38. package/dist/v2/Migration.d.ts +112 -0
  39. package/dist/v2/Migration.js +96 -0
  40. package/dist/v2/RunningV2Variant.d.ts +40 -0
  41. package/dist/v2/RunningV2Variant.js +127 -0
  42. package/dist/v2/Serialization.d.ts +12 -0
  43. package/dist/v2/Serialization.js +93 -0
  44. package/dist/v2/Sync.d.ts +208 -0
  45. package/dist/v2/Sync.js +436 -0
  46. package/dist/v2/Transacting.d.ts +50 -0
  47. package/dist/v2/Transacting.js +258 -0
  48. package/dist/v2/TransactionHistory.d.ts +100 -0
  49. package/dist/v2/TransactionHistory.js +132 -0
  50. package/dist/v2/TransactionImbalances.d.ts +10 -0
  51. package/dist/v2/TransactionImbalances.js +30 -0
  52. package/dist/v2/TransactionOps.d.ts +17 -0
  53. package/dist/v2/TransactionOps.js +73 -0
  54. package/dist/v2/V2Builder.d.ts +154 -0
  55. package/dist/v2/V2Builder.js +273 -0
  56. package/dist/v2/WalletError.d.ts +74 -0
  57. package/dist/v2/WalletError.js +53 -0
  58. package/dist/v2/index.d.ts +13 -0
  59. package/dist/v2/index.js +26 -0
  60. package/package.json +16 -8
@@ -0,0 +1,305 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) Midnight Foundation
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // You may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ import * as ledger from '@midnightntwrk/ledger-v9';
14
+ import { ProtocolVersion, SyncProgress } from '@midnightntwrk/wallet-sdk-abstractions';
15
+ import { LedgerOps } from '@midnightntwrk/wallet-sdk-utilities';
16
+ import { Either, Iterable, pipe, Record, Array as Arr } from 'effect';
17
+ import { InvalidCoinHashesError } from './WalletError.js';
18
+ export const PublicKeys = {
19
+ fromSecretKeys: (secretKeys) => {
20
+ return {
21
+ coinPublicKey: secretKeys.coinPublicKey,
22
+ encryptionPublicKey: secretKeys.encryptionPublicKey,
23
+ };
24
+ },
25
+ };
26
+ export const CoinHashesMap = {
27
+ empty: {},
28
+ pickAllCoins(state) {
29
+ return [...state.coins, ...state.pendingOutputs.values().map(([coin]) => coin)];
30
+ },
31
+ assertValid(map, state) {
32
+ const coins = CoinHashesMap.pickAllCoins(state);
33
+ const coinNonces = new Set(Iterable.map(coins, (coin) => coin.nonce));
34
+ const definedNonces = new Set(Object.keys(map));
35
+ const missingNonces = coinNonces.difference(definedNonces);
36
+ return missingNonces.size === 0 ? Either.void : Either.left(missingNonces);
37
+ },
38
+ updateWithCoins(secretKeys, existing, coins) {
39
+ return Record.fromIterableWith(coins, (coin) => [
40
+ coin.nonce,
41
+ existing[coin.nonce] ?? {
42
+ commitment: ledger.coinCommitment(coin, secretKeys.coinPublicKey),
43
+ nullifier: ledger.coinNullifier(coin, secretKeys.coinSecretKey),
44
+ },
45
+ ]);
46
+ },
47
+ updateWithNewCoins(secretKeys, existing, coins) {
48
+ const newMap = CoinHashesMap.updateWithCoins(secretKeys, CoinHashesMap.empty, coins);
49
+ return Record.union(existing, newMap, (a) => a);
50
+ },
51
+ init(secretKeys, coins) {
52
+ return CoinHashesMap.updateWithCoins(secretKeys, {}, coins);
53
+ },
54
+ };
55
+ /**
56
+ * Releases every coin reservation a wallet carried across the ledger-version boundary.
57
+ *
58
+ * @remarks
59
+ * `pendingSpends` crosses with the rest of the local state, and on the far side its entries name transactions of a
60
+ * ledger version the chain has left behind: none of them can be included, so no inclusion will ever clear them, and a
61
+ * coin held for one would sit outside the available set for the rest of the wallet's life. Re-deriving each spend and
62
+ * reporting it failed is the ledger's own way of un-booking one: the reservation goes, and the coin stays at the
63
+ * Merkle index the chain gave it, in a tree of unchanged height and root.
64
+ * @param state The local state as it crossed.
65
+ * @param secretKeys The keys the first sync update arrived with.
66
+ * @returns The same state with nothing reserved.
67
+ */
68
+ const releaseStrandedSpends = (state, secretKeys) => pipe([...state.pendingSpends.values()], Arr.reduce(state, (released, [coin]) => pipe(LedgerOps.ledgerTry(() => {
69
+ const [reserved, input] = released.spend(secretKeys, coin, 0);
70
+ return reserved.applyFailed(ledger.ZswapOffer.fromInput(input, coin.type, coin.value));
71
+ }),
72
+ // A reservation the ledger declines to re-derive is left exactly where it already was: one stranded coin is
73
+ // not worth failing the update every other coin in this wallet is waiting on.
74
+ Either.getOrElse(() => released))));
75
+ export const CoreWallet = {
76
+ init(localState, secretKeys, networkId) {
77
+ const publicKeys = PublicKeys.fromSecretKeys(secretKeys);
78
+ const coinHashes = CoinHashesMap.init(secretKeys, CoinHashesMap.pickAllCoins(localState));
79
+ const progress = SyncProgress.createSyncProgress();
80
+ const protocolVersion = ProtocolVersion.MinSupportedVersion;
81
+ return { state: localState, publicKeys, networkId, coinHashes, progress, protocolVersion };
82
+ },
83
+ empty(publicKeys, networkId) {
84
+ return {
85
+ state: new ledger.ZswapLocalState(),
86
+ publicKeys,
87
+ networkId,
88
+ coinHashes: CoinHashesMap.empty,
89
+ progress: SyncProgress.createSyncProgress(),
90
+ protocolVersion: ProtocolVersion.MinSupportedVersion,
91
+ };
92
+ },
93
+ restore(localState, secretKeys, syncProgress, protocolVersion, networkId) {
94
+ const publicKeys = PublicKeys.fromSecretKeys(secretKeys);
95
+ const coinHashes = CoinHashesMap.init(secretKeys, CoinHashesMap.pickAllCoins(localState));
96
+ return {
97
+ state: localState,
98
+ publicKeys,
99
+ networkId,
100
+ coinHashes,
101
+ progress: SyncProgress.createSyncProgress(syncProgress),
102
+ protocolVersion: ProtocolVersion.ProtocolVersion(protocolVersion),
103
+ };
104
+ },
105
+ restoreWithCoinHashes(publicKeys, localState, coinHashes, syncProgress, protocolVersion, networkId) {
106
+ return CoinHashesMap.assertValid(coinHashes, localState).pipe(Either.mapBoth({
107
+ onLeft: (missingNonces) => new InvalidCoinHashesError({ message: 'Missing coin hashes for coins present in the state', missingNonces }),
108
+ onRight: () => ({
109
+ state: localState,
110
+ publicKeys,
111
+ networkId,
112
+ coinHashes,
113
+ progress: SyncProgress.createSyncProgress(syncProgress),
114
+ protocolVersion: ProtocolVersion.ProtocolVersion(protocolVersion),
115
+ }),
116
+ }));
117
+ },
118
+ /**
119
+ * Restores a wallet whose coin hashes were never computed — a snapshot taken mid-crossing.
120
+ *
121
+ * @remarks
122
+ * The counterpart of {@link CoreWallet.restoreWithCoinHashes} for the one shape that validation would otherwise
123
+ * refuse: a full local state, carried across the ledger-version boundary as bytes, beside an empty hash map. The
124
+ * hashes are commitments and nullifiers, and computing them needs the secret keys a deserializer does not hold, so
125
+ * the wallet comes back still marked pending and the first sync update fills them in
126
+ * ({@link CoreWallet.resolveCoinHashes}). Total: there is nothing here to reject, which is exactly why it is a
127
+ * separate entry point rather than a flag on the validating one.
128
+ * @param publicKeys The identity the snapshot declared.
129
+ * @param localState The local state the snapshot carried.
130
+ * @param syncProgress Where the snapshot's reading had got to.
131
+ * @param protocolVersion The version the snapshot was written under.
132
+ * @param networkId The network the snapshot claims.
133
+ * @returns A wallet holding that state, its coin hashes still pending.
134
+ */
135
+ restoreWithPendingCoinHashes(publicKeys, localState, syncProgress, protocolVersion, networkId) {
136
+ return {
137
+ state: localState,
138
+ publicKeys,
139
+ networkId,
140
+ coinHashes: CoinHashesMap.empty,
141
+ coinHashesPending: true,
142
+ progress: SyncProgress.createSyncProgress(syncProgress),
143
+ protocolVersion: ProtocolVersion.ProtocolVersion(protocolVersion),
144
+ };
145
+ },
146
+ initEmpty(keys, networkId) {
147
+ return this.empty(PublicKeys.fromSecretKeys(keys), networkId);
148
+ },
149
+ /**
150
+ * Records an observed protocol version on the wallet, never going backwards.
151
+ *
152
+ * @remarks
153
+ * The version is a signal, not a measurement: writing one outside the running variant's activation range is what
154
+ * makes it hand over to the next variant. A source that briefly reports an older version — a reconnect replaying
155
+ * from an earlier cursor, say — must therefore not be able to drag the wallet back below a boundary it has already
156
+ * crossed, which would ask the runtime to migrate backwards. Taking the maximum makes that unrepresentable.
157
+ * @param wallet The wallet to annotate.
158
+ * @param version The protocol version just observed.
159
+ * @returns `wallet` unchanged if it already records `version` or a later one, otherwise a copy recording `version`.
160
+ */
161
+ withProtocolVersion(wallet, version) {
162
+ return version > wallet.protocolVersion ? { ...wallet, protocolVersion: version } : wallet;
163
+ },
164
+ /**
165
+ * Adopts a wallet inherited from the previous ledger version: its local state, its identity, and its position.
166
+ *
167
+ * @remarks
168
+ * The state passed in is this ledger version's own object, decoded from the previous version's bytes — the two majors
169
+ * share the `zswap-local-state` codec, so the crossing is a round-trip rather than a reconstruction (see
170
+ * `Migration.makeCrossLedgerMigration`, and the characterization test that pins the codec). So everything the
171
+ * previous wallet's tree held arrives intact: its coins at the Merkle indices the chain gave them, the height it
172
+ * had reached, and the outputs it was still expecting. What also crosses is the public keys, the network, the
173
+ * protocol version that triggered the hand-over (kept so the new variant starts inside its own activation range
174
+ * rather than immediately signalling backwards), and the cursor the previous variant stopped at.
175
+ *
176
+ * **Sync progress is parked at the fork, not rewound**: the previous wallet's cursor crosses unchanged. The indexer
177
+ * numbers ledger-v9 events onwards from whatever id it had reached when the fork happened, never from zero, so
178
+ * resuming from the inherited cursor is what puts this wallet in front of them. Rewinding to zero would park it on
179
+ * a stretch of history that this ledger version's events do not occupy. What does not cross is `isConnected`: no
180
+ * sync is running behind this state yet.
181
+ *
182
+ * The one thing the bytes cannot supply is the coin hashes: commitments and nullifiers are computed from the secret
183
+ * keys, which a migration by design does not hold. They are therefore left empty and marked
184
+ * {@link CoreWallet.coinHashesPending}, for {@link CoreWallet.completeCrossing} to fill in at the first sync update —
185
+ * the first place in this variant where keys and state meet. The same step releases the spends the state crossed
186
+ * with: those transactions belong to the ledger version the chain has left behind and can never be included, so
187
+ * nothing else would ever free the coins they reserved.
188
+ * @param previous The identity and position read off the previous ledger version's wallet, and its decoded state.
189
+ * @returns A wallet of this ledger version holding what its predecessor held, positioned at the fork.
190
+ */
191
+ fromPreviousVersion(previous) {
192
+ return {
193
+ state: previous.state,
194
+ publicKeys: previous.publicKeys,
195
+ networkId: previous.networkId,
196
+ coinHashes: CoinHashesMap.empty,
197
+ coinHashesPending: true,
198
+ progress: SyncProgress.createSyncProgress({ ...previous.progress, isConnected: false }),
199
+ protocolVersion: ProtocolVersion.ProtocolVersion(previous.protocolVersion),
200
+ };
201
+ },
202
+ /**
203
+ * Finishes a crossing at the first sync update: the reservations released, then the coin hashes computed.
204
+ *
205
+ * @remarks
206
+ * The other half of {@link CoreWallet.fromPreviousVersion}. A migrated wallet arrives holding its whole local state
207
+ * but nothing it needs the secret keys for — no commitments or nullifiers, and no way to un-book the spends it
208
+ * carried — because a migration is by design handed no key material. The first sync update carries it, so this is
209
+ * where both gaps close: {@link releaseStrandedSpends} first, since a released coin is one more coin the hashes have
210
+ * to name, and {@link CoreWallet.resolveCoinHashes} over the state that comes back. Applied at the head of both sync
211
+ * capabilities, before anything else they do, so a batch that turns out to be empty still runs it: a wallet that
212
+ * crossed into a quiet timeline must not be left unable to name or to spend the coins it crossed with.
213
+ *
214
+ * Idempotent and self-clearing: without the marker this is the identity, so it costs a field read on every update
215
+ * thereafter and nothing else. That is also what keeps it away from a reservation made on this side of the
216
+ * boundary, whose transaction is live and whose coins are genuinely spoken for.
217
+ * @param wallet The wallet to complete.
218
+ * @param secretKeys The keys the update arrived with.
219
+ * @returns `wallet` unchanged if it never crossed, otherwise a copy whose coins are nameable and spendable again.
220
+ */
221
+ completeCrossing(wallet, secretKeys) {
222
+ return wallet.coinHashesPending === undefined
223
+ ? wallet
224
+ : CoreWallet.resolveCoinHashes({ ...wallet, state: releaseStrandedSpends(wallet.state, secretKeys) }, secretKeys);
225
+ },
226
+ /**
227
+ * Computes the coin hashes a wallet crossed the ledger-version boundary without.
228
+ *
229
+ * @remarks
230
+ * The hash half of {@link CoreWallet.completeCrossing}, which is what the sync capabilities call: a crossing has a
231
+ * second gap to close beside this one, so a caller finishing one should go through the combined step rather than
232
+ * here. A migrated wallet arrives holding its whole local state but no commitments or nullifiers for it, because
233
+ * deriving those needs the secret keys, and the first sync update is what carries them.
234
+ *
235
+ * Idempotent and self-clearing: without the marker this is the identity.
236
+ * @param wallet The wallet to complete.
237
+ * @param secretKeys The keys the update arrived with.
238
+ * @returns `wallet` unchanged if its hashes were never pending, otherwise a copy holding them.
239
+ */
240
+ resolveCoinHashes(wallet, secretKeys) {
241
+ return wallet.coinHashesPending === undefined
242
+ ? wallet
243
+ : {
244
+ state: wallet.state,
245
+ publicKeys: wallet.publicKeys,
246
+ networkId: wallet.networkId,
247
+ coinHashes: CoinHashesMap.init(secretKeys, CoinHashesMap.pickAllCoins(wallet.state)),
248
+ progress: wallet.progress,
249
+ protocolVersion: wallet.protocolVersion,
250
+ };
251
+ },
252
+ applyCollapsedUpdate(wallet, collapsed) {
253
+ const newState = wallet.state.applyCollapsedUpdate(collapsed);
254
+ return { ...wallet, state: newState };
255
+ },
256
+ apply(wallet, secretKeys, offer) {
257
+ const newState = wallet.state.apply(secretKeys, offer);
258
+ const newCoinHashes = CoinHashesMap.updateWithCoins(secretKeys, wallet.coinHashes, CoinHashesMap.pickAllCoins(newState));
259
+ return { ...wallet, state: newState, coinHashes: newCoinHashes };
260
+ },
261
+ replayEventsWithChanges(wallet, secretKeys, events) {
262
+ const stateWithChanges = wallet.state.replayEventsWithChanges(secretKeys, events);
263
+ const newState = stateWithChanges.state;
264
+ const newCoinHashes = CoinHashesMap.updateWithCoins(secretKeys, wallet.coinHashes, CoinHashesMap.pickAllCoins(newState));
265
+ const updatedWallet = { ...wallet, state: newState, coinHashes: newCoinHashes };
266
+ return [updatedWallet, stateWithChanges.changes];
267
+ },
268
+ updateProgress(wallet, { appliedIndex, highestRelevantWalletIndex, highestIndex, highestRelevantIndex, isConnected, }) {
269
+ const updatedProgress = SyncProgress.createSyncProgress({
270
+ appliedIndex: appliedIndex ?? wallet.progress.appliedIndex,
271
+ highestRelevantWalletIndex: highestRelevantWalletIndex ?? wallet.progress.highestRelevantWalletIndex,
272
+ highestIndex: highestIndex ?? wallet.progress.highestIndex,
273
+ highestRelevantIndex: highestRelevantIndex ?? wallet.progress.highestRelevantIndex,
274
+ isConnected: isConnected ?? wallet.progress.isConnected,
275
+ });
276
+ return { ...wallet, progress: updatedProgress };
277
+ },
278
+ // TODO: Remove after tx history is implemented
279
+ addTransaction(wallet, _tx) {
280
+ return wallet;
281
+ },
282
+ /* not implemented until this is done https://shielded.atlassian.net/browse/PM-19678 */
283
+ revertTransaction(wallet, tx) {
284
+ const newState = wallet.state.revertTransaction(tx);
285
+ return { ...wallet, state: newState };
286
+ },
287
+ // TODO: Remove after tx history is implemented
288
+ updateTxHistory(wallet, _newTxs) {
289
+ return wallet;
290
+ },
291
+ spendCoins(wallet, secretKeys, coins, segment) {
292
+ const [offers, newLocalState] = pipe(coins, Arr.reduce([[], wallet.state], ([accOffers, localState], coinToSpend) => {
293
+ const [nextState, newInput] = localState.spend(secretKeys, coinToSpend, segment);
294
+ const inputOffer = ledger.ZswapOffer.fromInput(newInput, coinToSpend.type, coinToSpend.value);
295
+ return [accOffers.concat([inputOffer]), nextState];
296
+ }));
297
+ const updated = { ...wallet, state: newLocalState };
298
+ return [offers, updated];
299
+ },
300
+ watchCoins(wallet, secretKeys, coins) {
301
+ const newLocalState = coins.reduce((localState, coin) => localState.watchFor(wallet.publicKeys.coinPublicKey, coin), wallet.state);
302
+ const newCoinHashes = CoinHashesMap.updateWithNewCoins(secretKeys, wallet.coinHashes, coins);
303
+ return { ...wallet, state: newLocalState, coinHashes: newCoinHashes };
304
+ },
305
+ };
@@ -0,0 +1,8 @@
1
+ import type { CoreWallet } from './CoreWallet.js';
2
+ import { ShieldedAddress, ShieldedCoinPublicKey, ShieldedEncryptionPublicKey } from '@midnightntwrk/wallet-sdk-address-format';
3
+ export type KeysCapability<TState> = {
4
+ getCoinPublicKey(state: TState): ShieldedCoinPublicKey;
5
+ getEncryptionPublicKey(state: TState): ShieldedEncryptionPublicKey;
6
+ getAddress(state: TState): ShieldedAddress;
7
+ };
8
+ export declare const makeDefaultKeysCapability: () => KeysCapability<CoreWallet>;
@@ -0,0 +1,16 @@
1
+ import { ShieldedAddress, ShieldedCoinPublicKey, ShieldedEncryptionPublicKey, } from '@midnightntwrk/wallet-sdk-address-format';
2
+ export const makeDefaultKeysCapability = () => {
3
+ return {
4
+ getCoinPublicKey: (state) => {
5
+ return new ShieldedCoinPublicKey(Buffer.from(state.publicKeys.coinPublicKey, 'hex'));
6
+ },
7
+ getEncryptionPublicKey: (state) => {
8
+ return new ShieldedEncryptionPublicKey(Buffer.from(state.publicKeys.encryptionPublicKey, 'hex'));
9
+ },
10
+ getAddress: (state) => {
11
+ const coinPublicKey = new ShieldedCoinPublicKey(Buffer.from(state.publicKeys.coinPublicKey, 'hex'));
12
+ const encryptionPublicKey = new ShieldedEncryptionPublicKey(Buffer.from(state.publicKeys.encryptionPublicKey, 'hex'));
13
+ return new ShieldedAddress(coinPublicKey, encryptionPublicKey);
14
+ },
15
+ };
16
+ };
@@ -0,0 +1,112 @@
1
+ import { type NetworkId, type SyncProgress } from '@midnightntwrk/wallet-sdk-abstractions';
2
+ import { Effect } from 'effect';
3
+ import { CoreWallet } from './CoreWallet.js';
4
+ import { type WalletError } from './WalletError.js';
5
+ /**
6
+ * How this variant produces its first state from whatever came before it.
7
+ *
8
+ * @remarks
9
+ * The strategy is injected through the builder rather than hard-coded into the variant, because "whatever came before"
10
+ * is not one thing: an empty wallet at first start, a wallet of this same ledger version, or a wallet of the ledger
11
+ * version this one replaced. Those are different functions with different input types, and the runtime only ever
12
+ * needs one of them per registration — so which one is a build-time decision, expressed as a dictionary in the
13
+ * repository's usual dictionary-passing style.
14
+ * @typeParam TPreviousState The state this migration consumes.
15
+ */
16
+ export type StateMigration<TPreviousState> = {
17
+ migrate: (previousState: TPreviousState) => Effect.Effect<CoreWallet, WalletError>;
18
+ };
19
+ export type EmptyWalletMigrationConfiguration = {
20
+ networkId: NetworkId.NetworkId;
21
+ };
22
+ /**
23
+ * The migration used when there is no previous wallet at all.
24
+ *
25
+ * @remarks
26
+ * It backs `WalletLike.startEmpty`, which builds a wallet before any key material exists. The resulting state has no
27
+ * coins and public keys derived from a fixed placeholder seed; it is a scaffold to be replaced by a real start, not a
28
+ * wallet anybody can transact with. Preserved verbatim from the ledger-v8 implementation so that `startEmpty` behaves
29
+ * exactly as it did.
30
+ * @example
31
+ * ```typescript
32
+ * const builder = new V2Builder().withDefaults().withMigration(makeEmptyWalletMigration({ networkId }));
33
+ * ```;
34
+ *
35
+ * @param configuration Supplies the network the scaffold state claims to be on.
36
+ * @returns A migration from `null`.
37
+ */
38
+ export declare const makeEmptyWalletMigration: (configuration: EmptyWalletMigrationConfiguration) => StateMigration<null>;
39
+ /**
40
+ * The migration between two variants that share this ledger version.
41
+ *
42
+ * @remarks
43
+ * Both sides speak the same state type, so the carry is the identity: local Merkle tree, coins, coin hashes and sync
44
+ * progress all remain valid, and nothing has to be re-read. This is the right strategy for a protocol bump that does
45
+ * not change serialization.
46
+ * @returns A migration from a {@link CoreWallet} of this ledger version.
47
+ */
48
+ export declare const makeCarryOverMigration: () => StateMigration<CoreWallet>;
49
+ /**
50
+ * The shape a wallet of the _previous_ ledger version must expose to be carried across a hard fork.
51
+ *
52
+ * @remarks
53
+ * Structural on purpose. The previous variant's `CoreWallet` is built on a different ledger module, and naming that
54
+ * module here would drag a second multi-megabyte WASM binary into this variant for the sake of a type. Everything the
55
+ * crossing reads is plain data — the ledger's key and network types are all string aliases — with one exception,
56
+ * `state`, which is named by the single capability the crossing uses: it can serialize itself. Both ledger versions'
57
+ * `ZswapLocalState` satisfies that as it is, and describing it that narrowly is what keeps this type version-agnostic
58
+ * — the bytes are the interface, not the object (see {@link makeCrossLedgerMigration}).
59
+ *
60
+ * `progress` is where the migrated wallet resumes from: the ledger-v9 timeline continues the indexer's event ids rather
61
+ * than restarting them, so the previous variant's cursor is the position the next one has to start at (see
62
+ * {@link makeCrossLedgerMigration}).
63
+ */
64
+ export type PreviousLedgerWallet = Readonly<{
65
+ publicKeys: {
66
+ readonly coinPublicKey: string;
67
+ readonly encryptionPublicKey: string;
68
+ };
69
+ networkId: string;
70
+ protocolVersion: bigint;
71
+ progress: SyncProgress.SyncProgressData;
72
+ state: Readonly<{
73
+ serialize: () => Uint8Array;
74
+ }>;
75
+ }>;
76
+ /**
77
+ * The migration across a ledger-version boundary: the local state crosses as bytes, identity and position beside it.
78
+ *
79
+ * @remarks
80
+ * The chain's state translation carries every commitment across the fork in place — the ledger-v9 tree continues at the
81
+ * index the ledger-v8 tree reached, and the indexer does **not** replay the ledger-v8 timeline as new-version events.
82
+ * So the wallet's own state must cross with it, or the coins are gone.
83
+ *
84
+ * It crosses as bytes. The two ledger majors either side of this boundary serialize `ZswapLocalState` under the same
85
+ * `zswap-local-state` codec — the transaction codec moved at the fork, this one did not — so the previous version's
86
+ * serialization is something this version can simply read. The crossing is therefore a round-trip and not a
87
+ * reconstruction, and everything survives it: the coins at the Merkle indices the chain gave them, the height the
88
+ * tree had reached, and the outputs the wallet was still expecting, which no reconstruction from spendable coins
89
+ * could have recovered. `src/v2/test/byteCrossing.test.ts` pins that codec against both real ledgers, and states what
90
+ * to do if it ever goes red.
91
+ *
92
+ * **The coins booked for transactions still in flight cross too, and are released at the first sync update.** Those
93
+ * transactions belong to the ledger version the chain has left behind, so none of them can ever be included and no
94
+ * inclusion will ever clear the reservations they left in `pendingSpends` — the coins would simply stop being
95
+ * spendable. The release cannot happen here, because un-booking a spend needs the secret keys a migration is by
96
+ * design handed none of; it happens at the first sync update instead, which is where keys and carried state meet
97
+ * (`CoreWallet.completeCrossing`). The change outputs those same transactions would have paid back are left as they
98
+ * are, and nothing ever removes them: these entries carry no TTL and `ZswapLocalState.clearPending` does nothing in
99
+ * either ledger, so they sit in `pendingOutputs` for the life of the wallet. A known limitation, and a mild one —
100
+ * they overstate the pending balance, but they reserve no coin and block no spending.
101
+ *
102
+ * Read through `LedgerOps.ledgerTry`, so a codec that has moved is a `WalletError` at the boundary and never a throw:
103
+ * the failure mode of a future major moving `zswap-local-state` is loud, and lands here, where the
104
+ * {@link StateMigration} seam can be given the ledger team's own translation instead.
105
+ *
106
+ * Sync progress is **parked at the fork** rather than rewound: the indexer numbers ledger-v9 events onwards from
107
+ * whatever id it had reached when the fork happened, never from zero, so the inherited cursor is exactly where this
108
+ * wallet's reading resumes. A wallet that rewound to zero would wait on a stretch of the timeline this ledger
109
+ * version's events do not occupy.
110
+ * @returns A migration from a previous-ledger-version wallet.
111
+ */
112
+ export declare const makeCrossLedgerMigration: () => StateMigration<PreviousLedgerWallet>;
@@ -0,0 +1,96 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) Midnight Foundation
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // You may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ import * as ledger from '@midnightntwrk/ledger-v9';
14
+ import { WalletSeed } from '@midnightntwrk/wallet-sdk-abstractions';
15
+ import { EitherOps, LedgerOps } from '@midnightntwrk/wallet-sdk-utilities';
16
+ import { Effect, Either, pipe } from 'effect';
17
+ import { CoreWallet, PublicKeys } from './CoreWallet.js';
18
+ /**
19
+ * The migration used when there is no previous wallet at all.
20
+ *
21
+ * @remarks
22
+ * It backs `WalletLike.startEmpty`, which builds a wallet before any key material exists. The resulting state has no
23
+ * coins and public keys derived from a fixed placeholder seed; it is a scaffold to be replaced by a real start, not a
24
+ * wallet anybody can transact with. Preserved verbatim from the ledger-v8 implementation so that `startEmpty` behaves
25
+ * exactly as it did.
26
+ * @example
27
+ * ```typescript
28
+ * const builder = new V2Builder().withDefaults().withMigration(makeEmptyWalletMigration({ networkId }));
29
+ * ```;
30
+ *
31
+ * @param configuration Supplies the network the scaffold state claims to be on.
32
+ * @returns A migration from `null`.
33
+ */
34
+ export const makeEmptyWalletMigration = (configuration) => ({
35
+ migrate: () => {
36
+ const seed = WalletSeed.fromString('0000000000000000000000000000000000000000000000000000000000000001');
37
+ return Effect.succeed(CoreWallet.empty(PublicKeys.fromSecretKeys(ledger.ZswapSecretKeys.fromSeed(seed)), configuration.networkId));
38
+ },
39
+ });
40
+ /**
41
+ * The migration between two variants that share this ledger version.
42
+ *
43
+ * @remarks
44
+ * Both sides speak the same state type, so the carry is the identity: local Merkle tree, coins, coin hashes and sync
45
+ * progress all remain valid, and nothing has to be re-read. This is the right strategy for a protocol bump that does
46
+ * not change serialization.
47
+ * @returns A migration from a {@link CoreWallet} of this ledger version.
48
+ */
49
+ export const makeCarryOverMigration = () => ({
50
+ migrate: (previousState) => Effect.succeed(previousState),
51
+ });
52
+ /**
53
+ * The migration across a ledger-version boundary: the local state crosses as bytes, identity and position beside it.
54
+ *
55
+ * @remarks
56
+ * The chain's state translation carries every commitment across the fork in place — the ledger-v9 tree continues at the
57
+ * index the ledger-v8 tree reached, and the indexer does **not** replay the ledger-v8 timeline as new-version events.
58
+ * So the wallet's own state must cross with it, or the coins are gone.
59
+ *
60
+ * It crosses as bytes. The two ledger majors either side of this boundary serialize `ZswapLocalState` under the same
61
+ * `zswap-local-state` codec — the transaction codec moved at the fork, this one did not — so the previous version's
62
+ * serialization is something this version can simply read. The crossing is therefore a round-trip and not a
63
+ * reconstruction, and everything survives it: the coins at the Merkle indices the chain gave them, the height the
64
+ * tree had reached, and the outputs the wallet was still expecting, which no reconstruction from spendable coins
65
+ * could have recovered. `src/v2/test/byteCrossing.test.ts` pins that codec against both real ledgers, and states what
66
+ * to do if it ever goes red.
67
+ *
68
+ * **The coins booked for transactions still in flight cross too, and are released at the first sync update.** Those
69
+ * transactions belong to the ledger version the chain has left behind, so none of them can ever be included and no
70
+ * inclusion will ever clear the reservations they left in `pendingSpends` — the coins would simply stop being
71
+ * spendable. The release cannot happen here, because un-booking a spend needs the secret keys a migration is by
72
+ * design handed none of; it happens at the first sync update instead, which is where keys and carried state meet
73
+ * (`CoreWallet.completeCrossing`). The change outputs those same transactions would have paid back are left as they
74
+ * are, and nothing ever removes them: these entries carry no TTL and `ZswapLocalState.clearPending` does nothing in
75
+ * either ledger, so they sit in `pendingOutputs` for the life of the wallet. A known limitation, and a mild one —
76
+ * they overstate the pending balance, but they reserve no coin and block no spending.
77
+ *
78
+ * Read through `LedgerOps.ledgerTry`, so a codec that has moved is a `WalletError` at the boundary and never a throw:
79
+ * the failure mode of a future major moving `zswap-local-state` is loud, and lands here, where the
80
+ * {@link StateMigration} seam can be given the ledger team's own translation instead.
81
+ *
82
+ * Sync progress is **parked at the fork** rather than rewound: the indexer numbers ledger-v9 events onwards from
83
+ * whatever id it had reached when the fork happened, never from zero, so the inherited cursor is exactly where this
84
+ * wallet's reading resumes. A wallet that rewound to zero would wait on a stretch of the timeline this ledger
85
+ * version's events do not occupy.
86
+ * @returns A migration from a previous-ledger-version wallet.
87
+ */
88
+ export const makeCrossLedgerMigration = () => ({
89
+ migrate: (previousState) => pipe(LedgerOps.ledgerTry(() => ledger.ZswapLocalState.deserialize(previousState.state.serialize())), Either.map((state) => CoreWallet.fromPreviousVersion({
90
+ state,
91
+ publicKeys: previousState.publicKeys,
92
+ networkId: previousState.networkId,
93
+ protocolVersion: previousState.protocolVersion,
94
+ progress: previousState.progress,
95
+ })), EitherOps.toEffect),
96
+ });
@@ -0,0 +1,40 @@
1
+ import type * as ledger from '@midnightntwrk/ledger-v9';
2
+ import { Effect, Scope, Stream } from 'effect';
3
+ import { type WalletRuntimeError, type Variant, StateChange } from '@midnightntwrk/wallet-sdk-runtime/abstractions';
4
+ import { type SerializationCapability } from './Serialization.js';
5
+ import { type ChangesResult, type EventsSyncUpdate, type SyncCapability, type SyncService } from './Sync.js';
6
+ import { type TransactingCapability, type TokenTransfer, type BalancingResult } from './Transacting.js';
7
+ import { type WalletError } from './WalletError.js';
8
+ import { type CoinsAndBalancesCapability } from './CoinsAndBalances.js';
9
+ import { type KeysCapability } from './Keys.js';
10
+ import { type CoinSelection } from '@midnightntwrk/wallet-sdk-capabilities';
11
+ import { type CoreWallet } from './CoreWallet.js';
12
+ import { type TransactionHistoryService } from './TransactionHistory.js';
13
+ export declare namespace RunningV2Variant {
14
+ type Context<TSerialized, TSyncUpdate, TTransaction, TStartAux> = {
15
+ serializationCapability: SerializationCapability<CoreWallet, null, TSerialized>;
16
+ syncService: SyncService<CoreWallet, TStartAux, TSyncUpdate>;
17
+ syncCapability: SyncCapability<CoreWallet, TSyncUpdate, ChangesResult>;
18
+ transactingCapability: TransactingCapability<ledger.ZswapSecretKeys, CoreWallet, TTransaction>;
19
+ coinsAndBalancesCapability: CoinsAndBalancesCapability<CoreWallet>;
20
+ keysCapability: KeysCapability<CoreWallet>;
21
+ coinSelection: CoinSelection<ledger.QualifiedShieldedCoinInfo>;
22
+ transactionHistoryService: TransactionHistoryService;
23
+ };
24
+ type AnyContext = Context<any, any, any, any>;
25
+ }
26
+ export declare const V2Tag: unique symbol;
27
+ export type DefaultRunningV2 = RunningV2Variant<string, EventsSyncUpdate, ledger.FinalizedTransaction, ledger.ZswapSecretKeys>;
28
+ export declare class RunningV2Variant<TSerialized, TSyncUpdate, TTransaction, TStartAux> implements Variant.RunningVariant<typeof V2Tag, CoreWallet> {
29
+ #private;
30
+ readonly __polyTag__: typeof V2Tag;
31
+ readonly state: Stream.Stream<StateChange.StateChange<CoreWallet>, WalletRuntimeError>;
32
+ constructor(scope: Scope.Scope, context: Variant.VariantContext<CoreWallet>, v2Context: RunningV2Variant.Context<TSerialized, TSyncUpdate, TTransaction, TStartAux>);
33
+ startSyncInBackground(startAux: TStartAux): Effect.Effect<void>;
34
+ startSync(startAux: TStartAux): Stream.Stream<void, WalletError, Scope.Scope>;
35
+ balanceTransaction(secretKeys: ledger.ZswapSecretKeys, tx: ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>): Effect.Effect<BalancingResult, WalletError>;
36
+ transferTransaction(secretKeys: ledger.ZswapSecretKeys, outputs: ReadonlyArray<TokenTransfer>): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
37
+ initSwap(secretKeys: ledger.ZswapSecretKeys, desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
38
+ revertTransaction(transaction: ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>): Effect.Effect<void, WalletError>;
39
+ serializeState(state: CoreWallet): TSerialized;
40
+ }