@midnight-ntwrk/wallet-sdk-facade 5.0.0-beta.2 → 5.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 +41 -0
- package/dist/index.d.ts +449 -75
- package/dist/index.js +529 -111
- package/dist/transaction.d.ts +16 -4
- package/dist/transaction.js +115 -7
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -10,21 +10,31 @@
|
|
|
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 * as
|
|
13
|
+
import * as ledgerV9 from '@midnightntwrk/ledger-v9';
|
|
14
14
|
import { makeDefaultSubmissionService, } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
15
|
-
import {
|
|
15
|
+
import { makeDefaultVersionedProvingService, } from '@midnight-ntwrk/wallet-sdk-capabilities/proving';
|
|
16
16
|
import { ShieldedSectionSchema, mergeShieldedSections, } from '@midnight-ntwrk/wallet-sdk-shielded';
|
|
17
17
|
import { UnshieldedSectionSchema, mergeUnshieldedSections, } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
|
|
18
18
|
import { DustSectionSchema, mergeDustSections } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
|
|
19
19
|
import { Clock } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
20
20
|
import { FetchTermsAndConditions as FetchTermsAndConditionsQuery } from '@midnight-ntwrk/wallet-sdk-indexer-client';
|
|
21
21
|
import { QueryRunner } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
|
|
22
|
-
import { Array as Arr, pipe, Schema } from 'effect';
|
|
23
|
-
import { TransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
24
|
-
import
|
|
22
|
+
import { Array as Arr, Either, Option, pipe, Schema } from 'effect';
|
|
23
|
+
import { ProtocolVersion, TransactionHistoryStorage, WalletTransaction, WireFormatError, } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
24
|
+
import * as ledgerV8 from '@midnight-ntwrk/ledger-v8';
|
|
25
|
+
import * as Signatures from '@midnight-ntwrk/wallet-sdk-capabilities/signatures';
|
|
26
|
+
import { BehaviorSubject, combineLatest, concatMap, distinctUntilChanged, firstValueFrom, map, tap, } from 'rxjs';
|
|
25
27
|
import { PendingTransactions, PendingTransactionsServiceImpl, } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
26
|
-
import { makeDefaultBlockDataFetcher,
|
|
27
|
-
import {
|
|
28
|
+
import { makeDefaultBlockDataFetcher, makeDefaultVersionedValidationService, ValidationFetchError, WellFormedError, } from '@midnight-ntwrk/wallet-sdk-capabilities/validation';
|
|
29
|
+
import { finalizedTransactionTraits, txHistoryHash } from './transaction.js';
|
|
30
|
+
/**
|
|
31
|
+
* Why the wallet gave up on a transaction, for the history entry.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* A chain rejection speaks for itself through the entry's status; an orphaned transaction has no chain verdict at all,
|
|
35
|
+
* so the entry is the only place the reason can be recorded.
|
|
36
|
+
*/
|
|
37
|
+
const rejectionReason = (result) => result.status === 'ORPHANED_BY_FORK' ? 'orphaned-by-protocol-upgrade' : undefined;
|
|
28
38
|
/**
|
|
29
39
|
* Full entry schema for transaction history. The common entry data and wallet-specific sections (`shielded`,
|
|
30
40
|
* `unshielded`, `dust`) live on every entry regardless of lifecycle; the `lifecycle` field is the only discriminator.
|
|
@@ -44,7 +54,7 @@ export const isFinalizedWalletEntry = (entry) => entry.lifecycle.status === 'fin
|
|
|
44
54
|
* wallet has set the value, later writes are no-ops for these fields. This is correct because the value is the same
|
|
45
55
|
* across all wallets (it's a property of the on-chain tx, not the wallet's view of it).
|
|
46
56
|
* - **`identifiers`** — unioned (each wallet may surface a different identifier subset).
|
|
47
|
-
* - **`lifecycle`** — incoming wins
|
|
57
|
+
* - **`lifecycle`** — incoming wins, except that a late rejection cannot overwrite recorded on-chain inclusion.
|
|
48
58
|
* - **Wallet sections** (`shielded`, `unshielded`, `dust`) — combined via per-section merge when both sides have them;
|
|
49
59
|
* otherwise whichever side is present is used.
|
|
50
60
|
*/
|
|
@@ -73,8 +83,9 @@ export function mergeWalletEntries(existing, incoming) {
|
|
|
73
83
|
status: existing.status ?? incoming.status,
|
|
74
84
|
timestamp: existing.timestamp ?? incoming.timestamp,
|
|
75
85
|
fees: existing.fees ?? incoming.fees,
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
lifecycle: existing.lifecycle.status === 'finalized' && incoming.lifecycle.status === 'rejected'
|
|
87
|
+
? existing.lifecycle
|
|
88
|
+
: incoming.lifecycle,
|
|
78
89
|
...(shielded !== undefined ? { shielded } : {}),
|
|
79
90
|
...(unshielded !== undefined ? { unshielded } : {}),
|
|
80
91
|
...(dust !== undefined ? { dust } : {}),
|
|
@@ -99,6 +110,26 @@ const revertTxHistoryKey = (tx) => {
|
|
|
99
110
|
return undefined;
|
|
100
111
|
return { hash: txHistoryHash(tx), identifiers };
|
|
101
112
|
};
|
|
113
|
+
const v9Authoring = {
|
|
114
|
+
sampleSigningKey: () => ledgerV9.sampleSigningKey(),
|
|
115
|
+
signatureVerifyingKey: (signingKey) => ledgerV9.signatureVerifyingKey(signingKey),
|
|
116
|
+
signData: (signingKey, data) => ledgerV9.signData(signingKey, data),
|
|
117
|
+
deserializers: {
|
|
118
|
+
Unproven: (bytes) => ledgerV9.Transaction.deserialize('signature', 'pre-proof', 'pre-binding', bytes),
|
|
119
|
+
Unbound: (bytes) => ledgerV9.Transaction.deserialize('signature', 'proof', 'pre-binding', bytes),
|
|
120
|
+
Finalized: (bytes) => ledgerV9.Transaction.deserialize('signature', 'proof', 'binding', bytes),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const v8Authoring = {
|
|
124
|
+
sampleSigningKey: () => ledgerV8.sampleSigningKey(),
|
|
125
|
+
signatureVerifyingKey: (signingKey) => Signatures.liftSignatureVerifyingKey(ledgerV8.signatureVerifyingKey(signingKey)),
|
|
126
|
+
signData: (signingKey, data) => Signatures.liftSignature(ledgerV8.signData(signingKey, data)),
|
|
127
|
+
deserializers: {
|
|
128
|
+
Unproven: (bytes) => ledgerV8.Transaction.deserialize('signature', 'pre-proof', 'pre-binding', bytes),
|
|
129
|
+
Unbound: (bytes) => ledgerV8.Transaction.deserialize('signature', 'proof', 'pre-binding', bytes),
|
|
130
|
+
Finalized: (bytes) => ledgerV8.Transaction.deserialize('signature', 'proof', 'binding', bytes),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
102
133
|
const TokenKindsToBalance = new (class {
|
|
103
134
|
allTokenKinds = ['shielded', 'unshielded', 'dust'];
|
|
104
135
|
toFlags = (tokenKinds) => {
|
|
@@ -132,21 +163,132 @@ export const BalancingRecipe = {
|
|
|
132
163
|
}
|
|
133
164
|
},
|
|
134
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* The lowest of the protocol versions the three wallets have reached.
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* The three wallets follow the same chain but not in lock-step: each recognises a protocol version change when its own
|
|
171
|
+
* synchronization reaches it, so around a fork they disagree for a while. A transaction spans all three, so the one
|
|
172
|
+
* still behind is what bounds the facade as a whole — the highest version every wallet is known to be at.
|
|
173
|
+
* @param versions The version each wallet has reached.
|
|
174
|
+
* @returns The lowest of the three.
|
|
175
|
+
*/
|
|
176
|
+
export const lowestProtocolVersion = (versions) => [versions.shielded, versions.unshielded, versions.dust].reduce((lowest, candidate) => candidate < lowest ? candidate : lowest);
|
|
177
|
+
/**
|
|
178
|
+
* The ledger-v9 key objects the wallets' own `start` takes, from whichever material the caller supplied.
|
|
179
|
+
*
|
|
180
|
+
* @remarks
|
|
181
|
+
* Only the ledger-v9 side is needed here: a wallet built from seeds or from both versions' keys already holds what its
|
|
182
|
+
* V1 variant needs, retained when it was built. What `start` supplies is the side the wallet's own API speaks.
|
|
183
|
+
*/
|
|
184
|
+
const v9KeysOf = (material) => 'v9' in material
|
|
185
|
+
? material.v9
|
|
186
|
+
: {
|
|
187
|
+
shielded: ledgerV9.ZswapSecretKeys.fromSeed(material.shielded),
|
|
188
|
+
dust: ledgerV9.DustSecretKey.fromSeed(material.dust),
|
|
189
|
+
};
|
|
190
|
+
/** The three wallets in a fixed order, so {@link protocolPhaseOf} reports them the same way every time. */
|
|
191
|
+
const walletKinds = ['shielded', 'unshielded', 'dust'];
|
|
192
|
+
/**
|
|
193
|
+
* Reads whether the wallets are settled on one side of a protocol boundary, or still crossing it.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* Derived entirely from the version each wallet has reported and where the boundary lies — the same two facts every
|
|
197
|
+
* other version-routing decision in the SDK is made from, so this reading cannot disagree with them.
|
|
198
|
+
* @param versions The version each wallet has reached.
|
|
199
|
+
* @param forkVersion The version at which the chain hands over to the next ledger version.
|
|
200
|
+
* @returns The reading. See {@link ProtocolPhase}.
|
|
201
|
+
*/
|
|
202
|
+
export const protocolPhaseOf = (versions, forkVersion) => {
|
|
203
|
+
const reported = walletKinds.map((kind) => [kind, versions[kind]]);
|
|
204
|
+
const from = lowestProtocolVersion(versions);
|
|
205
|
+
const epoch = ProtocolVersion.epochOf(from, forkVersion);
|
|
206
|
+
const crossed = reported.filter(([, version]) => !ProtocolVersion.withinRange(version, epoch));
|
|
207
|
+
if (crossed.length === 0)
|
|
208
|
+
return { _tag: 'Settled', version: from };
|
|
209
|
+
return {
|
|
210
|
+
_tag: 'Crossing',
|
|
211
|
+
from,
|
|
212
|
+
to: crossed.reduce((highest, [, version]) => (version > highest ? version : highest), crossed[0][1]),
|
|
213
|
+
behind: reported.filter(([, version]) => ProtocolVersion.withinRange(version, epoch)).map(([kind]) => kind),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
/** Reads what has become of a transaction from the verdict the pending set holds, if it holds one. */
|
|
217
|
+
const pendingStatusOf = (result) => {
|
|
218
|
+
if (result === undefined)
|
|
219
|
+
return { _tag: 'Submitted' };
|
|
220
|
+
switch (result.status) {
|
|
221
|
+
case 'SUCCESS':
|
|
222
|
+
return { _tag: 'Confirmed', segments: result.segments };
|
|
223
|
+
case 'FAILURE':
|
|
224
|
+
case 'PARTIAL_SUCCESS':
|
|
225
|
+
// A transaction only some of whose segments succeeded is still not a transaction that happened as submitted.
|
|
226
|
+
return { _tag: 'Rejected', segments: result.segments };
|
|
227
|
+
case 'ORPHANED_BY_FORK':
|
|
228
|
+
return { _tag: 'Orphaned', authoredFor: result.authoredFor, chainNow: result.chainNow };
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* The pending transactions as an application reads them.
|
|
233
|
+
*
|
|
234
|
+
* @remarks
|
|
235
|
+
* A projection over the pending set the services keep, not a second copy of it: the facts are the same, stated as a
|
|
236
|
+
* list of transactions with a status each rather than as the bag the machinery works in.
|
|
237
|
+
* @param pending The pending set.
|
|
238
|
+
* @returns One entry per transaction, in the order the wallet recorded them.
|
|
239
|
+
*/
|
|
240
|
+
export const pendingTransactionsOf = (pending) => pending.all.map((item) => ({
|
|
241
|
+
transaction: item.tx,
|
|
242
|
+
submittedAt: item.creationTime,
|
|
243
|
+
authoredFor: item.protocolVersion,
|
|
244
|
+
status: pendingStatusOf('result' in item ? item.result : undefined),
|
|
245
|
+
}));
|
|
135
246
|
export class FacadeState {
|
|
136
247
|
shielded;
|
|
137
248
|
unshielded;
|
|
138
249
|
dust;
|
|
139
250
|
pending;
|
|
251
|
+
/** The protocol version each of the three wallets has reached. */
|
|
252
|
+
get protocolVersion() {
|
|
253
|
+
return {
|
|
254
|
+
shielded: this.shielded.protocolVersion,
|
|
255
|
+
unshielded: this.unshielded.protocolVersion,
|
|
256
|
+
dust: this.dust.protocolVersion,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The protocol version the facade as a whole can act at: the lowest the three wallets have reached.
|
|
261
|
+
*
|
|
262
|
+
* @remarks
|
|
263
|
+
* Around a protocol boundary the three wallets cross at slightly different moments, and a transaction needs all
|
|
264
|
+
* three. This is the version every one of them is known to understand.
|
|
265
|
+
*/
|
|
266
|
+
get activeProtocolVersion() {
|
|
267
|
+
return lowestProtocolVersion(this.protocolVersion);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Whether the three wallets are settled on one side of the protocol boundary, or still crossing it.
|
|
271
|
+
*
|
|
272
|
+
* @remarks
|
|
273
|
+
* Additive, and the reading `protocolVersion` alone cannot give: three versions that differ tell an application
|
|
274
|
+
* nothing about whether the difference matters. See {@link ProtocolPhase}.
|
|
275
|
+
*/
|
|
276
|
+
get protocol() {
|
|
277
|
+
return protocolPhaseOf(this.protocolVersion, this.#forkVersion);
|
|
278
|
+
}
|
|
140
279
|
get isSynced() {
|
|
141
280
|
return (this.shielded.state.progress.isStrictlyComplete() &&
|
|
142
281
|
this.dust.state.progress.isStrictlyComplete() &&
|
|
143
282
|
this.unshielded.progress.isStrictlyComplete());
|
|
144
283
|
}
|
|
145
|
-
|
|
284
|
+
/** Where the chain hands over from one ledger version to the next, which is what {@link protocol} is read against. */
|
|
285
|
+
#forkVersion;
|
|
286
|
+
constructor(shielded, unshielded, dust, pending, forkVersion = ProtocolVersion.MinSupportedVersion) {
|
|
146
287
|
this.shielded = shielded;
|
|
147
288
|
this.unshielded = unshielded;
|
|
148
289
|
this.dust = dust;
|
|
149
|
-
this.pending = pending;
|
|
290
|
+
this.pending = pendingTransactionsOf(pending);
|
|
291
|
+
this.#forkVersion = forkVersion;
|
|
150
292
|
}
|
|
151
293
|
}
|
|
152
294
|
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
@@ -161,29 +303,65 @@ const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
|
161
303
|
* without a circular dependency.
|
|
162
304
|
*/
|
|
163
305
|
export { Clock };
|
|
306
|
+
/**
|
|
307
|
+
* The fork schedule the facade presets when a configuration names none: {@link ProtocolVersion.V9NativeForkSchedule},
|
|
308
|
+
* ledger-v9 from the version a 2.x node reports.
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
311
|
+
* Where a chain forks is a fact about the chain, which is why each wallet package requires `forks` and presets nothing.
|
|
312
|
+
* The facade is the one place a preset decides nothing the SDK has not already decided: every chain the 2.x node line
|
|
313
|
+
* runs hands over at this version, so an application copying the constant into its configuration only restates it. A
|
|
314
|
+
* chain that hands over elsewhere states its own `forks`, which wins.
|
|
315
|
+
*/
|
|
316
|
+
export const DefaultForkSchedule = ProtocolVersion.V9NativeForkSchedule;
|
|
164
317
|
// `BlockData` is not re-exported from the facade to avoid a name collision with the
|
|
165
318
|
// `@midnight-ntwrk/wallet-sdk-dust-wallet` export. The two are structurally identical; users can name the type via
|
|
166
319
|
// `@midnight-ntwrk/wallet-sdk-dust-wallet` or `@midnight-ntwrk/wallet-sdk-capabilities/validation`.
|
|
167
320
|
export { ValidationFetchError, WellFormedError, };
|
|
168
321
|
export class WalletFacade {
|
|
169
322
|
static makeDefaultSubmissionService(config) {
|
|
323
|
+
// A handle serializes itself, which is all submission needs of a transaction — and the one thing every ledger
|
|
324
|
+
// version's transaction does identically.
|
|
170
325
|
return makeDefaultSubmissionService(config);
|
|
171
326
|
}
|
|
172
327
|
static makeDefaultPendingTransactionsService(config) {
|
|
173
328
|
return PendingTransactionsServiceImpl.init({
|
|
174
329
|
configuration: config,
|
|
175
|
-
|
|
330
|
+
txTraits: finalizedTransactionTraits(config.forks.v9),
|
|
176
331
|
});
|
|
177
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Builds the proving service a configuration describes.
|
|
335
|
+
*
|
|
336
|
+
* @remarks
|
|
337
|
+
* Handed the same fork schedule the wallets are built with, because a proving backend is chosen by the epoch a
|
|
338
|
+
* transaction belongs to and the two ends of that question must not be able to compute the boundary differently.
|
|
339
|
+
*/
|
|
178
340
|
static makeDefaultProvingService(config) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
341
|
+
return Either.getOrThrowWith(makeDefaultVersionedProvingService(config, config.forks), (error) => new Error(error.message));
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Fills in what the facade presets — `forks`, as {@link DefaultForkSchedule} — so that the result is a configuration a
|
|
345
|
+
* wallet package can be built from.
|
|
346
|
+
*
|
|
347
|
+
* @remarks
|
|
348
|
+
* {@link WalletFacade.init} does this itself, once and before anything is built, so the wallets, the default services
|
|
349
|
+
* and the facade's own boundary cannot be given different schedules, and no factory needs to call it. It is public
|
|
350
|
+
* for code outside a factory that needs the configuration the facade will use: an application building a wallet
|
|
351
|
+
* package directly, or reading `forks` back to author a transaction for the right ledger version. Resolving twice
|
|
352
|
+
* is the same as resolving once, so `init` may be handed the result.
|
|
353
|
+
* @example
|
|
354
|
+
* ```typescript
|
|
355
|
+
* const configuration = WalletFacade.resolveConfiguration({ networkId: 'undeployed', ... });
|
|
356
|
+
* const facade = await WalletFacade.init({ configuration, shielded: (config) => ShieldedWallet(config), ... });
|
|
357
|
+
* const authoredForV9 = protocolVersion >= configuration.forks.v9;
|
|
358
|
+
* ```;
|
|
359
|
+
*
|
|
360
|
+
* @param configuration The configuration as given, with or without `forks`.
|
|
361
|
+
* @returns The same configuration with `forks` present: the schedule it named, or {@link DefaultForkSchedule}.
|
|
362
|
+
*/
|
|
363
|
+
static resolveConfiguration(configuration) {
|
|
364
|
+
return { ...configuration, forks: configuration.forks ?? DefaultForkSchedule };
|
|
187
365
|
}
|
|
188
366
|
/**
|
|
189
367
|
* Fetches the current Terms and Conditions from the network indexer.
|
|
@@ -215,7 +393,9 @@ export class WalletFacade {
|
|
|
215
393
|
* and initialization of necessary components. Specifically - it requires following fields:
|
|
216
394
|
*
|
|
217
395
|
* - `configuration` - holding a configuration, which needs to extend {@link DefaultConfiguration} - this way allows to
|
|
218
|
-
* convey use-case-specific settings in the same way, as the SDK works by default
|
|
396
|
+
* convey use-case-specific settings in the same way, as the SDK works by default. `forks` may be left out; every
|
|
397
|
+
* factory below is handed a {@link ResolvedConfiguration}, which always has it — what
|
|
398
|
+
* {@link WalletFacade.resolveConfiguration} returns
|
|
219
399
|
* - `shielded` - a function taking the configuration and returning shielded wallet (or a promise with such)
|
|
220
400
|
* implementing {@link ShieldedWalletAPI}
|
|
221
401
|
* - `unshielded` - a function taking the configuration and returning unshielded wallet (or a promise with such)
|
|
@@ -224,38 +404,38 @@ export class WalletFacade {
|
|
|
224
404
|
* {@link DustWalletAPI} There are some optional services/abstractions to provide, too. If not provided - default
|
|
225
405
|
* implementations will be used, each of them is initialized by a function taking the configuration and returning
|
|
226
406
|
* proper implementation (wrapped in a {@link Promise} or not).
|
|
227
|
-
* - `submissionService` - needs to implement {@link SubmissionService} for a {@link
|
|
407
|
+
* - `submissionService` - needs to implement {@link SubmissionService} for a {@link ledgerV9.FinalizedTransaction} to
|
|
228
408
|
* submit transactions to the network, default uses Node RPC connection
|
|
229
409
|
* - `pendingTransactionsService` - needs to implement {@link PendingTransactionsService} for a
|
|
230
|
-
* {@link
|
|
231
|
-
*
|
|
410
|
+
* {@link ledgerV9.FinalizedTransaction} to keep track of pending transactions, default uses in-memory
|
|
411
|
+
* implementation
|
|
412
|
+
* - `provingService` - needs to implement {@link VersionedProvingService} to prove it, default uses proving server
|
|
232
413
|
* - `clock` - needs to implement {@link Clock.Clock} for getting current time, default uses system clock
|
|
233
414
|
*/
|
|
234
415
|
static async init(initParams) {
|
|
416
|
+
const configuration = WalletFacade.resolveConfiguration(initParams.configuration);
|
|
235
417
|
const submissionService = await Promise.resolve(initParams.submissionService
|
|
236
|
-
? initParams.submissionService(
|
|
237
|
-
: WalletFacade.makeDefaultSubmissionService(
|
|
418
|
+
? initParams.submissionService(configuration)
|
|
419
|
+
: WalletFacade.makeDefaultSubmissionService(configuration));
|
|
238
420
|
const pendingTransactionsService = await Promise.resolve(initParams.pendingTransactionsService
|
|
239
|
-
? initParams.pendingTransactionsService(
|
|
240
|
-
: WalletFacade.makeDefaultPendingTransactionsService(
|
|
421
|
+
? initParams.pendingTransactionsService(configuration)
|
|
422
|
+
: WalletFacade.makeDefaultPendingTransactionsService(configuration));
|
|
241
423
|
const provingService = await Promise.resolve(initParams.provingService
|
|
242
|
-
? initParams.provingService(
|
|
243
|
-
: WalletFacade.makeDefaultProvingService(
|
|
244
|
-
const shielded = await Promise.resolve(initParams.shielded(
|
|
245
|
-
const unshielded = await Promise.resolve(initParams.unshielded(
|
|
246
|
-
const dust = await Promise.resolve(initParams.dust(
|
|
247
|
-
const clock = await Promise.resolve(initParams.clock ? initParams.clock(
|
|
248
|
-
const fetchBlockData = await Promise.resolve(initParams.fetchBlockData
|
|
249
|
-
? initParams.fetchBlockData(initParams.configuration)
|
|
250
|
-
: makeDefaultBlockDataFetcher(initParams.configuration));
|
|
424
|
+
? initParams.provingService(configuration)
|
|
425
|
+
: WalletFacade.makeDefaultProvingService(configuration));
|
|
426
|
+
const shielded = await Promise.resolve(initParams.shielded(configuration));
|
|
427
|
+
const unshielded = await Promise.resolve(initParams.unshielded(configuration));
|
|
428
|
+
const dust = await Promise.resolve(initParams.dust(configuration));
|
|
429
|
+
const clock = await Promise.resolve(initParams.clock ? initParams.clock(configuration) : Clock.systemClock);
|
|
430
|
+
const fetchBlockData = await Promise.resolve(initParams.fetchBlockData ? initParams.fetchBlockData(configuration) : makeDefaultBlockDataFetcher(configuration));
|
|
251
431
|
const validationService = await Promise.resolve(initParams.validationService
|
|
252
|
-
? initParams.validationService(
|
|
253
|
-
:
|
|
432
|
+
? initParams.validationService(configuration, { fetchBlockData, clock })
|
|
433
|
+
: makeDefaultVersionedValidationService({
|
|
254
434
|
fetchBlockData,
|
|
255
|
-
networkId:
|
|
435
|
+
networkId: configuration.networkId,
|
|
256
436
|
clock,
|
|
257
|
-
}));
|
|
258
|
-
return new WalletFacade(shielded, unshielded, dust, submissionService, pendingTransactionsService, provingService, validationService,
|
|
437
|
+
}, configuration.forks.v9));
|
|
438
|
+
return new WalletFacade(shielded, unshielded, dust, submissionService, pendingTransactionsService, provingService, validationService, configuration.txHistoryStorage, configuration.forks.v9, clock);
|
|
259
439
|
}
|
|
260
440
|
shielded;
|
|
261
441
|
unshielded;
|
|
@@ -267,13 +447,24 @@ export class WalletFacade {
|
|
|
267
447
|
#txHistoryStorage;
|
|
268
448
|
clock;
|
|
269
449
|
#pendingSubscription;
|
|
450
|
+
#protocolVersionSubscription;
|
|
451
|
+
/**
|
|
452
|
+
* The protocol version the wallets have all reached, as last observed.
|
|
453
|
+
*
|
|
454
|
+
* @remarks
|
|
455
|
+
* `Option.none()` until the three wallets have each emitted once. A transaction stamped with `none` is never
|
|
456
|
+
* orphaned, so an unobserved version costs a transaction nothing but the ability to be given up on early.
|
|
457
|
+
*/
|
|
458
|
+
#observedProtocolVersion = new BehaviorSubject(Option.none());
|
|
459
|
+
/** Where the chain hands over from one ledger version to the next, which is what divides the two epochs. */
|
|
460
|
+
#forkVersion;
|
|
270
461
|
/**
|
|
271
462
|
* Constructor is private on purpose - much of initialization of the facade is potentially asynchronous, and adding
|
|
272
463
|
* new parameters is a breaking change to the users Use {@link WalletFacade.init} instead
|
|
273
464
|
*
|
|
274
465
|
* @private
|
|
275
466
|
*/
|
|
276
|
-
constructor(shieldedWallet, unshieldedWallet, dustWallet, submissionService, pendingTransactionsService, provingService, validationService, txHistoryStorage, clock = Clock.systemClock) {
|
|
467
|
+
constructor(shieldedWallet, unshieldedWallet, dustWallet, submissionService, pendingTransactionsService, provingService, validationService, txHistoryStorage, forkVersion, clock = Clock.systemClock) {
|
|
277
468
|
this.shielded = shieldedWallet;
|
|
278
469
|
this.unshielded = unshieldedWallet;
|
|
279
470
|
this.dust = dustWallet;
|
|
@@ -282,12 +473,126 @@ export class WalletFacade {
|
|
|
282
473
|
this.provingService = provingService;
|
|
283
474
|
this.validationService = validationService;
|
|
284
475
|
this.#txHistoryStorage = txHistoryStorage;
|
|
476
|
+
this.#forkVersion = forkVersion;
|
|
285
477
|
this.clock = clock;
|
|
286
478
|
this.#pendingSubscription = this.pendingTransactionsService
|
|
287
479
|
.state()
|
|
288
|
-
.pipe(concatMap((pending) => PendingTransactions.
|
|
480
|
+
.pipe(concatMap((pending) => PendingTransactions.allRejected(pending)), concatMap(async (item) => {
|
|
481
|
+
if (!(await this.reconcileFinalizedTransaction(item.tx, await this.#txHistoryStorage.getAll()))) {
|
|
482
|
+
await this.revert(item.tx, rejectionReason(item.result));
|
|
483
|
+
}
|
|
484
|
+
}))
|
|
485
|
+
.subscribe();
|
|
486
|
+
// Deliberately built from the wallets' own states rather than from `state()`: `state()` includes the pending set,
|
|
487
|
+
// and orphaning writes to it, so feeding that back here would be a cycle.
|
|
488
|
+
this.#protocolVersionSubscription = combineLatest([this.shielded.state, this.unshielded.state, this.dust.state])
|
|
489
|
+
.pipe(map(([shieldedState, unshieldedState, dustState]) => lowestProtocolVersion({
|
|
490
|
+
shielded: shieldedState.protocolVersion,
|
|
491
|
+
unshielded: unshieldedState.protocolVersion,
|
|
492
|
+
dust: dustState.protocolVersion,
|
|
493
|
+
})), distinctUntilChanged(), tap((version) => this.#observedProtocolVersion.next(Option.some(version))),
|
|
494
|
+
// Nothing fallible belongs here: this chain is the only writer of the observed version, and an orphaned
|
|
495
|
+
// transaction sync already saw included is reconciled by the pending subscription like any other verdict.
|
|
496
|
+
concatMap((version) => this.pendingTransactionsService.orphanBeyond(version)))
|
|
289
497
|
.subscribe();
|
|
290
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* The protocol version the facade is currently acting at.
|
|
501
|
+
*
|
|
502
|
+
* @remarks
|
|
503
|
+
* The lowest the three wallets have reached, once they have all reported; the minimum supported version until then,
|
|
504
|
+
* which is the epoch a wallet with no history belongs to. It is what a transaction the facade builds is stamped
|
|
505
|
+
* with, and what the side of the boundary it accepts transactions from is measured against.
|
|
506
|
+
*/
|
|
507
|
+
currentVersion() {
|
|
508
|
+
return Option.getOrElse(this.#observedProtocolVersion.getValue(), () => ProtocolVersion.MinSupportedVersion);
|
|
509
|
+
}
|
|
510
|
+
/** The range of protocol versions on the same side of the boundary as a given one. */
|
|
511
|
+
epochOf(version) {
|
|
512
|
+
return ProtocolVersion.epochOf(version, this.#forkVersion);
|
|
513
|
+
}
|
|
514
|
+
/** The range of protocol versions on the facade's current side of the boundary. */
|
|
515
|
+
currentEpoch() {
|
|
516
|
+
return this.epochOf(this.currentVersion());
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Reads a transaction an application handed in, refusing one built on the other side of the boundary.
|
|
520
|
+
*
|
|
521
|
+
* @remarks
|
|
522
|
+
* The enforcement point for everything that enters the facade — a transaction the wallets built, or one an
|
|
523
|
+
* application authored and sealed with `WalletTransaction.adopt`. Only the current epoch is accepted, because a
|
|
524
|
+
* transaction of the other one cannot be merged, proved or submitted alongside anything the facade would build for
|
|
525
|
+
* it. Both a stranded ledger-v8 transaction after the crossing and a ledger-v9 transaction offered before it are
|
|
526
|
+
* refused here, by name, with the versions written down.
|
|
527
|
+
* @param handle The handle to read.
|
|
528
|
+
* @returns The carried transaction.
|
|
529
|
+
* @throws {@link ProtocolVersionMismatchError} When the transaction was built on the other side of the boundary.
|
|
530
|
+
*/
|
|
531
|
+
accept(handle) {
|
|
532
|
+
return Either.getOrThrowWith(WalletTransaction.unwrapWithin(handle, this.currentEpoch()), (error) => error);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Seals a transaction the facade or a wallet produced, at the version its bytes were fixed by.
|
|
536
|
+
*
|
|
537
|
+
* @remarks
|
|
538
|
+
* The version is the caller's to state, never read from the facade here, because the two can differ: a crossing can
|
|
539
|
+
* land while a transaction is at the prover, and what comes back is still the bytes of the ledger version that made
|
|
540
|
+
* them. A caller sealing something it has just built passes the version it is building at; a caller sealing
|
|
541
|
+
* something that has travelled through an await passes the version stamped on the handle it started from.
|
|
542
|
+
* @param stage How far along the building of the transaction the sealed handle is.
|
|
543
|
+
* @param transaction The transaction to seal.
|
|
544
|
+
* @param protocolVersion The version the transaction's bytes were fixed by.
|
|
545
|
+
* @returns The sealed handle.
|
|
546
|
+
*/
|
|
547
|
+
seal(stage, transaction, protocolVersion) {
|
|
548
|
+
return WalletTransaction.adopt(stage, transaction, protocolVersion);
|
|
549
|
+
}
|
|
550
|
+
/** The ledger primitives the facade signs with on its current side of the boundary. */
|
|
551
|
+
authoring() {
|
|
552
|
+
return this.currentVersion() < this.#forkVersion ? v8Authoring : v9Authoring;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Reads a transaction from bytes that name no protocol version, at the version the facade is acting at.
|
|
556
|
+
*
|
|
557
|
+
* @remarks
|
|
558
|
+
* For bytes that reach the wallet from outside the SDK — a dApp connector's serialized transaction above all, whose
|
|
559
|
+
* contract carries no version alongside them. The bytes cannot say which ledger version wrote them, so this answers
|
|
560
|
+
* with the only version the facade could act at anyway, and reads them with that version's ledger. Bytes the other
|
|
561
|
+
* ledger version wrote are refused here rather than misread, because the markers a stage names are part of what the
|
|
562
|
+
* ledger checks as it reads.
|
|
563
|
+
*
|
|
564
|
+
* The version is the facade's rather than the caller's, which is the point: a caller that reads
|
|
565
|
+
* {@link FacadeState.activeProtocolVersion} and seals a handle itself is answering the same question a moment
|
|
566
|
+
* earlier, with a crossing able to land in between. It is still only the version at the moment of the call — a
|
|
567
|
+
* crossing can land before the handle is used, and {@link ProtocolVersionMismatchError} from the operation that uses
|
|
568
|
+
* it is what says so.
|
|
569
|
+
* @example
|
|
570
|
+
* Balancing a transaction a dApp connector handed over as hex:
|
|
571
|
+
*
|
|
572
|
+
* ```typescript
|
|
573
|
+
* const handle = facade.adoptTransaction(Buffer.from(txHex, 'hex'), 'Unbound');
|
|
574
|
+
* const recipe = await facade.balanceUnboundTransaction(handle, { ttl });
|
|
575
|
+
* ```;
|
|
576
|
+
*
|
|
577
|
+
* @param bytes The transaction's own serialization, as the ledger version that wrote it produced it.
|
|
578
|
+
* @param stage How far along the building of the transaction these bytes are, which decides the markers they are read
|
|
579
|
+
* with.
|
|
580
|
+
* @returns The sealed handle, stamped with the protocol version the facade is acting at.
|
|
581
|
+
* @throws {@link WireFormatError} When the bytes are not a transaction of that version at that stage — which is what
|
|
582
|
+
* a transaction authored on the other side of a protocol boundary looks like from here.
|
|
583
|
+
*/
|
|
584
|
+
adoptTransaction(bytes, stage) {
|
|
585
|
+
const protocolVersion = this.currentVersion();
|
|
586
|
+
return Either.getOrThrowWith(Either.try({
|
|
587
|
+
try: () => this.authoring().deserializers[stage](bytes),
|
|
588
|
+
catch: (cause) => new WireFormatError({
|
|
589
|
+
message: `These bytes are not a transaction of protocol version ${protocolVersion} at stage ${stage} — that is ` +
|
|
590
|
+
`the version this wallet is acting at, and a transaction built for another one has to be built again ` +
|
|
591
|
+
`for it.`,
|
|
592
|
+
cause,
|
|
593
|
+
}),
|
|
594
|
+
}).pipe(Either.map((transaction) => WalletTransaction.adopt(stage, transaction, protocolVersion))), (error) => error);
|
|
595
|
+
}
|
|
291
596
|
defaultTtl() {
|
|
292
597
|
return new Date(this.clock.now().getTime() + DEFAULT_TTL_MS);
|
|
293
598
|
}
|
|
@@ -337,11 +642,21 @@ export class WalletFacade {
|
|
|
337
642
|
* @throws {@link ValidationFetchError} If the block-data fetch fails.
|
|
338
643
|
*/
|
|
339
644
|
async validateTransaction(tx, options) {
|
|
340
|
-
|
|
645
|
+
// Checked at the version the transaction says it was authored for, and never at the version the chain has since
|
|
646
|
+
// reached: well-formedness asks whether the ledger that produced these bytes would accept them, and a fork landing
|
|
647
|
+
// afterwards cannot change that answer. The transaction is read at its own epoch for the same reason.
|
|
648
|
+
return this.validationService.validateTx(Either.getOrThrowWith(WalletTransaction.unwrapWithin(tx, ProtocolVersion.epochOf(tx.protocolVersion, this.#forkVersion)), (error) => error), tx.protocolVersion, options);
|
|
341
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Merges two unproven transactions of the same epoch, or returns whichever there is.
|
|
652
|
+
*
|
|
653
|
+
* @remarks
|
|
654
|
+
* Both are unwrapped before merging, which is where a pair from different epochs is refused: a merge across the
|
|
655
|
+
* boundary is not a failure to compute, it is unrepresentable, and saying so is the whole point of the stamp.
|
|
656
|
+
*/
|
|
342
657
|
mergeUnprovenTransactions(a, b) {
|
|
343
658
|
if (a && b)
|
|
344
|
-
return a.merge(b);
|
|
659
|
+
return this.seal('Unproven', this.accept(a).merge(this.accept(b)), this.currentVersion());
|
|
345
660
|
return a ?? b;
|
|
346
661
|
}
|
|
347
662
|
async createDustActionTransaction(action, nightUtxos, nightVerifyingKey, signDustRegistration) {
|
|
@@ -390,6 +705,11 @@ export class WalletFacade {
|
|
|
390
705
|
// on-chain with BalanceCheckOverspend. Skip for re-registration (all guaranteed UTxOs already
|
|
391
706
|
// registered) since `feePayment` is 0 by design and the caller is expected to balance the fee
|
|
392
707
|
// externally via `balanceUnprovenTransaction({ tokenKindsToBalance: ['dust'] })`.
|
|
708
|
+
//
|
|
709
|
+
// `registeredForDustGeneration` is the indexer's answer as of the chain's current dust epoch, so it is the one
|
|
710
|
+
// authority on which of the two a registration is. Night carried across the v8 -> v9 fork arrives with it set to
|
|
711
|
+
// `false` — the fork wipes dust generation state and the unshielded crossing carries the flag accordingly — so a
|
|
712
|
+
// re-registration on ledger-v9 is correctly treated as first-time.
|
|
393
713
|
const hasUnregisteredGuaranteed = split.guaranteedUtxos.some((u) => !u.utxo.registeredForDustGeneration);
|
|
394
714
|
if (isRegistration && hasUnregisteredGuaranteed) {
|
|
395
715
|
const fee = await this.dust.calculateFee([txWithDustActions]);
|
|
@@ -402,7 +722,7 @@ export class WalletFacade {
|
|
|
402
722
|
// Step 5 — Sign via the standard signRecipe pathway, which now stamps both the unshielded
|
|
403
723
|
// offers and the dust registration. Signing failures also need to release the booking.
|
|
404
724
|
try {
|
|
405
|
-
const signedRecipe = await this.signRecipe({ type: 'UNPROVEN_TRANSACTION', transaction: txWithDustActions }, signDustRegistration);
|
|
725
|
+
const signedRecipe = await this.signRecipe({ type: 'UNPROVEN_TRANSACTION', protocolVersion: this.currentVersion(), transaction: txWithDustActions }, signDustRegistration);
|
|
406
726
|
if (signedRecipe.type !== 'UNPROVEN_TRANSACTION') {
|
|
407
727
|
throw Error('signRecipe returned unexpected recipe type for dust action transaction.');
|
|
408
728
|
}
|
|
@@ -419,7 +739,7 @@ export class WalletFacade {
|
|
|
419
739
|
this.unshielded.state,
|
|
420
740
|
this.dust.state,
|
|
421
741
|
this.pendingTransactionsService.state(),
|
|
422
|
-
]).pipe(map(([shieldedState, unshieldedState, dustState, pending]) => new FacadeState(shieldedState, unshieldedState, dustState, pending)));
|
|
742
|
+
]).pipe(map(([shieldedState, unshieldedState, dustState, pending]) => new FacadeState(shieldedState, unshieldedState, dustState, pending, this.#forkVersion)));
|
|
423
743
|
}
|
|
424
744
|
async waitForSyncedState() {
|
|
425
745
|
const [shieldedState, unshieldedState, dustState, pending] = await Promise.all([
|
|
@@ -428,7 +748,7 @@ export class WalletFacade {
|
|
|
428
748
|
this.dust.waitForSyncedState(),
|
|
429
749
|
firstValueFrom(this.pendingTransactionsService.state()),
|
|
430
750
|
]);
|
|
431
|
-
return new FacadeState(shieldedState, unshieldedState, dustState, pending);
|
|
751
|
+
return new FacadeState(shieldedState, unshieldedState, dustState, pending, this.#forkVersion);
|
|
432
752
|
}
|
|
433
753
|
/**
|
|
434
754
|
* Submits a finalized transaction to the network and tracks it as pending until finalized or discarded.
|
|
@@ -441,12 +761,13 @@ export class WalletFacade {
|
|
|
441
761
|
* @throws {@link WellFormedError} — call {@link validateTransaction} first to get this error early.
|
|
442
762
|
*/
|
|
443
763
|
async submitTransaction(tx) {
|
|
444
|
-
const
|
|
764
|
+
const carried = this.accept(tx);
|
|
765
|
+
const identifiers = carried.identifiers();
|
|
445
766
|
try {
|
|
446
|
-
await this.pendingTransactionsService.addPendingTransaction(tx);
|
|
767
|
+
await this.pendingTransactionsService.addPendingTransaction(tx, this.#observedProtocolVersion.getValue());
|
|
447
768
|
// Insert before awaiting submission so the entry exists while the tx is in flight — the per-wallet sync
|
|
448
769
|
// handlers' gotFinalized call clears the pending entry on confirmation.
|
|
449
|
-
const key = submitTxHistoryKey(
|
|
770
|
+
const key = submitTxHistoryKey(carried);
|
|
450
771
|
await this.#txHistoryStorage.gotPending({ ...key, submittedAt: this.clock.now() });
|
|
451
772
|
await this.submissionService.submitTransaction(tx, 'Finalized');
|
|
452
773
|
return identifiers.at(-1);
|
|
@@ -468,22 +789,19 @@ export class WalletFacade {
|
|
|
468
789
|
* @param options - TTL for the balancing transaction, and optional subset of token kinds to balance.
|
|
469
790
|
* @returns A {@link FinalizedTransactionRecipe} containing the original and balancing transactions.
|
|
470
791
|
*/
|
|
471
|
-
async balanceFinalizedTransaction(tx,
|
|
472
|
-
const { shieldedSecretKeys, dustSecretKey } = secretKeys;
|
|
792
|
+
async balanceFinalizedTransaction(tx, options) {
|
|
473
793
|
const { ttl, tokenKindsToBalance = 'all' } = options;
|
|
474
794
|
const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
|
|
475
795
|
// Step 1: Run unshielded and shielded balancing
|
|
476
796
|
const unshieldedBalancingTx = shouldBalanceUnshielded
|
|
477
797
|
? await this.unshielded.balanceFinalizedTransaction(tx)
|
|
478
798
|
: undefined;
|
|
479
|
-
const shieldedBalancingTx = shouldBalanceShielded
|
|
480
|
-
? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
|
|
481
|
-
: undefined;
|
|
799
|
+
const shieldedBalancingTx = shouldBalanceShielded ? await this.shielded.balanceTransaction(tx) : undefined;
|
|
482
800
|
// Step 2: Merge unshielded and shielded balancing
|
|
483
801
|
const mergedBalancingTx = this.mergeUnprovenTransactions(shieldedBalancingTx, unshieldedBalancingTx);
|
|
484
802
|
// Step 3: Conditionally add dust/fee balancing
|
|
485
803
|
const dustResult = shouldBalanceDust
|
|
486
|
-
? await this.dust.balanceTransactions(
|
|
804
|
+
? await this.dust.balanceTransactions(mergedBalancingTx ? [tx, mergedBalancingTx] : [tx], ttl)
|
|
487
805
|
: undefined;
|
|
488
806
|
const feeBalancingTx = dustResult?.transaction;
|
|
489
807
|
// Step 4: Merge fee balancing and create final recipe
|
|
@@ -493,6 +811,7 @@ export class WalletFacade {
|
|
|
493
811
|
}
|
|
494
812
|
return {
|
|
495
813
|
type: 'FINALIZED_TRANSACTION',
|
|
814
|
+
protocolVersion: this.currentVersion(),
|
|
496
815
|
originalTransaction: tx,
|
|
497
816
|
balancingTransaction: balancingTx,
|
|
498
817
|
...(dustResult ? { blockData: dustResult.blockData } : {}),
|
|
@@ -510,14 +829,11 @@ export class WalletFacade {
|
|
|
510
829
|
* @param options - TTL for the balancing transaction, and optional subset of token kinds to balance.
|
|
511
830
|
* @returns An {@link UnboundTransactionRecipe} containing the base and optional balancing transactions.
|
|
512
831
|
*/
|
|
513
|
-
async balanceUnboundTransaction(tx,
|
|
514
|
-
const { shieldedSecretKeys, dustSecretKey } = secretKeys;
|
|
832
|
+
async balanceUnboundTransaction(tx, options) {
|
|
515
833
|
const { ttl, tokenKindsToBalance = 'all' } = options;
|
|
516
834
|
const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
|
|
517
835
|
// Step 1: Run unshielded and shielded balancing
|
|
518
|
-
const shieldedBalancingTx = shouldBalanceShielded
|
|
519
|
-
? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
|
|
520
|
-
: undefined;
|
|
836
|
+
const shieldedBalancingTx = shouldBalanceShielded ? await this.shielded.balanceTransaction(tx) : undefined;
|
|
521
837
|
// For unbound transactions, unshielded balancing happens in place not with a balancing transaction
|
|
522
838
|
const balancedUnshieldedTx = shouldBalanceUnshielded
|
|
523
839
|
? await this.unshielded.balanceUnboundTransaction(tx)
|
|
@@ -526,7 +842,7 @@ export class WalletFacade {
|
|
|
526
842
|
const baseTx = balancedUnshieldedTx ?? tx;
|
|
527
843
|
// Step 3: Conditionally add dust/fee balancing
|
|
528
844
|
const dustResult = shouldBalanceDust
|
|
529
|
-
? await this.dust.balanceTransactions(
|
|
845
|
+
? await this.dust.balanceTransactions(shieldedBalancingTx ? [baseTx, shieldedBalancingTx] : [baseTx], ttl)
|
|
530
846
|
: undefined;
|
|
531
847
|
const feeBalancingTransaction = dustResult?.transaction;
|
|
532
848
|
// Step 4: Create the final balancing transaction
|
|
@@ -537,6 +853,7 @@ export class WalletFacade {
|
|
|
537
853
|
}
|
|
538
854
|
return {
|
|
539
855
|
type: 'UNBOUND_TRANSACTION',
|
|
856
|
+
protocolVersion: this.currentVersion(),
|
|
540
857
|
baseTransaction: baseTx,
|
|
541
858
|
balancingTransaction: balancingTransaction ?? undefined,
|
|
542
859
|
...(dustResult ? { blockData: dustResult.blockData } : {}),
|
|
@@ -554,14 +871,11 @@ export class WalletFacade {
|
|
|
554
871
|
* @param options - TTL for the balancing transaction, and optional subset of token kinds to balance.
|
|
555
872
|
* @returns An {@link UnprovenTransactionRecipe} containing the balanced transaction.
|
|
556
873
|
*/
|
|
557
|
-
async balanceUnprovenTransaction(tx,
|
|
558
|
-
const { shieldedSecretKeys, dustSecretKey } = secretKeys;
|
|
874
|
+
async balanceUnprovenTransaction(tx, options) {
|
|
559
875
|
const { ttl, tokenKindsToBalance = 'all' } = options;
|
|
560
876
|
const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
|
|
561
877
|
// Step 1: Run unshielded and shielded balancing
|
|
562
|
-
const shieldedBalancingTx = shouldBalanceShielded
|
|
563
|
-
? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
|
|
564
|
-
: undefined;
|
|
878
|
+
const shieldedBalancingTx = shouldBalanceShielded ? await this.shielded.balanceTransaction(tx) : undefined;
|
|
565
879
|
// For unproven transactions, unshielded balancing happens in place
|
|
566
880
|
const balancedUnshieldedTx = shouldBalanceUnshielded
|
|
567
881
|
? await this.unshielded.balanceUnprovenTransaction(tx)
|
|
@@ -571,14 +885,13 @@ export class WalletFacade {
|
|
|
571
885
|
// Step 3: Merge shielded balancing into base tx if present
|
|
572
886
|
const mergedTx = this.mergeUnprovenTransactions(baseTx, shieldedBalancingTx);
|
|
573
887
|
// Step 4: Conditionally add dust/fee balancing
|
|
574
|
-
const dustResult = shouldBalanceDust
|
|
575
|
-
? await this.dust.balanceTransactions(dustSecretKey, [mergedTx], ttl)
|
|
576
|
-
: undefined;
|
|
888
|
+
const dustResult = shouldBalanceDust ? await this.dust.balanceTransactions([mergedTx], ttl) : undefined;
|
|
577
889
|
const feeBalancingTx = dustResult?.transaction;
|
|
578
890
|
// Step 5: Merge fee balancing if present
|
|
579
891
|
const balancedTx = this.mergeUnprovenTransactions(mergedTx, feeBalancingTx);
|
|
580
892
|
return {
|
|
581
893
|
type: 'UNPROVEN_TRANSACTION',
|
|
894
|
+
protocolVersion: this.currentVersion(),
|
|
582
895
|
transaction: balancedTx,
|
|
583
896
|
...(dustResult ? { blockData: dustResult.blockData } : {}),
|
|
584
897
|
};
|
|
@@ -589,14 +902,16 @@ export class WalletFacade {
|
|
|
589
902
|
switch (recipe.type) {
|
|
590
903
|
case 'FINALIZED_TRANSACTION': {
|
|
591
904
|
const finalizedBalancing = await this.finalizeTransaction(recipe.balancingTransaction);
|
|
592
|
-
return recipe.originalTransaction.merge(finalizedBalancing);
|
|
905
|
+
return this.seal('Finalized', this.accept(recipe.originalTransaction).merge(this.accept(finalizedBalancing)), recipe.originalTransaction.protocolVersion);
|
|
593
906
|
}
|
|
594
907
|
case 'UNBOUND_TRANSACTION': {
|
|
595
908
|
const finalizedBalancingTx = recipe.balancingTransaction
|
|
596
909
|
? await this.finalizeTransaction(recipe.balancingTransaction)
|
|
597
910
|
: undefined;
|
|
598
|
-
const finalizedTransaction = recipe.baseTransaction.bind();
|
|
599
|
-
return
|
|
911
|
+
const finalizedTransaction = this.accept(recipe.baseTransaction).bind();
|
|
912
|
+
return this.seal('Finalized', finalizedBalancingTx
|
|
913
|
+
? finalizedTransaction.merge(this.accept(finalizedBalancingTx))
|
|
914
|
+
: finalizedTransaction, recipe.baseTransaction.protocolVersion);
|
|
600
915
|
}
|
|
601
916
|
case 'UNPROVEN_TRANSACTION': {
|
|
602
917
|
return await this.finalizeTransaction(recipe.transaction);
|
|
@@ -604,7 +919,7 @@ export class WalletFacade {
|
|
|
604
919
|
}
|
|
605
920
|
})
|
|
606
921
|
.then(async (finalizedTx) => {
|
|
607
|
-
await this.pendingTransactionsService.addPendingTransaction(finalizedTx);
|
|
922
|
+
await this.pendingTransactionsService.addPendingTransaction(finalizedTx, Option.some(finalizedTx.protocolVersion));
|
|
608
923
|
return finalizedTx;
|
|
609
924
|
});
|
|
610
925
|
}
|
|
@@ -615,6 +930,7 @@ export class WalletFacade {
|
|
|
615
930
|
const withDustSig = await this.#signDustRegistrationIfPresent(signedBalancingTx, signSegment);
|
|
616
931
|
return {
|
|
617
932
|
type: 'FINALIZED_TRANSACTION',
|
|
933
|
+
protocolVersion: recipe.protocolVersion,
|
|
618
934
|
originalTransaction: recipe.originalTransaction,
|
|
619
935
|
balancingTransaction: withDustSig,
|
|
620
936
|
...(recipe.blockData ? { blockData: recipe.blockData } : {}),
|
|
@@ -627,6 +943,7 @@ export class WalletFacade {
|
|
|
627
943
|
const signedBaseTx = await this.signUnboundTransaction(recipe.baseTransaction, signSegment);
|
|
628
944
|
return {
|
|
629
945
|
type: 'UNBOUND_TRANSACTION',
|
|
946
|
+
protocolVersion: recipe.protocolVersion,
|
|
630
947
|
baseTransaction: signedBaseTx,
|
|
631
948
|
balancingTransaction: signedBalancingTx,
|
|
632
949
|
...(recipe.blockData ? { blockData: recipe.blockData } : {}),
|
|
@@ -637,6 +954,7 @@ export class WalletFacade {
|
|
|
637
954
|
const withDustSig = await this.#signDustRegistrationIfPresent(signedTx, signSegment);
|
|
638
955
|
return {
|
|
639
956
|
type: 'UNPROVEN_TRANSACTION',
|
|
957
|
+
protocolVersion: recipe.protocolVersion,
|
|
640
958
|
transaction: withDustSig,
|
|
641
959
|
...(recipe.blockData ? { blockData: recipe.blockData } : {}),
|
|
642
960
|
};
|
|
@@ -644,7 +962,7 @@ export class WalletFacade {
|
|
|
644
962
|
}
|
|
645
963
|
}
|
|
646
964
|
async #signDustRegistrationIfPresent(tx, signSegment) {
|
|
647
|
-
const intent = tx.intents?.get(1);
|
|
965
|
+
const intent = this.accept(tx).intents?.get(1);
|
|
648
966
|
const registrations = intent?.dustActions?.registrations ?? [];
|
|
649
967
|
if (!intent || registrations.length === 0) {
|
|
650
968
|
return tx;
|
|
@@ -658,11 +976,34 @@ export class WalletFacade {
|
|
|
658
976
|
async signUnboundTransaction(tx, signSegment) {
|
|
659
977
|
return await this.unshielded.signUnboundTransaction(tx, signSegment);
|
|
660
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Proves and binds an unproven transaction, and records it as pending.
|
|
981
|
+
*
|
|
982
|
+
* @remarks
|
|
983
|
+
* Proved at the version stamped on the transaction itself, which is the version that fixed its bytes, and sealed at
|
|
984
|
+
* that same version: proving is a long await, so the wallets can move under it, and neither the prover a
|
|
985
|
+
* transaction is routed to nor the stamp it comes back with may be decided by where they have moved to.
|
|
986
|
+
*
|
|
987
|
+
* Where they have moved to decides one thing only, and it is decided again once the proof is back: a crossing while
|
|
988
|
+
* the transaction was in flight leaves it belonging to an epoch the wallets have left, and no chain they are on can
|
|
989
|
+
* include it. Such a transaction is refused rather than sealed, and nothing is recorded as pending for it — a
|
|
990
|
+
* pending entry would only wait out a TTL for an inclusion that cannot happen — while the reservations it made in
|
|
991
|
+
* the three wallets are given back like those of any other failure here.
|
|
992
|
+
* @param tx The unproven transaction.
|
|
993
|
+
* @returns The finalized transaction, stamped with the version it was authored at.
|
|
994
|
+
* @throws {@link ProtocolVersionMismatchError} When the wallets left the transaction's epoch while it was being
|
|
995
|
+
* proved, or were never in it.
|
|
996
|
+
*/
|
|
661
997
|
async finalizeTransaction(tx) {
|
|
662
998
|
try {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
await this.
|
|
999
|
+
// Named as the prover's input rather than its output: the router hands the transaction to the prover registered
|
|
1000
|
+
// for the version it was authored at, and what comes back is that ledger version's unbound transaction.
|
|
1001
|
+
const unboundTx = await this.provingService.prove(this.accept(tx), tx.protocolVersion);
|
|
1002
|
+
// Read again on the far side of the proof: the epoch is the one thing about the transaction that a crossing
|
|
1003
|
+
// during proving can change, and this is where it is caught.
|
|
1004
|
+
this.accept(tx);
|
|
1005
|
+
const finalizedTx = this.seal('Finalized', unboundTx.bind(), tx.protocolVersion);
|
|
1006
|
+
await this.pendingTransactionsService.addPendingTransaction(finalizedTx, Option.some(finalizedTx.protocolVersion));
|
|
666
1007
|
return finalizedTx;
|
|
667
1008
|
}
|
|
668
1009
|
catch (error) {
|
|
@@ -679,12 +1020,11 @@ export class WalletFacade {
|
|
|
679
1020
|
return await this.dust.calculateFee([tx]);
|
|
680
1021
|
}
|
|
681
1022
|
/** Calculates the total fee for the given transaction plus the fee of the balancing transaction. */
|
|
682
|
-
async estimateTransactionFee(tx,
|
|
1023
|
+
async estimateTransactionFee(tx, options) {
|
|
683
1024
|
const ttl = options?.ttl ?? this.defaultTtl();
|
|
684
|
-
return await this.dust.estimateFee(
|
|
1025
|
+
return await this.dust.estimateFee([tx], ttl, options?.currentTime);
|
|
685
1026
|
}
|
|
686
|
-
async transferTransaction(outputs,
|
|
687
|
-
const { shieldedSecretKeys, dustSecretKey } = secretKeys;
|
|
1027
|
+
async transferTransaction(outputs, options) {
|
|
688
1028
|
const { ttl, payFees = true } = options;
|
|
689
1029
|
const unshieldedOutputs = outputs
|
|
690
1030
|
.filter((output) => output.type === 'unshielded')
|
|
@@ -693,17 +1033,16 @@ export class WalletFacade {
|
|
|
693
1033
|
if (unshieldedOutputs.length === 0 && shieldedOutputs.length === 0) {
|
|
694
1034
|
throw Error('At least one shielded or unshielded output is required.');
|
|
695
1035
|
}
|
|
696
|
-
const shieldedTx = shieldedOutputs.length > 0
|
|
697
|
-
? await this.shielded.transferTransaction(shieldedSecretKeys, shieldedOutputs)
|
|
698
|
-
: undefined;
|
|
1036
|
+
const shieldedTx = shieldedOutputs.length > 0 ? await this.shielded.transferTransaction(shieldedOutputs) : undefined;
|
|
699
1037
|
const unshieldedTx = unshieldedOutputs.length > 0 ? await this.unshielded.transferTransaction(unshieldedOutputs, ttl) : undefined;
|
|
700
1038
|
const mergedTxs = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
|
|
701
1039
|
// Add fee payment
|
|
702
|
-
const dustResult = payFees ? await this.dust.balanceTransactions(
|
|
1040
|
+
const dustResult = payFees ? await this.dust.balanceTransactions([mergedTxs], ttl) : undefined;
|
|
703
1041
|
const feeBalancingTx = dustResult?.transaction;
|
|
704
1042
|
const finalTx = this.mergeUnprovenTransactions(mergedTxs, feeBalancingTx);
|
|
705
1043
|
return {
|
|
706
1044
|
type: 'UNPROVEN_TRANSACTION',
|
|
1045
|
+
protocolVersion: this.currentVersion(),
|
|
707
1046
|
transaction: finalTx,
|
|
708
1047
|
...(dustResult ? { blockData: dustResult.blockData } : {}),
|
|
709
1048
|
};
|
|
@@ -722,9 +1061,12 @@ export class WalletFacade {
|
|
|
722
1061
|
...utxo,
|
|
723
1062
|
ctime: meta.ctime,
|
|
724
1063
|
registeredForDustGeneration: meta.registeredForDustGeneration,
|
|
725
|
-
})), (utxosWithMeta) => dustState.estimateDustGeneration(utxosWithMeta, now), (estimatedUtxos) => dustState.
|
|
726
|
-
const
|
|
727
|
-
|
|
1064
|
+
})), (utxosWithMeta) => dustState.estimateDustGeneration(utxosWithMeta, now), (estimatedUtxos) => dustState.splitNightUtxos(estimatedUtxos), (split) => split.guaranteed);
|
|
1065
|
+
const authoring = this.authoring();
|
|
1066
|
+
// Type cast required because: a signing key belongs to one ledger version's runtime and the two are nominally
|
|
1067
|
+
// distinct, so the epoch's own primitives are the only things that may touch it; it never leaves this block.
|
|
1068
|
+
const fakeSigningKey = authoring.sampleSigningKey();
|
|
1069
|
+
const fakeVerifyingKey = authoring.signatureVerifyingKey(fakeSigningKey);
|
|
728
1070
|
// Use the legacy dust-only construction path here so estimation does NOT book real UTxOs in the
|
|
729
1071
|
// unshielded wallet state. (The race-fix path in createDustActionTransaction books on purpose;
|
|
730
1072
|
// estimation is meant to be observation-only.)
|
|
@@ -734,22 +1076,20 @@ export class WalletFacade {
|
|
|
734
1076
|
ctime: meta.ctime,
|
|
735
1077
|
registeredForDustGeneration: meta.registeredForDustGeneration,
|
|
736
1078
|
})), fakeVerifyingKey, dustState.address);
|
|
737
|
-
const intent = fakeUnsignedTx.intents?.get(1);
|
|
1079
|
+
const intent = this.accept(fakeUnsignedTx).intents?.get(1);
|
|
738
1080
|
if (!intent) {
|
|
739
1081
|
throw Error('Dust generation transaction is missing intent segment 1.');
|
|
740
1082
|
}
|
|
741
|
-
const
|
|
742
|
-
const signature = ledger.signData(fakeSigningKey, signatureData);
|
|
1083
|
+
const signature = authoring.signData(fakeSigningKey, intent.signatureData(1));
|
|
743
1084
|
const fakeSignedTx = await this.dust.addDustGenerationSignature(fakeUnsignedTx, signature);
|
|
744
|
-
const finalizedFakeTx = fakeSignedTx.mockProve().bind();
|
|
1085
|
+
const finalizedFakeTx = this.seal('Finalized', this.accept(fakeSignedTx).mockProve().bind(), this.currentVersion());
|
|
745
1086
|
const fee = await this.calculateTransactionFee(finalizedFakeTx);
|
|
746
1087
|
return {
|
|
747
1088
|
fee,
|
|
748
1089
|
dustGenerationEstimations,
|
|
749
1090
|
};
|
|
750
1091
|
}
|
|
751
|
-
async initSwap(desiredInputs, desiredOutputs,
|
|
752
|
-
const { shieldedSecretKeys, dustSecretKey } = secretKeys;
|
|
1092
|
+
async initSwap(desiredInputs, desiredOutputs, options) {
|
|
753
1093
|
const { ttl, payFees = false } = options;
|
|
754
1094
|
const { shielded: shieldedInputs, unshielded: unshieldedInputs } = desiredInputs;
|
|
755
1095
|
const shieldedOutputs = desiredOutputs
|
|
@@ -763,21 +1103,23 @@ export class WalletFacade {
|
|
|
763
1103
|
if (!hasShieldedPart && !hasUnshieldedPart) {
|
|
764
1104
|
throw Error('At least one shielded or unshielded swap is required.');
|
|
765
1105
|
}
|
|
766
|
-
|
|
767
|
-
|
|
1106
|
+
// Build each leg if the swap involves that token kind, even when it has outputs but no inputs.
|
|
1107
|
+
const shieldedTx = hasShieldedPart
|
|
1108
|
+
? await this.shielded.initSwap(shieldedInputs ?? {}, shieldedOutputs)
|
|
768
1109
|
: undefined;
|
|
769
|
-
const unshieldedTx = hasUnshieldedPart
|
|
770
|
-
? await this.unshielded.initSwap(unshieldedInputs, unshieldedOutputs, ttl)
|
|
1110
|
+
const unshieldedTx = hasUnshieldedPart
|
|
1111
|
+
? await this.unshielded.initSwap(unshieldedInputs ?? {}, unshieldedOutputs, ttl)
|
|
771
1112
|
: undefined;
|
|
772
1113
|
const combinedTx = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
|
|
773
1114
|
if (!combinedTx) {
|
|
774
1115
|
throw Error('Unexpected transaction state.');
|
|
775
1116
|
}
|
|
776
|
-
const dustResult = payFees ? await this.dust.balanceTransactions(
|
|
1117
|
+
const dustResult = payFees ? await this.dust.balanceTransactions([combinedTx], ttl) : undefined;
|
|
777
1118
|
const feeBalancingTx = dustResult?.transaction;
|
|
778
1119
|
const finalTx = this.mergeUnprovenTransactions(combinedTx, feeBalancingTx);
|
|
779
1120
|
return {
|
|
780
1121
|
type: 'UNPROVEN_TRANSACTION',
|
|
1122
|
+
protocolVersion: this.currentVersion(),
|
|
781
1123
|
transaction: finalTx,
|
|
782
1124
|
...(dustResult ? { blockData: dustResult.blockData } : {}),
|
|
783
1125
|
};
|
|
@@ -790,6 +1132,7 @@ export class WalletFacade {
|
|
|
790
1132
|
const dustRegistrationTx = await this.createDustActionTransaction({ type: 'registration', dustReceiverAddress: receiverAddress }, nightUtxos, nightVerifyingKey, signDustRegistration);
|
|
791
1133
|
return {
|
|
792
1134
|
type: 'UNPROVEN_TRANSACTION',
|
|
1135
|
+
protocolVersion: this.currentVersion(),
|
|
793
1136
|
transaction: dustRegistrationTx,
|
|
794
1137
|
};
|
|
795
1138
|
}
|
|
@@ -814,37 +1157,111 @@ export class WalletFacade {
|
|
|
814
1157
|
const dustDeregistrationTx = await this.createDustActionTransaction({ type: 'deregistration' }, nightUtxos, nightVerifyingKey, signDustRegistration);
|
|
815
1158
|
return {
|
|
816
1159
|
type: 'UNPROVEN_TRANSACTION',
|
|
1160
|
+
protocolVersion: this.currentVersion(),
|
|
817
1161
|
transaction: dustDeregistrationTx,
|
|
818
1162
|
};
|
|
819
1163
|
}
|
|
820
|
-
async revert(txOrRecipe) {
|
|
1164
|
+
async revert(txOrRecipe, reason) {
|
|
821
1165
|
// avoid instanceof check
|
|
822
1166
|
const transactionsToRevert = BalancingRecipe.isRecipe(txOrRecipe)
|
|
823
1167
|
? BalancingRecipe.getTransactions(txOrRecipe)
|
|
824
1168
|
: [txOrRecipe];
|
|
825
|
-
await Promise.all(transactionsToRevert.map((tx) => this.revertTransaction(tx)));
|
|
1169
|
+
await Promise.all(transactionsToRevert.map((tx) => this.revertTransaction(tx, reason)));
|
|
826
1170
|
}
|
|
827
|
-
|
|
1171
|
+
/**
|
|
1172
|
+
* The finalized entry recording `tx`, if sync has written one, even under the hash aggregation gave it on-chain. A
|
|
1173
|
+
* transaction whose stamp no epoch can read has no entry, rather than an error, so a verdict on it still resolves.
|
|
1174
|
+
*/
|
|
1175
|
+
finalizedEntryFor(tx, history) {
|
|
1176
|
+
return Option.match(Either.getRight(WalletTransaction.unwrapWithin(tx, this.epochOf(tx.protocolVersion))), {
|
|
1177
|
+
onNone: () => undefined,
|
|
1178
|
+
onSome: (carried) => {
|
|
1179
|
+
const key = submitTxHistoryKey(carried);
|
|
1180
|
+
return history.find((entry) => isFinalizedWalletEntry(entry) && TransactionHistoryStorage.coversTransaction(entry, key));
|
|
1181
|
+
},
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/** Sync can record inclusion before the independent pending-status poller observes it. */
|
|
1185
|
+
async reconcileFinalizedTransaction(tx, history) {
|
|
1186
|
+
const entry = this.finalizedEntryFor(tx, history);
|
|
1187
|
+
if (entry === undefined)
|
|
1188
|
+
return false;
|
|
1189
|
+
// Included failures still need their unexecuted coin reservations released.
|
|
1190
|
+
if (entry.status === 'FAILURE' || entry.status === 'PARTIAL_SUCCESS') {
|
|
1191
|
+
await this.revertTransaction(tx);
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
await this.pendingTransactionsService.clear(tx);
|
|
1195
|
+
}
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
async revertTransaction(tx, reason) {
|
|
828
1199
|
await Promise.all([
|
|
829
1200
|
this.shielded.revertTransaction(tx),
|
|
830
1201
|
this.unshielded.revertTransaction(tx),
|
|
831
1202
|
this.dust.revertTransaction(tx),
|
|
832
1203
|
]).then(async () => {
|
|
1204
|
+
// Reverting is total over the stages: a transaction at any of them may have booked coins, and the pending set
|
|
1205
|
+
// recognises only the finalized ones. Narrowing rather than casting is what says so.
|
|
833
1206
|
await this.pendingTransactionsService.clear(tx);
|
|
834
|
-
|
|
1207
|
+
// Read at the epoch the transaction was built for, not the one the facade now acts at. A verdict on a
|
|
1208
|
+
// transaction submitted before a protocol boundary can only arrive after it — a chain rejection, a TTL run out,
|
|
1209
|
+
// or the wallet giving up on bytes that can never be included — and by then the facade has crossed. The pending
|
|
1210
|
+
// entry that verdict has to land on was written by this same session before the crossing, and it is the only
|
|
1211
|
+
// record an application has that the transaction will never be included; keying it against the current epoch
|
|
1212
|
+
// would leave it saying `pending` for the rest of the session. The stamp is what chooses the reader, which is
|
|
1213
|
+
// what the stamp is for.
|
|
1214
|
+
const key = Option.match(Either.getRight(WalletTransaction.unwrapWithin(tx, this.epochOf(tx.protocolVersion))), { onNone: () => undefined, onSome: revertTxHistoryKey });
|
|
835
1215
|
if (key !== undefined) {
|
|
836
|
-
await this.#txHistoryStorage.gotRejected({
|
|
1216
|
+
await this.#txHistoryStorage.gotRejected({
|
|
1217
|
+
...key,
|
|
1218
|
+
rejectedAt: this.clock.now(),
|
|
1219
|
+
...(reason !== undefined ? { reason } : {}),
|
|
1220
|
+
});
|
|
837
1221
|
}
|
|
838
1222
|
});
|
|
839
1223
|
}
|
|
840
|
-
|
|
1224
|
+
/**
|
|
1225
|
+
* Starts the wallets and their background synchronization.
|
|
1226
|
+
*
|
|
1227
|
+
* @remarks
|
|
1228
|
+
* Seeds, not key objects. A seed is the only key material that crosses a protocol boundary — every ledger version
|
|
1229
|
+
* derives its own keys from the same seed and arrives at the same identity — so a wallet started from seeds can
|
|
1230
|
+
* follow the chain across a fork, and one started from key objects of a single ledger version cannot. Derive them
|
|
1231
|
+
* once with `WalletSeeds.fromMasterSeed` and hand them over.
|
|
1232
|
+
*
|
|
1233
|
+
* {@link FacadeKeysByEpoch} is the escape hatch for a caller that will not part with a seed. It requires **both**
|
|
1234
|
+
* ledger versions' key objects, because a wallet holding one side could not read the other side of the chain, and
|
|
1235
|
+
* it costs the caller an import of both ledger packages and the same derivation performed twice. A seed costs
|
|
1236
|
+
* neither.
|
|
1237
|
+
* @example
|
|
1238
|
+
* ```typescript
|
|
1239
|
+
* await facade.start(WalletSeeds.fromMasterSeed(masterSeed));
|
|
1240
|
+
* ```;
|
|
1241
|
+
*
|
|
1242
|
+
* @param material The three wallets' seeds, or both ledger versions' key objects.
|
|
1243
|
+
* @param options `manualSync` leaves the dust wallet unstarted in the background; drive it explicitly with
|
|
1244
|
+
* {@link doSync} instead (requires a dust wallet built with the projections sync service, see
|
|
1245
|
+
* `makeEventLessSyncService`).
|
|
1246
|
+
*/
|
|
1247
|
+
async start(material, options = {}) {
|
|
1248
|
+
const keys = v9KeysOf(material);
|
|
841
1249
|
await Promise.all([
|
|
842
|
-
this.shielded.start(
|
|
1250
|
+
this.shielded.start(keys.shielded),
|
|
843
1251
|
this.unshielded.start(),
|
|
844
|
-
this.dust.start(
|
|
1252
|
+
!options.manualSync ? this.dust.start(keys.dust) : undefined,
|
|
845
1253
|
this.pendingTransactionsService.start(),
|
|
846
1254
|
]);
|
|
847
1255
|
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Runs a single dust synchronization pass and resolves when it completes. Only the dust wallet supports manual sync;
|
|
1258
|
+
* the shielded and unshielded wallets keep syncing in the background via {@link start}.
|
|
1259
|
+
*
|
|
1260
|
+
* @param material The same material {@link start} was given.
|
|
1261
|
+
*/
|
|
1262
|
+
async doSync(material) {
|
|
1263
|
+
await this.dust.stepSync(v9KeysOf(material).dust);
|
|
1264
|
+
}
|
|
848
1265
|
async stop() {
|
|
849
1266
|
await Promise.all([
|
|
850
1267
|
this.shielded.stop(),
|
|
@@ -853,6 +1270,7 @@ export class WalletFacade {
|
|
|
853
1270
|
this.submissionService.close(),
|
|
854
1271
|
this.pendingTransactionsService.stop(),
|
|
855
1272
|
Promise.resolve(this.#pendingSubscription?.unsubscribe()),
|
|
1273
|
+
Promise.resolve(this.#protocolVersionSubscription?.unsubscribe()),
|
|
856
1274
|
]);
|
|
857
1275
|
}
|
|
858
1276
|
async queryTxHistoryByHash(hash) {
|