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