@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.
- package/README.md +18 -9
- package/dist/Restore.d.ts +40 -0
- package/dist/Restore.js +63 -0
- package/dist/ShieldedWallet.d.ts +248 -62
- package/dist/ShieldedWallet.js +366 -76
- package/dist/ShieldedWalletAPI.d.ts +179 -0
- package/dist/ShieldedWalletAPI.js +75 -0
- package/dist/SingleVariantShieldedWallet.d.ts +35 -0
- package/dist/SingleVariantShieldedWallet.js +240 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/v1/CoinsAndBalances.d.ts +1 -1
- package/dist/v1/CoreWallet.d.ts +48 -1
- package/dist/v1/CoreWallet.js +54 -1
- package/dist/v1/Migration.d.ts +95 -0
- package/dist/v1/Migration.js +82 -0
- package/dist/v1/RunningV1Variant.d.ts +1 -1
- package/dist/v1/RunningV1Variant.js +37 -8
- package/dist/v1/Serialization.js +1 -1
- package/dist/v1/Sync.d.ts +132 -9
- package/dist/v1/Sync.js +272 -36
- package/dist/v1/Transacting.d.ts +1 -1
- package/dist/v1/Transacting.js +3 -2
- package/dist/v1/TransactionHistory.d.ts +9 -2
- package/dist/v1/TransactionHistory.js +26 -5
- package/dist/v1/TransactionOps.d.ts +1 -1
- package/dist/v1/V1Builder.d.ts +87 -26
- package/dist/v1/V1Builder.js +109 -8
- package/dist/v1/WalletError.d.ts +27 -2
- package/dist/v1/WalletError.js +16 -0
- package/dist/v1/index.d.ts +2 -1
- package/dist/v1/index.js +5 -1
- package/dist/v2/CoinsAndBalances.d.ts +24 -0
- package/dist/v2/CoinsAndBalances.js +41 -0
- package/dist/v2/CoreWallet.d.ts +162 -0
- package/dist/v2/CoreWallet.js +305 -0
- package/dist/v2/Keys.d.ts +8 -0
- package/dist/v2/Keys.js +16 -0
- package/dist/v2/Migration.d.ts +112 -0
- package/dist/v2/Migration.js +96 -0
- package/dist/v2/RunningV2Variant.d.ts +40 -0
- package/dist/v2/RunningV2Variant.js +127 -0
- package/dist/v2/Serialization.d.ts +12 -0
- package/dist/v2/Serialization.js +93 -0
- package/dist/v2/Sync.d.ts +208 -0
- package/dist/v2/Sync.js +463 -0
- package/dist/v2/Transacting.d.ts +50 -0
- package/dist/v2/Transacting.js +259 -0
- package/dist/v2/TransactionHistory.d.ts +100 -0
- package/dist/v2/TransactionHistory.js +132 -0
- package/dist/v2/TransactionImbalances.d.ts +10 -0
- package/dist/v2/TransactionImbalances.js +30 -0
- package/dist/v2/TransactionOps.d.ts +17 -0
- package/dist/v2/TransactionOps.js +73 -0
- package/dist/v2/V2Builder.d.ts +154 -0
- package/dist/v2/V2Builder.js +273 -0
- package/dist/v2/WalletError.d.ts +99 -0
- package/dist/v2/WalletError.js +69 -0
- package/dist/v2/index.d.ts +13 -0
- package/dist/v2/index.js +26 -0
- package/package.json +16 -8
package/dist/ShieldedWallet.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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,
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
76
|
-
const
|
|
77
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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(
|
|
339
|
+
transferTransaction(outputs) {
|
|
100
340
|
return this.runtime
|
|
101
341
|
.dispatch({
|
|
102
|
-
[V1Tag]: (v1) => v1.transferTransaction(
|
|
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(
|
|
347
|
+
initSwap(desiredInputs, desiredOutputs) {
|
|
107
348
|
return this.runtime
|
|
108
|
-
.dispatch({
|
|
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(
|
|
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
|
|
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
|
+
}
|