@midnight-ntwrk/wallet-sdk-shielded 4.0.0-beta.2 → 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.
Files changed (61) hide show
  1. package/README.md +18 -9
  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 +272 -36
  22. package/dist/v1/Transacting.d.ts +1 -1
  23. package/dist/v1/Transacting.js +3 -2
  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 +27 -2
  30. package/dist/v1/WalletError.js +16 -0
  31. package/dist/v1/index.d.ts +2 -1
  32. package/dist/v1/index.js +5 -1
  33. package/dist/v2/CoinsAndBalances.d.ts +24 -0
  34. package/dist/v2/CoinsAndBalances.js +41 -0
  35. package/dist/v2/CoreWallet.d.ts +162 -0
  36. package/dist/v2/CoreWallet.js +305 -0
  37. package/dist/v2/Keys.d.ts +8 -0
  38. package/dist/v2/Keys.js +16 -0
  39. package/dist/v2/Migration.d.ts +112 -0
  40. package/dist/v2/Migration.js +96 -0
  41. package/dist/v2/RunningV2Variant.d.ts +40 -0
  42. package/dist/v2/RunningV2Variant.js +127 -0
  43. package/dist/v2/Serialization.d.ts +12 -0
  44. package/dist/v2/Serialization.js +93 -0
  45. package/dist/v2/Sync.d.ts +208 -0
  46. package/dist/v2/Sync.js +463 -0
  47. package/dist/v2/Transacting.d.ts +50 -0
  48. package/dist/v2/Transacting.js +259 -0
  49. package/dist/v2/TransactionHistory.d.ts +100 -0
  50. package/dist/v2/TransactionHistory.js +132 -0
  51. package/dist/v2/TransactionImbalances.d.ts +10 -0
  52. package/dist/v2/TransactionImbalances.js +30 -0
  53. package/dist/v2/TransactionOps.d.ts +17 -0
  54. package/dist/v2/TransactionOps.js +73 -0
  55. package/dist/v2/V2Builder.d.ts +154 -0
  56. package/dist/v2/V2Builder.js +273 -0
  57. package/dist/v2/WalletError.d.ts +99 -0
  58. package/dist/v2/WalletError.js +69 -0
  59. package/dist/v2/index.d.ts +13 -0
  60. package/dist/v2/index.js +26 -0
  61. package/package.json +16 -8
@@ -10,108 +10,367 @@
10
10
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
- import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
14
- import { V1Builder, V1Tag, CoreWallet, } from './v1/index.js';
15
- import * as ledger from '@midnightntwrk/ledger-v9';
16
- import { Effect, Either } from 'effect';
17
- import * as rx from 'rxjs';
13
+ import { ProtocolVersion, WalletSeed, WalletTransaction, } from '@midnight-ntwrk/wallet-sdk-abstractions';
14
+ import { makeIndexerChainVersionProbe, } from '@midnight-ntwrk/wallet-sdk-capabilities/chainVersion';
18
15
  import { WalletBuilder } from '@midnight-ntwrk/wallet-sdk-runtime';
19
- export class ShieldedWalletState {
20
- static mapState = (variant) => (state) => {
21
- const { serialization, coinsAndBalances, keys } = variant;
22
- const { transactionHistory } = variant;
23
- return new ShieldedWalletState(state, { serialization, coinsAndBalances, keys }, { transactionHistory });
24
- };
25
- protocolVersion;
26
- state;
27
- capabilities;
28
- services;
29
- get balances() {
30
- return this.capabilities.coinsAndBalances.getAvailableBalances(this.state);
31
- }
32
- get totalCoins() {
33
- return this.capabilities.coinsAndBalances.getTotalCoins(this.state);
34
- }
35
- get availableCoins() {
36
- return this.capabilities.coinsAndBalances.getAvailableCoins(this.state);
37
- }
38
- get pendingCoins() {
39
- return this.capabilities.coinsAndBalances.getPendingCoins(this.state);
40
- }
41
- get coinPublicKey() {
42
- return this.capabilities.keys.getCoinPublicKey(this.state);
43
- }
44
- get encryptionPublicKey() {
45
- return this.capabilities.keys.getEncryptionPublicKey(this.state);
46
- }
47
- get address() {
48
- return this.capabilities.keys.getAddress(this.state);
49
- }
50
- get progress() {
51
- return this.state.progress;
52
- }
53
- constructor(state, capabilities, services) {
54
- this.protocolVersion = state.version;
55
- this.state = state.state;
56
- this.capabilities = capabilities;
57
- this.services = services;
58
- }
59
- serialize() {
60
- return this.capabilities.serialization.serialize(this.state);
61
- }
62
- }
63
- export function ShieldedWallet(configuration) {
64
- return CustomShieldedWallet(configuration, new V1Builder().withDefaults());
65
- }
66
- export function CustomShieldedWallet(configuration, builder) {
67
- const buildArgs = [configuration];
16
+ import { StartMaterial, Variant, } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
17
+ import { EitherOps, HList } from '@midnight-ntwrk/wallet-sdk-utilities';
18
+ import { Duration, Effect, Either, Option, Ref, pipe } from 'effect';
19
+ import * as rx from 'rxjs';
20
+ import { variantForSnapshot } from './Restore.js';
21
+ import { ShieldedWalletState, } from './ShieldedWalletAPI.js';
22
+ import { CoreWallet as V1CoreWallet, V1Builder, V1Tag } from './v1/index.js';
23
+ import { CoreWallet, Migration, V2Builder, V2Tag } from './v2/index.js';
24
+ /**
25
+ * Builds a shielded wallet class over a variant either side of a protocol boundary.
26
+ *
27
+ * @remarks
28
+ * The boundary is `configuration.forks.v9` and nothing else: the V1 variant is registered from the minimum supported
29
+ * version and the ledger-v9 one from the fork version, so the version at which the runtime hands over and the version
30
+ * at which each variant stops applying are the same number, taken from one place.
31
+ *
32
+ * Each variant is built from its own configuration. What the wallet layer needs — the network it is on and where the
33
+ * boundary lies — it takes separately, because neither variant knows there is another one.
34
+ * @example
35
+ * ```typescript
36
+ * const Wallet = CustomForkingShieldedWallet(
37
+ * { networkId, forks },
38
+ * { builder: new V1Builder().withDefaults(), configuration: v1Configuration },
39
+ * { builder: new V2Builder().withDefaults().withMigration(makeCrossLedgerMigration), configuration: v2Configuration },
40
+ * );
41
+ * const wallet = Wallet.startWithSeed(seed);
42
+ * ```;
43
+ *
44
+ * @param configuration What the wallet layer needs: the network, and where the boundary lies.
45
+ * @param v1 The variant that reads the chain below the boundary, with the configuration it is built from.
46
+ * @param v2 The variant that reads it from the boundary, with the configuration it is built from.
47
+ * @returns The wallet class.
48
+ */
49
+ export function CustomForkingShieldedWallet(configuration, v1, v2) {
50
+ // Registered through the shape the builder states rather than through the one this function was handed. The two are
51
+ // the same builder; what is dropped is the configuration *type*, which the parameter types have already paired with
52
+ // its builder — and which, left generic, keeps `build`'s "nothing further is owed" argument list unresolvable.
53
+ const v1Builder = v1.builder;
54
+ const v2Builder = v2.builder;
68
55
  const BaseWallet = WalletBuilder.init()
69
- .withVariant(ProtocolVersion.MinSupportedVersion, builder)
70
- .build(...buildArgs);
71
- return class CustomShieldedWalletImplementation extends BaseWallet {
72
- static startWithSecretKeys(secretKeys) {
73
- return CustomShieldedWalletImplementation.startFirst(CustomShieldedWalletImplementation, CoreWallet.initEmpty(secretKeys, CustomShieldedWalletImplementation.configuration.networkId));
56
+ .withVariant(ProtocolVersion.MinSupportedVersion, v1Builder, v1.configuration)
57
+ .withVariant(configuration.forks.v9, v2Builder, v2.configuration)
58
+ .build();
59
+ const variants = BaseWallet.allVariantsRecord();
60
+ /** Nothing retained: never started, or stopped. */
61
+ const noKeys = { v8: Option.none(), v9: Option.none() };
62
+ /** Both sides, derived from one seed. */
63
+ const keysFromSeed = (seed) => ({
64
+ v8: Option.some(variants[V1Tag].variant.startAux.fromSeed(seed)),
65
+ v9: Option.some(variants[V2Tag].variant.startAux.fromSeed(seed)),
66
+ });
67
+ /**
68
+ * What a variant can be started with, or why it cannot be.
69
+ *
70
+ * @remarks
71
+ * Two different failures, deliberately distinguished: a wallet that was never started (or has been stopped) holds
72
+ * nothing at all, while one started with the other side's key objects holds something it must not use here.
73
+ */
74
+ const auxFor = (retained, side, variantTag) => Either.fromOption(side, () => new StartMaterial.MissingStartAuxError({
75
+ message: Option.isNone(retained.v8) && Option.isNone(retained.v9)
76
+ ? `This wallet holds no key material: it has not been started, or it has been stopped. Start it ` +
77
+ `before asking it to synchronize or to build a transaction.`
78
+ : `This wallet holds key material of the other protocol version only, which the variant ` +
79
+ `${String(variantTag)} cannot use: key objects belong to one ledger version's runtime. Start this ` +
80
+ `wallet with material that answers for either side — startWithSeed(seed), or ` +
81
+ `startWithKeys({ v8, v9 }).`,
82
+ variantTag,
83
+ }));
84
+ const v8Aux = (retained) => auxFor(retained, retained.v8, V1Tag);
85
+ const v9Aux = (retained) => auxFor(retained, retained.v9, V2Tag);
86
+ /**
87
+ * The protocol versions each variant owns, and the version a transaction it builds is stamped with.
88
+ *
89
+ * @remarks
90
+ * The stamp is the version at which the variant that built the transaction became current — the floor of its epoch —
91
+ * rather than the block height the chain happened to be at. Every decision the stamp is later read for asks which
92
+ * side of the boundary the bytes belong to: which prover proves them, which validator checks them, which variant
93
+ * may unwrap them. The epoch floor is the one value guaranteed to answer that the same way as any other version in
94
+ * the same epoch, and it is derived here from the same `forks.v9` the variants are registered at.
95
+ */
96
+ const v8Epoch = ProtocolVersion.epochOf(ProtocolVersion.MinSupportedVersion, configuration.forks.v9);
97
+ const v9Epoch = ProtocolVersion.epochOf(configuration.forks.v9, configuration.forks.v9);
98
+ const [v8Stamp] = v8Epoch;
99
+ const [v9Stamp] = v9Epoch;
100
+ /** Seals a balancing result, which is absent when the wallet had nothing of its own to add. */
101
+ const sealV8 = (result) => result === undefined ? undefined : WalletTransaction.adopt('Unproven', result, v8Stamp);
102
+ const sealV9 = (result) => result === undefined ? undefined : WalletTransaction.adopt('Unproven', result, v9Stamp);
103
+ /**
104
+ * How long a start waits for the chain to say which version it is on.
105
+ *
106
+ * @remarks
107
+ * Short, because what is being bought is small: the alternative to an answer is the hand-over this wallet has always
108
+ * done, which costs one migration and no correctness on a chain that produces events. It is a ceiling rather than a
109
+ * typical cost — an unreachable indexer refuses a connection long before this — and it exists so that a probe which
110
+ * neither answers nor fails cannot hold a start open indefinitely.
111
+ */
112
+ const probeTimeout = Duration.seconds(5);
113
+ /**
114
+ * The variant the chain's current version belongs to, or nothing.
115
+ *
116
+ * @remarks
117
+ * Nothing covers every way the question can fail to produce an answer, and they are deliberately not distinguished:
118
+ * no probe configured, a probe that rejected, a probe that outlived the wallet's patience, or a version no
119
+ * registered variant claims. Each means the same thing to a caller — start where a wallet with no history starts —
120
+ * and none of them is a reason to fail.
121
+ */
122
+ const probedStart = configuration.chainVersionProbe === undefined
123
+ ? Effect.succeedNone
124
+ : pipe(Effect.tryPromise(configuration.chainVersionProbe), Effect.timeout(probeTimeout), Effect.map((version) => Option.map(BaseWallet.variantFor(version), (variant) => ({ version, variant }))), Effect.orElseSucceed(() => Option.none()));
125
+ /**
126
+ * A fresh state of the variant a probed start begins at, recording the version the chain reported.
127
+ *
128
+ * @remarks
129
+ * Built exactly as the head-variant boot path builds its own — an empty state of that variant, from that ledger
130
+ * version's keys — and then annotated with the observed version, which is what keeps a variant from starting
131
+ * outside its own activation range and signalling backwards on sight.
132
+ */
133
+ const freshStateAt = (variant, keys, version) => Variant.getVersionedVariantTag(variant) === V2Tag
134
+ ? CoreWallet.withProtocolVersion(CoreWallet.initEmpty(keys.v9, configuration.networkId), version)
135
+ : V1CoreWallet.withProtocolVersion(V1CoreWallet.initEmpty(keys.v8, configuration.networkId), version);
136
+ return class ForkingShieldedWalletImplementation extends BaseWallet {
137
+ static configuration = configuration;
138
+ /**
139
+ * Starts at the variant the chain says it is on, or at the head variant when it does not say.
140
+ *
141
+ * @remarks
142
+ * The second branch is the whole of this wallet's previous behaviour, unchanged and reached whenever the chain was
143
+ * not asked or did not answer: the V1 variant, an empty state, and a hand-over on the first batch that reports a
144
+ * version it does not own.
145
+ * @param keys One ledger version's keys per side of the boundary, since either side may be the one that runs.
146
+ * @returns The started wallet.
147
+ */
148
+ static #startProbed(keys) {
149
+ return pipe(probedStart, Effect.map(Option.match({
150
+ onNone: () => ForkingShieldedWalletImplementation.startFirst(ForkingShieldedWalletImplementation, V1CoreWallet.initEmpty(keys.v8, configuration.networkId)),
151
+ onSome: ({ version, variant }) => ForkingShieldedWalletImplementation.startAtVariant(ForkingShieldedWalletImplementation, variant, freshStateAt(variant, keys, version)),
152
+ })), Effect.runPromise);
153
+ }
154
+ static async startWithSeed(seed) {
155
+ const derived = keysFromSeed(WalletSeed.WalletSeed(seed));
156
+ const wallet = await ForkingShieldedWalletImplementation.#startProbed({
157
+ v8: Option.getOrThrow(derived.v8),
158
+ v9: Option.getOrThrow(derived.v9),
159
+ });
160
+ // Both sides derived here and now, and the seed reference dropped with this frame: from this point the wallet
161
+ // holds key objects only, which is strictly less than it held before and does the same work.
162
+ wallet.#retainKeys(derived);
163
+ return wallet;
164
+ }
165
+ static async startWithKeys(keys) {
166
+ const wallet = await ForkingShieldedWalletImplementation.#startProbed(keys);
167
+ wallet.#retainKeys({ v8: Option.some(keys.v8), v9: Option.some(keys.v9) });
168
+ return wallet;
74
169
  }
75
- static startWithSeed(seed) {
76
- const secretKeys = ledger.ZswapSecretKeys.fromSeed(seed);
77
- return CustomShieldedWalletImplementation.startWithSecretKeys(secretKeys);
170
+ static tryRestore(serializedState) {
171
+ const headVariant = HList.head(ForkingShieldedWalletImplementation.allVariants());
172
+ return variantForSnapshot(serializedState, (version) => ForkingShieldedWalletImplementation.variantFor(version), headVariant).pipe(
173
+ // Stated with its result type because the resolved variant is either of the two, so its deserializer is
174
+ // either of theirs: what comes back is a state of whichever one wrote the snapshot, which is what
175
+ // `startAtVariant` takes.
176
+ Either.flatMap((variant) => {
177
+ // Annotated rather than inferred because the resolved variant is either of the two, so its deserializer is
178
+ // either of theirs: what comes back is a state of whichever one wrote the snapshot, which is what
179
+ // `startAtVariant` takes.
180
+ const deserialized = variant.variant.deserializeState(serializedState);
181
+ return Either.map(deserialized, (state) => ForkingShieldedWalletImplementation.startAtVariant(ForkingShieldedWalletImplementation, variant, state));
182
+ }));
78
183
  }
79
184
  static restore(serializedState) {
80
- const deserialized = CustomShieldedWalletImplementation.allVariantsRecord()[V1Tag].variant.deserializeState(serializedState)
81
- .pipe(Either.getOrThrow);
82
- return CustomShieldedWalletImplementation.startFirst(CustomShieldedWalletImplementation, deserialized);
185
+ return Either.getOrThrow(ForkingShieldedWalletImplementation.tryRestore(serializedState));
83
186
  }
84
187
  state;
188
+ /**
189
+ * What the application started this wallet with, kept so synchronization can be started again.
190
+ *
191
+ * @remarks
192
+ * A migration starts a fresh variant whose sync has never run, and sync needs key material that is deliberately
193
+ * absent from anything the wallet serializes. A retained seed answers for both variants; retained key objects
194
+ * answer only for the ledger-v9 one, which is the ledger version this wallet's public API speaks. Cleared by
195
+ * {@link stop} so a stopped wallet cannot be resurrected by a late activation.
196
+ */
197
+ #retainedKeys = Ref.unsafeMake(noKeys);
198
+ /** Remembers key material for both sides of the boundary. */
199
+ #retainKeys(keys) {
200
+ Ref.set(this.#retainedKeys, keys).pipe(Effect.runSync);
201
+ }
202
+ /**
203
+ * Starts synchronization on a variant that has just become current, with key material it can use.
204
+ *
205
+ * @param running The variant to start.
206
+ * @param auxFor That variant's own resolution of what it can be started with.
207
+ */
208
+ #resumeSyncOn(running, keysFor) {
209
+ return Ref.get(this.#retainedKeys).pipe(Effect.flatMap((retained) =>
210
+ // Stopped, or never started: there is nothing to resume and nothing to resume it with.
211
+ Option.isNone(retained.v8) && Option.isNone(retained.v9)
212
+ ? Effect.void
213
+ : EitherOps.toEffect(keysFor(retained)).pipe(Effect.flatMap((aux) => running.startSyncInBackground(aux)))));
214
+ }
215
+ /**
216
+ * Whether the activation watcher has been registered.
217
+ *
218
+ * @remarks
219
+ * Registration is per wallet, not per `start`: watchers accumulate, so registering on every call would restart sync
220
+ * once per historical `start` on the next activation. Flipped with `getAndSet` so concurrent `start` calls cannot
221
+ * both observe it unset.
222
+ */
223
+ #watcherRegistered = Ref.unsafeMake(false);
85
224
  constructor(runtime, scope) {
86
225
  super(runtime, scope);
87
- this.state = this.rawState.pipe(rx.map(ShieldedWalletState.mapState(CustomShieldedWalletImplementation.allVariantsRecord()[V1Tag].variant)), rx.shareReplay({ refCount: true, bufferSize: 1 }));
226
+ this.state = this.rawState.pipe(rx.map((emission) =>
227
+ // The capabilities that understand a state and the state itself are chosen together, in the branch where
228
+ // the producing variant is known. The two variants' capability types are structurally identical, so a
229
+ // capability of one would type-check against a state of the other and be wrong at runtime.
230
+ emission.variantTag === V2Tag
231
+ ? ShieldedWalletState.fromVariant(variants[V2Tag].variant, emission)
232
+ : ShieldedWalletState.fromVariant(variants[V1Tag].variant, emission)), rx.shareReplay({ refCount: true, bufferSize: 1 }));
233
+ }
234
+ /**
235
+ * Everything a start does once the wallet holds its key material: watch for the next variant, synchronize on this
236
+ * one.
237
+ *
238
+ * @remarks
239
+ * The same work whichever start was called — what the three differ in is only what they retained a moment earlier —
240
+ * so it is stated once. The watcher is registered before the first dispatch and only once per wallet:
241
+ * `onVariantActivation` resolves only after its subscription is live, so an activation racing a start is queued
242
+ * rather than missed, and watchers accumulate, so registering per call would restart synchronization once per
243
+ * historical start.
244
+ *
245
+ * Both branches of the dispatch resolve their keys from what was retained rather than from what the caller passed,
246
+ * because the variant that is current is not the caller's to know: a wallet restored below the boundary is on the
247
+ * ledger-v8 one whatever the application holds.
248
+ */
249
+ #startSynchronizing() {
250
+ return Effect.gen(this, function* () {
251
+ const alreadyRegistered = yield* Ref.getAndSet(this.#watcherRegistered, true);
252
+ if (!alreadyRegistered) {
253
+ yield* this.runtime.onVariantActivation({
254
+ [V1Tag]: (v1) => this.#resumeSyncOn(v1, v8Aux),
255
+ [V2Tag]: (v2) => this.#resumeSyncOn(v2, v9Aux),
256
+ });
257
+ }
258
+ yield* this.runtime.dispatch({
259
+ [V1Tag]: (v1) => this.#resumeSyncOn(v1, v8Aux),
260
+ [V2Tag]: (v2) => this.#resumeSyncOn(v2, v9Aux),
261
+ });
262
+ });
88
263
  }
264
+ /**
265
+ * Starts background synchronization on whichever variant is current, and keeps the keys for the next one.
266
+ *
267
+ * @remarks
268
+ * The keys are ledger-v9's, so they are retained against that variant alone and answer for it alone. A wallet still
269
+ * on the V1 variant is started from whatever it holds for _that_ side — which a wallet built from a seed, or from
270
+ * both versions' keys, can always answer and one holding only these cannot.
271
+ *
272
+ * That is the position of a wallet restored from a snapshot written below the boundary: a snapshot carries no key
273
+ * material, so these keys leave it with nothing the running variant can use, and it says so rather than
274
+ * pretending to have started. {@link ForkingShieldedWallet.startWithSeed} and
275
+ * {@link ForkingShieldedWallet.startWithKeys} are the same wallet started with material that answers for either
276
+ * side.
277
+ * @param secretKeys The ledger-v9's Zswap secret keys.
278
+ */
89
279
  start(secretKeys) {
90
- return this.runtime.dispatch({ [V1Tag]: (v1) => v1.startSyncInBackground(secretKeys) }).pipe(Effect.runPromise);
280
+ return Effect.gen(this, function* () {
281
+ // The ledger-v9 side only: these keys belong to that ledger version's runtime. Whatever the wallet already
282
+ // holds for the ledger-v8 side is left as it is, so a wallet built from a seed keeps the ability to read a
283
+ // chain that has not forked yet.
284
+ yield* Ref.update(this.#retainedKeys, (retained) => ({ ...retained, v9: Option.some(secretKeys) }));
285
+ yield* this.#startSynchronizing();
286
+ }).pipe(Effect.runPromise);
287
+ }
288
+ startWithSeed(seed) {
289
+ return Effect.gen(this, function* () {
290
+ // Replaced rather than merged: a seed answers for every variant, so whatever partial material the wallet held
291
+ // has nothing left to contribute.
292
+ yield* Ref.set(this.#retainedKeys, keysFromSeed(WalletSeed.WalletSeed(seed)));
293
+ yield* this.#startSynchronizing();
294
+ }).pipe(Effect.runPromise);
295
+ }
296
+ startWithKeys(keys) {
297
+ return Effect.gen(this, function* () {
298
+ yield* Ref.set(this.#retainedKeys, { v8: Option.some(keys.v8), v9: Option.some(keys.v9) });
299
+ yield* this.#startSynchronizing();
300
+ }).pipe(Effect.runPromise);
301
+ }
302
+ async stop() {
303
+ // Released before the runtime is torn down: the key material outlives neither the wallet nor an in-flight
304
+ // activation.
305
+ Ref.set(this.#retainedKeys, noKeys).pipe(Effect.runSync);
306
+ await super.stop();
307
+ }
308
+ /**
309
+ * The key material the variant that is current can use, from what this wallet retained.
310
+ *
311
+ * @remarks
312
+ * Transacting needs the same secrets synchronization does, and for the same reason: coin selection reads coins only
313
+ * their owner can decrypt. A wallet that was never started, or one holding key objects of the other ledger
314
+ * version, has none the current variant can use and says so by name.
315
+ */
316
+ #requireAux(keysFor) {
317
+ return Ref.get(this.#retainedKeys).pipe(Effect.flatMap((retained) => EitherOps.toEffect(keysFor(retained))));
91
318
  }
92
- balanceTransaction(secretKeys, tx) {
319
+ /**
320
+ * Balances a transaction with shielded coins, on whichever side of the boundary it was built.
321
+ *
322
+ * @param tx The transaction to balance, which must have been built in the epoch this wallet is currently in.
323
+ * @returns The balancing transaction, stamped with the version it was built at, or nothing when none is needed.
324
+ */
325
+ balanceTransaction(tx) {
93
326
  return this.runtime
94
327
  .dispatch({
95
- [V1Tag]: (v1) => v1.balanceTransaction(secretKeys, tx),
328
+ [V1Tag]: (v1) => Effect.all([
329
+ this.#requireAux(v8Aux),
330
+ EitherOps.toEffect(WalletTransaction.unwrapWithin(tx, v8Epoch)),
331
+ ]).pipe(Effect.flatMap(([keys, unwrapped]) => v1.balanceTransaction(keys, unwrapped)), Effect.map(sealV8)),
332
+ [V2Tag]: (v2) => Effect.all([
333
+ this.#requireAux(v9Aux),
334
+ EitherOps.toEffect(WalletTransaction.unwrapWithin(tx, v9Epoch)),
335
+ ]).pipe(Effect.flatMap(([keys, unwrapped]) => v2.balanceTransaction(keys, unwrapped)), Effect.map(sealV9)),
96
336
  })
97
337
  .pipe(Effect.runPromise);
98
338
  }
99
- transferTransaction(secretKeys, outputs) {
339
+ transferTransaction(outputs) {
100
340
  return this.runtime
101
341
  .dispatch({
102
- [V1Tag]: (v1) => v1.transferTransaction(secretKeys, outputs),
342
+ [V1Tag]: (v1) => this.#requireAux(v8Aux).pipe(Effect.flatMap((keys) => v1.transferTransaction(keys, outputs)), Effect.map((tx) => WalletTransaction.adopt('Unproven', tx, v8Stamp))),
343
+ [V2Tag]: (v2) => this.#requireAux(v9Aux).pipe(Effect.flatMap((keys) => v2.transferTransaction(keys, outputs)), Effect.map((tx) => WalletTransaction.adopt('Unproven', tx, v9Stamp))),
103
344
  })
104
345
  .pipe(Effect.runPromise);
105
346
  }
106
- initSwap(secretKeys, desiredInputs, desiredOutputs) {
347
+ initSwap(desiredInputs, desiredOutputs) {
107
348
  return this.runtime
108
- .dispatch({ [V1Tag]: (v1) => v1.initSwap(secretKeys, desiredInputs, desiredOutputs) })
349
+ .dispatch({
350
+ [V1Tag]: (v1) => this.#requireAux(v8Aux).pipe(Effect.flatMap((keys) => v1.initSwap(keys, desiredInputs, desiredOutputs)), Effect.map((tx) => WalletTransaction.adopt('Unproven', tx, v8Stamp))),
351
+ [V2Tag]: (v2) => this.#requireAux(v9Aux).pipe(Effect.flatMap((keys) => v2.initSwap(keys, desiredInputs, desiredOutputs)), Effect.map((tx) => WalletTransaction.adopt('Unproven', tx, v9Stamp))),
352
+ })
109
353
  .pipe(Effect.runPromise);
110
354
  }
355
+ /**
356
+ * Un-records a transaction this wallet produced, releasing the coins it had booked.
357
+ *
358
+ * @remarks
359
+ * A transaction built on the other side of the boundary is not one the current variant could have booked coins for,
360
+ * so there is nothing of it to release and this resolves having done nothing. That is the one place a version
361
+ * mismatch is not an error: the facade reverts all three wallets together when a submission fails, and a refusal
362
+ * here would strand that whole path over a transaction this wallet was never holding anything for.
363
+ *
364
+ * The coins a ledger-v8 transaction did book are not left behind by that no-op: they crossed the boundary as
365
+ * reservations, and the first sync update after the crossing releases every one of them, because no transaction
366
+ * of that ledger version can be included any more (`v2/CoreWallet.completeCrossing`).
367
+ * @param transaction The transaction to un-record.
368
+ */
111
369
  revertTransaction(transaction) {
112
370
  return this.runtime
113
371
  .dispatch({
114
- [V1Tag]: (v1) => v1.revertTransaction(transaction),
372
+ [V1Tag]: (v1) => Either.match(WalletTransaction.unwrapWithin(transaction, v8Epoch), { onLeft: () => Effect.void, onRight: (unwrapped) => v1.revertTransaction(unwrapped) }),
373
+ [V2Tag]: (v2) => Either.match(WalletTransaction.unwrapWithin(transaction, v9Epoch), { onLeft: () => Effect.void, onRight: (unwrapped) => v2.revertTransaction(unwrapped) }),
115
374
  })
116
375
  .pipe(Effect.runPromise);
117
376
  }
@@ -119,8 +378,8 @@ export function CustomShieldedWallet(configuration, builder) {
119
378
  return rx.firstValueFrom(this.state.pipe(rx.filter((state) => state.state.progress.isCompleteWithin(allowedGap))));
120
379
  }
121
380
  /**
122
- * Serializes the most recent state It's preferable to use [[ShieldedWalletState.serialize]] instead, to know
123
- * exactly, which state is serialized
381
+ * Serializes the most recent state. It's preferable to use [[ShieldedWalletState.serialize]] instead, to know
382
+ * exactly which state is serialized.
124
383
  */
125
384
  serializeState() {
126
385
  return rx.firstValueFrom(this.state).then((state) => state.serialize());
@@ -130,3 +389,34 @@ export function CustomShieldedWallet(configuration, builder) {
130
389
  }
131
390
  };
132
391
  }
392
+ /**
393
+ * Builds the shielded wallet this package ships: the default variant either side of `configuration.forks.v9`.
394
+ *
395
+ * @remarks
396
+ * Both variants read the chain through the indexer and record transaction history in the same storage; each is built
397
+ * from the same application configuration, because what they ask for happens to coincide. The V2 variant is
398
+ * registered with the cross-ledger migration, which is what makes the hand-over carry identity and a cursor onto a
399
+ * fresh state rather than start a wallet from nothing.
400
+ *
401
+ * **Transacting works on either side of the boundary**: the active variant builds with its own ledger and its own
402
+ * derived keys, and every result travels as a handle stamped with the epoch that built it. Proving a ledger-v8
403
+ * transaction still requires a proving server registered below the boundary.
404
+ *
405
+ * **The chain is asked where it is before a variant is chosen.** The indexer this wallet already syncs from answers
406
+ * which protocol version the chain is on, so a start on a chain past the boundary begins on V2 rather than handing
407
+ * over immediately. An application that would rather ask something else — a cache, a value it already holds —
408
+ * supplies its own `chainVersionProbe`; one whose chain cannot be reached loses nothing, because the answer is
409
+ * best-effort and its absence is the behaviour this wallet had before.
410
+ * @param configuration What the wallet and both its variants are built from, including where the boundary lies.
411
+ * @returns The wallet class.
412
+ */
413
+ export function ShieldedWallet(configuration) {
414
+ const withProbe = {
415
+ ...configuration,
416
+ chainVersionProbe: configuration.chainVersionProbe ?? makeIndexerChainVersionProbe(configuration),
417
+ };
418
+ return CustomForkingShieldedWallet(withProbe, { builder: new V1Builder().withDefaults(), configuration }, {
419
+ builder: new V2Builder().withDefaults().withMigration(() => Migration.makeCrossLedgerMigration()),
420
+ configuration,
421
+ });
422
+ }