@agoric/smart-wallet 0.5.4-u13.0 → 0.5.4-upgrade-14-dev-0a0580c.0
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/package.json +15 -13
- package/src/offerWatcher.d.ts +46 -0
- package/src/offerWatcher.d.ts.map +1 -0
- package/src/offerWatcher.js +248 -0
- package/src/offers.d.ts +0 -24
- package/src/offers.d.ts.map +1 -1
- package/src/offers.js +0 -174
- package/src/proposals/upgrade-wallet-factory2-proposal.d.ts +23 -0
- package/src/proposals/upgrade-wallet-factory2-proposal.d.ts.map +1 -0
- package/src/proposals/upgrade-wallet-factory2-proposal.js +59 -0
- package/src/smartWallet.d.ts +37 -14
- package/src/smartWallet.d.ts.map +1 -1
- package/src/smartWallet.js +518 -143
- package/src/walletFactory.d.ts +6 -4
- package/src/walletFactory.d.ts.map +1 -1
- package/src/walletFactory.js +35 -3
- package/src/payments.d.ts +0 -20
- package/src/payments.d.ts.map +0 -1
- package/src/payments.js +0 -89
package/src/smartWallet.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// @ts-nocheck
|
|
1
|
+
import { E } from '@endo/far';
|
|
3
2
|
import {
|
|
4
3
|
AmountShape,
|
|
5
4
|
BrandShape,
|
|
@@ -8,8 +7,13 @@ import {
|
|
|
8
7
|
PaymentShape,
|
|
9
8
|
PurseShape,
|
|
10
9
|
} from '@agoric/ertp';
|
|
11
|
-
import {
|
|
12
|
-
|
|
10
|
+
import {
|
|
11
|
+
deeplyFulfilledObject,
|
|
12
|
+
makeTracer,
|
|
13
|
+
objectMap,
|
|
14
|
+
StorageNodeShape,
|
|
15
|
+
} from '@agoric/internal';
|
|
16
|
+
import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';
|
|
13
17
|
import { M, mustMatch } from '@agoric/store';
|
|
14
18
|
import {
|
|
15
19
|
appendToStoredArray,
|
|
@@ -18,19 +22,25 @@ import {
|
|
|
18
22
|
import {
|
|
19
23
|
makeScalarBigMapStore,
|
|
20
24
|
makeScalarBigWeakMapStore,
|
|
25
|
+
prepareExoClass,
|
|
21
26
|
prepareExoClassKit,
|
|
22
27
|
provide,
|
|
28
|
+
watchPromise,
|
|
23
29
|
} from '@agoric/vat-data';
|
|
24
30
|
import {
|
|
31
|
+
prepareRecorderKit,
|
|
25
32
|
SubscriberShape,
|
|
26
33
|
TopicsRecordShape,
|
|
27
|
-
prepareRecorderKit,
|
|
28
34
|
} from '@agoric/zoe/src/contractSupport/index.js';
|
|
29
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
AmountKeywordRecordShape,
|
|
37
|
+
PaymentPKeywordRecordShape,
|
|
38
|
+
} from '@agoric/zoe/src/typeGuards.js';
|
|
39
|
+
|
|
30
40
|
import { makeInvitationsHelper } from './invitations.js';
|
|
31
|
-
import { makeOfferExecutor } from './offers.js';
|
|
32
41
|
import { shape } from './typeGuards.js';
|
|
33
42
|
import { objectMapStoragePath } from './utils.js';
|
|
43
|
+
import { prepareOfferWatcher, watchOfferOutcomes } from './offerWatcher.js';
|
|
34
44
|
|
|
35
45
|
const { Fail, quote: q } = assert;
|
|
36
46
|
|
|
@@ -42,17 +52,36 @@ const trace = makeTracer('SmrtWlt');
|
|
|
42
52
|
* @see {@link ../README.md}}
|
|
43
53
|
*/
|
|
44
54
|
|
|
55
|
+
/** @typedef {number | string} OfferId */
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {{
|
|
59
|
+
* id: OfferId,
|
|
60
|
+
* invitationSpec: import('./invitations').InvitationSpec,
|
|
61
|
+
* proposal: Proposal,
|
|
62
|
+
* offerArgs?: unknown
|
|
63
|
+
* }} OfferSpec
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {{
|
|
68
|
+
* logger: {info: (...args: any[]) => void, error: (...args: any[]) => void},
|
|
69
|
+
* makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher,
|
|
70
|
+
* invitationFromSpec: ERef<Invitation>,
|
|
71
|
+
* }} ExecutorPowers
|
|
72
|
+
*/
|
|
73
|
+
|
|
45
74
|
/**
|
|
46
75
|
* @typedef {{
|
|
47
76
|
* method: 'executeOffer'
|
|
48
|
-
* offer:
|
|
77
|
+
* offer: OfferSpec,
|
|
49
78
|
* }} ExecuteOfferAction
|
|
50
79
|
*/
|
|
51
80
|
|
|
52
81
|
/**
|
|
53
82
|
* @typedef {{
|
|
54
83
|
* method: 'tryExitOffer'
|
|
55
|
-
* offerId:
|
|
84
|
+
* offerId: OfferId,
|
|
56
85
|
* }} TryExitOfferAction
|
|
57
86
|
*/
|
|
58
87
|
|
|
@@ -83,7 +112,7 @@ const trace = makeTracer('SmrtWlt');
|
|
|
83
112
|
* purses: Array<{brand: Brand, balance: Amount}>,
|
|
84
113
|
* offerToUsedInvitation: Array<[ offerId: string, usedInvitation: Amount ]>,
|
|
85
114
|
* offerToPublicSubscriberPaths: Array<[ offerId: string, publicTopics: { [subscriberName: string]: string } ]>,
|
|
86
|
-
* liveOffers: Array<[
|
|
115
|
+
* liveOffers: Array<[OfferId, import('./offers.js').OfferStatus]>,
|
|
87
116
|
* }} CurrentWalletRecord
|
|
88
117
|
*/
|
|
89
118
|
|
|
@@ -108,18 +137,15 @@ const trace = makeTracer('SmrtWlt');
|
|
|
108
137
|
* brand: Brand,
|
|
109
138
|
* displayInfo: DisplayInfo,
|
|
110
139
|
* issuer: Issuer,
|
|
111
|
-
* petname: import('./types').Petname
|
|
140
|
+
* petname: import('./types.js').Petname
|
|
112
141
|
* }} BrandDescriptor
|
|
113
142
|
* For use by clients to describe brands to users. Includes `displayInfo` to save a remote call.
|
|
114
143
|
*/
|
|
115
144
|
|
|
116
|
-
// imports
|
|
117
|
-
/** @typedef {import('./types').RemotePurse} RemotePurse */
|
|
118
|
-
|
|
119
145
|
/**
|
|
120
146
|
* @typedef {{
|
|
121
147
|
* address: string,
|
|
122
|
-
* bank: ERef<import('@agoric/vats/src/vat-bank').Bank>,
|
|
148
|
+
* bank: ERef<import('@agoric/vats/src/vat-bank.js').Bank>,
|
|
123
149
|
* currentStorageNode: StorageNode,
|
|
124
150
|
* invitationPurse: Purse<'set'>,
|
|
125
151
|
* walletStorageNode: StorageNode,
|
|
@@ -134,6 +160,7 @@ const trace = makeTracer('SmrtWlt');
|
|
|
134
160
|
* invitationDisplayInfo: DisplayInfo,
|
|
135
161
|
* publicMarshaller: Marshaller,
|
|
136
162
|
* zoe: ERef<ZoeService>,
|
|
163
|
+
* secretWalletFactoryKey: any,
|
|
137
164
|
* }} SharedParams
|
|
138
165
|
*
|
|
139
166
|
* @typedef {ImmutableState & MutableState} State
|
|
@@ -144,14 +171,15 @@ const trace = makeTracer('SmrtWlt');
|
|
|
144
171
|
*
|
|
145
172
|
* @typedef {Readonly<UniqueParams & {
|
|
146
173
|
* paymentQueues: MapStore<Brand, Array<Payment>>,
|
|
147
|
-
* offerToInvitationMakers: MapStore<string, import('./types').
|
|
174
|
+
* offerToInvitationMakers: MapStore<string, import('./types').RemoteInvitationMakers>,
|
|
148
175
|
* offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>,
|
|
149
176
|
* offerToUsedInvitation: MapStore<string, Amount>,
|
|
150
|
-
* purseBalances: MapStore<
|
|
177
|
+
* purseBalances: MapStore<Purse, Amount>,
|
|
151
178
|
* updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>,
|
|
152
179
|
* currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<CurrentWalletRecord>,
|
|
153
|
-
* liveOffers: MapStore<
|
|
154
|
-
* liveOfferSeats:
|
|
180
|
+
* liveOffers: MapStore<OfferId, import('./offers.js').OfferStatus>,
|
|
181
|
+
* liveOfferSeats: MapStore<OfferId, UserSeat<unknown>>,
|
|
182
|
+
* liveOfferPayments: MapStore<OfferId, MapStore<Brand, Payment>>,
|
|
155
183
|
* }>} ImmutableState
|
|
156
184
|
*
|
|
157
185
|
* @typedef {BrandDescriptor & { purse: Purse }} PurseRecord
|
|
@@ -165,7 +193,7 @@ const trace = makeTracer('SmrtWlt');
|
|
|
165
193
|
* TODO: consider moving to nameHub.js?
|
|
166
194
|
*
|
|
167
195
|
* @param {unknown} target - passable Key
|
|
168
|
-
* @param {ERef<NameHub>} nameHub
|
|
196
|
+
* @param {ERef<import('@agoric/vats').NameHub>} nameHub
|
|
169
197
|
*/
|
|
170
198
|
const namesOf = async (target, nameHub) => {
|
|
171
199
|
const entries = await E(nameHub).entries();
|
|
@@ -223,6 +251,12 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
223
251
|
invitationDisplayInfo: DisplayInfoShape,
|
|
224
252
|
publicMarshaller: M.remotable('Marshaller'),
|
|
225
253
|
zoe: M.eref(M.remotable('ZoeService')),
|
|
254
|
+
|
|
255
|
+
// known only to smartWallets and walletFactory, this allows the
|
|
256
|
+
// walletFactory to invoke functions on the self facet that no one else
|
|
257
|
+
// can. Used to protect the upgrade-to-incarnation 2 repair. This can be
|
|
258
|
+
// dropped once the repair has taken place.
|
|
259
|
+
secretWalletFactoryKey: M.any(),
|
|
226
260
|
}),
|
|
227
261
|
);
|
|
228
262
|
|
|
@@ -237,8 +271,62 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
237
271
|
return store;
|
|
238
272
|
});
|
|
239
273
|
|
|
274
|
+
const makeOfferWatcher = prepareOfferWatcher(baggage);
|
|
275
|
+
|
|
276
|
+
const updateShape = {
|
|
277
|
+
value: AmountShape,
|
|
278
|
+
updateCount: M.bigint(),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const NotifierShape = M.remotable();
|
|
282
|
+
const amountWatcherGuard = M.interface('paymentWatcher', {
|
|
283
|
+
onFulfilled: M.call(updateShape, NotifierShape).returns(),
|
|
284
|
+
onRejected: M.call(M.any(), NotifierShape).returns(M.promise()),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const prepareAmountWatcher = () =>
|
|
288
|
+
prepareExoClass(
|
|
289
|
+
baggage,
|
|
290
|
+
'AmountWatcher',
|
|
291
|
+
amountWatcherGuard,
|
|
292
|
+
/**
|
|
293
|
+
* @param {Purse} purse
|
|
294
|
+
* @param {ReturnType<makeWalletWithResolvedStorageNodes>['helper']} helper
|
|
295
|
+
*/
|
|
296
|
+
(purse, helper) => ({ purse, helper }),
|
|
297
|
+
{
|
|
298
|
+
/**
|
|
299
|
+
* @param {{ value: Amount, updateCount: bigint | undefined }} updateRecord
|
|
300
|
+
* @param { Notifier<Amount> } notifier
|
|
301
|
+
* @returns {void}
|
|
302
|
+
*/
|
|
303
|
+
onFulfilled(updateRecord, notifier) {
|
|
304
|
+
const { helper, purse } = this.state;
|
|
305
|
+
helper.updateBalance(purse, updateRecord.value);
|
|
306
|
+
helper.watchNextBalance(
|
|
307
|
+
this.self,
|
|
308
|
+
notifier,
|
|
309
|
+
updateRecord.updateCount,
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
/**
|
|
313
|
+
* @param {unknown} err
|
|
314
|
+
* @returns {Promise<void>}
|
|
315
|
+
*/
|
|
316
|
+
onRejected(err) {
|
|
317
|
+
const { helper, purse } = this.state;
|
|
318
|
+
if (isUpgradeDisconnection(err)) {
|
|
319
|
+
return helper.watchPurse(purse); // retry
|
|
320
|
+
}
|
|
321
|
+
helper.logWalletError(`failed amount observer`, err);
|
|
322
|
+
throw err;
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const makeAmountWatcher = prepareAmountWatcher();
|
|
328
|
+
|
|
240
329
|
/**
|
|
241
|
-
*
|
|
242
330
|
* @param {UniqueParams} unique
|
|
243
331
|
* @returns {State}
|
|
244
332
|
*/
|
|
@@ -302,6 +390,9 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
302
390
|
liveOfferSeats: makeScalarBigMapStore('live offer seats', {
|
|
303
391
|
durable: true,
|
|
304
392
|
}),
|
|
393
|
+
liveOfferPayments: makeScalarBigMapStore('live offer payments', {
|
|
394
|
+
durable: true,
|
|
395
|
+
}),
|
|
305
396
|
};
|
|
306
397
|
|
|
307
398
|
return {
|
|
@@ -320,10 +411,34 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
320
411
|
.returns(M.promise()),
|
|
321
412
|
publishCurrentState: M.call().returns(),
|
|
322
413
|
watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()),
|
|
414
|
+
watchNextBalance: M.call(M.any(), NotifierShape, M.bigint()).returns(),
|
|
415
|
+
repairUnwatchedSeats: M.call().returns(M.promise()),
|
|
416
|
+
repairUnwatchedPurses: M.call().returns(M.promise()),
|
|
417
|
+
updateStatus: M.call(M.any()).returns(),
|
|
418
|
+
addContinuingOffer: M.call(
|
|
419
|
+
M.or(M.number(), M.string()),
|
|
420
|
+
AmountShape,
|
|
421
|
+
M.remotable('InvitationMaker'),
|
|
422
|
+
M.or(M.record(), M.undefined()),
|
|
423
|
+
).returns(M.promise()),
|
|
424
|
+
purseForBrand: M.call(BrandShape).returns(M.promise()),
|
|
425
|
+
logWalletInfo: M.call().rest(M.arrayOf(M.any())).returns(),
|
|
426
|
+
logWalletError: M.call().rest(M.arrayOf(M.any())).returns(),
|
|
427
|
+
getLiveOfferPayments: M.call().returns(M.remotable('mapStore')),
|
|
323
428
|
}),
|
|
429
|
+
|
|
324
430
|
deposit: M.interface('depositFacetI', {
|
|
325
431
|
receive: M.callWhen(M.await(M.eref(PaymentShape))).returns(AmountShape),
|
|
326
432
|
}),
|
|
433
|
+
payments: M.interface('payments support', {
|
|
434
|
+
withdrawGive: M.call(
|
|
435
|
+
AmountKeywordRecordShape,
|
|
436
|
+
M.or(M.number(), M.string()),
|
|
437
|
+
).returns(PaymentPKeywordRecordShape),
|
|
438
|
+
tryReclaimingWithdrawnPayments: M.call(
|
|
439
|
+
M.or(M.number(), M.string()),
|
|
440
|
+
).returns(M.promise()),
|
|
441
|
+
}),
|
|
327
442
|
offers: M.interface('offers facet', {
|
|
328
443
|
executeOffer: M.call(shape.OfferSpec).returns(M.promise()),
|
|
329
444
|
tryExitOffer: M.call(M.scalar()).returns(M.promise()),
|
|
@@ -337,6 +452,7 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
337
452
|
getCurrentSubscriber: M.call().returns(SubscriberShape),
|
|
338
453
|
getUpdatesSubscriber: M.call().returns(SubscriberShape),
|
|
339
454
|
getPublicTopics: M.call().returns(TopicsRecordShape),
|
|
455
|
+
repairWalletForIncarnation2: M.call(M.any()).returns(),
|
|
340
456
|
}),
|
|
341
457
|
};
|
|
342
458
|
|
|
@@ -360,6 +476,7 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
360
476
|
* @type {(id: string) => void}
|
|
361
477
|
*/
|
|
362
478
|
assertUniqueOfferId(id) {
|
|
479
|
+
const { facets } = this;
|
|
363
480
|
const {
|
|
364
481
|
liveOffers,
|
|
365
482
|
liveOfferSeats,
|
|
@@ -370,13 +487,14 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
370
487
|
const used =
|
|
371
488
|
liveOffers.has(id) ||
|
|
372
489
|
liveOfferSeats.has(id) ||
|
|
490
|
+
facets.helper.getLiveOfferPayments().has(id) ||
|
|
373
491
|
offerToInvitationMakers.has(id) ||
|
|
374
492
|
offerToPublicSubscriberPaths.has(id) ||
|
|
375
493
|
offerToUsedInvitation.has(id);
|
|
376
494
|
!used || Fail`cannot re-use offer id ${id}`;
|
|
377
495
|
},
|
|
378
496
|
/**
|
|
379
|
-
* @param {
|
|
497
|
+
* @param {Purse} purse
|
|
380
498
|
* @param {Amount<any>} balance
|
|
381
499
|
*/
|
|
382
500
|
updateBalance(purse, balance) {
|
|
@@ -415,44 +533,40 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
415
533
|
});
|
|
416
534
|
},
|
|
417
535
|
|
|
418
|
-
/** @type {(purse: ERef<
|
|
536
|
+
/** @type {(purse: ERef<Purse>) => Promise<void>} */
|
|
419
537
|
async watchPurse(purseRef) {
|
|
420
|
-
const {
|
|
538
|
+
const { helper } = this.facets;
|
|
539
|
+
|
|
540
|
+
// This would seem to fit the observeNotifier() pattern,
|
|
541
|
+
// but purse notifiers are not necessarily durable.
|
|
542
|
+
// If there is an error due to upgrade, retry watchPurse().
|
|
421
543
|
|
|
422
544
|
const purse = await purseRef; // promises don't fit in durable storage
|
|
545
|
+
const handler = makeAmountWatcher(purse, helper);
|
|
423
546
|
|
|
424
|
-
const { helper } = this.facets;
|
|
425
547
|
// publish purse's balance and changes
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
436
|
-
void observeNotifier(E(purse).getCurrentAmountNotifier(), {
|
|
437
|
-
updateState(balance) {
|
|
438
|
-
helper.updateBalance(purse, balance);
|
|
439
|
-
},
|
|
440
|
-
fail(reason) {
|
|
441
|
-
console.error(address, `failed updateState observer`, reason);
|
|
442
|
-
},
|
|
443
|
-
});
|
|
548
|
+
const notifier = await E(purse).getCurrentAmountNotifier();
|
|
549
|
+
const startP = E(notifier).getUpdateSince(undefined);
|
|
550
|
+
// @ts-expect-error import watchPromise's type is unknown
|
|
551
|
+
watchPromise(startP, handler, notifier);
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
watchNextBalance(handler, notifier, updateCount) {
|
|
555
|
+
const nextP = E(notifier).getUpdateSince(updateCount);
|
|
556
|
+
// @ts-expect-error import watchPromise's type is unknown
|
|
557
|
+
watchPromise(nextP, handler, notifier);
|
|
444
558
|
},
|
|
445
559
|
|
|
446
560
|
/**
|
|
447
561
|
* Provide a purse given a NameHub of issuers and their
|
|
448
562
|
* brands.
|
|
449
563
|
*
|
|
450
|
-
* We
|
|
564
|
+
* We currently support only one NameHub, agoricNames, and
|
|
451
565
|
* hence one purse per brand. But we store an array of them
|
|
452
566
|
* to facilitate a transition to decentralized introductions.
|
|
453
567
|
*
|
|
454
568
|
* @param {Brand} brand
|
|
455
|
-
* @param {ERef<NameHub>} known - namehub with brand, issuer branches
|
|
569
|
+
* @param {ERef<import('@agoric/vats').NameHub>} known - namehub with brand, issuer branches
|
|
456
570
|
* @returns {Promise<Purse | undefined>} undefined if brand is not known
|
|
457
571
|
*/
|
|
458
572
|
async getPurseIfKnownBrand(brand, known) {
|
|
@@ -499,6 +613,175 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
499
613
|
void helper.watchPurse(purse);
|
|
500
614
|
return purse;
|
|
501
615
|
},
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* see https://github.com/Agoric/agoric-sdk/issues/8445 and
|
|
619
|
+
* https://github.com/Agoric/agoric-sdk/issues/8286. As originally
|
|
620
|
+
* released, the smartWallet didn't durably monitor the promises for the
|
|
621
|
+
* outcomes of offers, and would have dropped them on upgrade of Zoe or
|
|
622
|
+
* the smartWallet itself. Using watchedPromises, (see offerWatcher.js)
|
|
623
|
+
* we've addressed the problem for new offers. This function will
|
|
624
|
+
* backfill the solution for offers that were outstanding before the
|
|
625
|
+
* transition to incarnation 2 of the smartWallet.
|
|
626
|
+
*/
|
|
627
|
+
async repairUnwatchedSeats() {
|
|
628
|
+
const { state, facets } = this;
|
|
629
|
+
const { address, invitationPurse, liveOffers, liveOfferSeats } =
|
|
630
|
+
state;
|
|
631
|
+
const { zoe, agoricNames, invitationBrand, invitationIssuer } =
|
|
632
|
+
shared;
|
|
633
|
+
|
|
634
|
+
const invitationFromSpec = makeInvitationsHelper(
|
|
635
|
+
zoe,
|
|
636
|
+
agoricNames,
|
|
637
|
+
invitationBrand,
|
|
638
|
+
invitationPurse,
|
|
639
|
+
state.offerToInvitationMakers.get,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
const watcherPromises = [];
|
|
643
|
+
for (const seatId of liveOfferSeats.keys()) {
|
|
644
|
+
facets.helper.logWalletInfo(`repairing ${seatId}`);
|
|
645
|
+
const offerSpec = liveOffers.get(seatId);
|
|
646
|
+
const seat = liveOfferSeats.get(seatId);
|
|
647
|
+
|
|
648
|
+
const invitation = invitationFromSpec(offerSpec.invitationSpec);
|
|
649
|
+
watcherPromises.push(
|
|
650
|
+
E.when(
|
|
651
|
+
E(invitationIssuer).getAmountOf(invitation),
|
|
652
|
+
invitationAmount => {
|
|
653
|
+
const watcher = makeOfferWatcher(
|
|
654
|
+
facets.helper,
|
|
655
|
+
facets.deposit,
|
|
656
|
+
offerSpec,
|
|
657
|
+
address,
|
|
658
|
+
invitationAmount,
|
|
659
|
+
seat,
|
|
660
|
+
);
|
|
661
|
+
return watchOfferOutcomes(watcher, seat);
|
|
662
|
+
},
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
trace(`Repaired seat ${seatId} for wallet ${address}`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
await Promise.all(watcherPromises);
|
|
669
|
+
},
|
|
670
|
+
async repairUnwatchedPurses() {
|
|
671
|
+
const { state, facets } = this;
|
|
672
|
+
const { helper, self } = facets;
|
|
673
|
+
const { invitationPurse, address } = state;
|
|
674
|
+
|
|
675
|
+
const brandToPurses = getBrandToPurses(walletPurses, self);
|
|
676
|
+
trace(`Found ${brandToPurses.values()} purse(s) for ${address}`);
|
|
677
|
+
for (const purses of brandToPurses.values()) {
|
|
678
|
+
for (const record of purses) {
|
|
679
|
+
void helper.watchPurse(record.purse);
|
|
680
|
+
trace(`Repaired purse ${record.petname} of ${address}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
void helper.watchPurse(invitationPurse);
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
/** @param {import('./offers.js').OfferStatus} offerStatus */
|
|
688
|
+
updateStatus(offerStatus) {
|
|
689
|
+
const { state, facets } = this;
|
|
690
|
+
facets.helper.logWalletInfo('offerStatus', offerStatus);
|
|
691
|
+
|
|
692
|
+
void state.updateRecorderKit.recorder.write({
|
|
693
|
+
updated: 'offerStatus',
|
|
694
|
+
status: offerStatus,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
if ('numWantsSatisfied' in offerStatus) {
|
|
698
|
+
if (state.liveOfferSeats.has(offerStatus.id)) {
|
|
699
|
+
state.liveOfferSeats.delete(offerStatus.id);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
|
|
703
|
+
facets.helper.getLiveOfferPayments().delete(offerStatus.id);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (state.liveOffers.has(offerStatus.id)) {
|
|
707
|
+
state.liveOffers.delete(offerStatus.id);
|
|
708
|
+
// This might get skipped in subsequent passes, since we .delete()
|
|
709
|
+
// the first time through
|
|
710
|
+
facets.helper.publishCurrentState();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
async addContinuingOffer(
|
|
715
|
+
offerId,
|
|
716
|
+
invitationAmount,
|
|
717
|
+
invitationMakers,
|
|
718
|
+
publicSubscribers,
|
|
719
|
+
) {
|
|
720
|
+
const { state, facets } = this;
|
|
721
|
+
|
|
722
|
+
state.offerToUsedInvitation.init(offerId, invitationAmount);
|
|
723
|
+
state.offerToInvitationMakers.init(offerId, invitationMakers);
|
|
724
|
+
const pathMap = await objectMapStoragePath(publicSubscribers);
|
|
725
|
+
if (pathMap) {
|
|
726
|
+
facets.helper.logWalletInfo('recording pathMap', pathMap);
|
|
727
|
+
state.offerToPublicSubscriberPaths.init(offerId, pathMap);
|
|
728
|
+
}
|
|
729
|
+
facets.helper.publishCurrentState();
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* @param {Brand} brand
|
|
734
|
+
* @returns {Promise<Purse>}
|
|
735
|
+
*/
|
|
736
|
+
async purseForBrand(brand) {
|
|
737
|
+
const { state, facets } = this;
|
|
738
|
+
const { registry, invitationBrand } = shared;
|
|
739
|
+
|
|
740
|
+
if (registry.has(brand)) {
|
|
741
|
+
// @ts-expect-error virtual purse
|
|
742
|
+
return E(state.bank).getPurse(brand);
|
|
743
|
+
} else if (invitationBrand === brand) {
|
|
744
|
+
return state.invitationPurse;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const purse = await facets.helper.getPurseIfKnownBrand(
|
|
748
|
+
brand,
|
|
749
|
+
shared.agoricNames,
|
|
750
|
+
);
|
|
751
|
+
if (purse) {
|
|
752
|
+
return purse;
|
|
753
|
+
}
|
|
754
|
+
throw Fail`cannot find/make purse for ${brand}`;
|
|
755
|
+
},
|
|
756
|
+
logWalletInfo(...args) {
|
|
757
|
+
const { state } = this;
|
|
758
|
+
console.info('wallet', state.address, ...args);
|
|
759
|
+
},
|
|
760
|
+
logWalletError(...args) {
|
|
761
|
+
const { state } = this;
|
|
762
|
+
console.error('wallet', state.address, ...args);
|
|
763
|
+
},
|
|
764
|
+
// In new SmartWallets, this is part of state, but we can't add fields
|
|
765
|
+
// to instance state for older SmartWallets, so put it in baggage.
|
|
766
|
+
getLiveOfferPayments() {
|
|
767
|
+
const { state } = this;
|
|
768
|
+
|
|
769
|
+
if (state.liveOfferPayments) {
|
|
770
|
+
return state.liveOfferPayments;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// This will only happen for legacy wallets, before WF incarnation 2
|
|
774
|
+
if (!baggage.has(state.address)) {
|
|
775
|
+
trace(`getLiveOfferPayments adding store for ${state.address}`);
|
|
776
|
+
baggage.init(
|
|
777
|
+
state.address,
|
|
778
|
+
makeScalarBigMapStore('live offer payments', {
|
|
779
|
+
durable: true,
|
|
780
|
+
}),
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return baggage.get(state.address);
|
|
784
|
+
},
|
|
502
785
|
},
|
|
503
786
|
/**
|
|
504
787
|
* Similar to {DepositFacet} but async because it has to look up the purse.
|
|
@@ -514,9 +797,13 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
514
797
|
* @throws if there's not yet a purse, though the payment is held to try again when there is
|
|
515
798
|
*/
|
|
516
799
|
async receive(payment) {
|
|
517
|
-
const {
|
|
518
|
-
|
|
800
|
+
const {
|
|
801
|
+
state,
|
|
802
|
+
facets: { helper },
|
|
803
|
+
} = this;
|
|
804
|
+
const { paymentQueues: queues, bank, invitationPurse } = state;
|
|
519
805
|
const { registry, invitationBrand } = shared;
|
|
806
|
+
|
|
520
807
|
const brand = await E(payment).getAllegedBrand();
|
|
521
808
|
|
|
522
809
|
// When there is a purse deposit into it
|
|
@@ -542,119 +829,187 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
542
829
|
throw Fail`cannot deposit payment with brand ${brand}: no purse`;
|
|
543
830
|
},
|
|
544
831
|
},
|
|
832
|
+
|
|
833
|
+
payments: {
|
|
834
|
+
/**
|
|
835
|
+
* @param {AmountKeywordRecord} give
|
|
836
|
+
* @param {OfferId} offerId
|
|
837
|
+
* @returns {PaymentPKeywordRecord}
|
|
838
|
+
*/
|
|
839
|
+
withdrawGive(give, offerId) {
|
|
840
|
+
const { facets } = this;
|
|
841
|
+
|
|
842
|
+
/** @type {MapStore<Brand, Payment>} */
|
|
843
|
+
const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
|
|
844
|
+
durable: true,
|
|
845
|
+
});
|
|
846
|
+
facets.helper
|
|
847
|
+
.getLiveOfferPayments()
|
|
848
|
+
.init(offerId, brandPaymentRecord);
|
|
849
|
+
|
|
850
|
+
// Add each payment to liveOfferPayments as it is withdrawn. If
|
|
851
|
+
// there's an error partway through, we can recover the withdrawals.
|
|
852
|
+
return objectMap(give, amount => {
|
|
853
|
+
/** @type {Promise<Purse>} */
|
|
854
|
+
const purseP = facets.helper.purseForBrand(amount.brand);
|
|
855
|
+
const paymentP = E(purseP).withdraw(amount);
|
|
856
|
+
void E.when(
|
|
857
|
+
paymentP,
|
|
858
|
+
payment => brandPaymentRecord.init(amount.brand, payment),
|
|
859
|
+
e => {
|
|
860
|
+
// recovery will be handled by tryReclaimingWithdrawnPayments()
|
|
861
|
+
facets.helper.logWalletInfo(
|
|
862
|
+
`⚠️ Payment withdrawal failed.`,
|
|
863
|
+
offerId,
|
|
864
|
+
e,
|
|
865
|
+
);
|
|
866
|
+
},
|
|
867
|
+
);
|
|
868
|
+
return paymentP;
|
|
869
|
+
});
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
async tryReclaimingWithdrawnPayments(offerId) {
|
|
873
|
+
const { facets } = this;
|
|
874
|
+
|
|
875
|
+
const liveOfferPayments = facets.helper.getLiveOfferPayments();
|
|
876
|
+
if (liveOfferPayments.has(offerId)) {
|
|
877
|
+
const brandPaymentRecord = liveOfferPayments.get(offerId);
|
|
878
|
+
if (!brandPaymentRecord) {
|
|
879
|
+
return Promise.resolve(undefined);
|
|
880
|
+
}
|
|
881
|
+
// Use allSettled to ensure we attempt all the deposits, regardless of
|
|
882
|
+
// individual rejections.
|
|
883
|
+
return Promise.allSettled(
|
|
884
|
+
Array.from(brandPaymentRecord.entries()).map(async ([b, p]) => {
|
|
885
|
+
// Wait for the withdrawal to complete. This protects against a
|
|
886
|
+
// race when updating paymentToPurse.
|
|
887
|
+
const purseP = facets.helper.purseForBrand(b);
|
|
888
|
+
|
|
889
|
+
// Now send it back to the purse.
|
|
890
|
+
return E(purseP).deposit(p);
|
|
891
|
+
}),
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
|
|
545
897
|
offers: {
|
|
546
898
|
/**
|
|
547
899
|
* Take an offer description provided in capData, augment it with payments and call zoe.offer()
|
|
548
900
|
*
|
|
549
|
-
* @param {
|
|
901
|
+
* @param {OfferSpec} offerSpec
|
|
550
902
|
* @returns {Promise<void>} after the offer has been both seated and exited by Zoe.
|
|
551
903
|
* @throws if any parts of the offer can be determined synchronously to be invalid
|
|
552
904
|
*/
|
|
553
905
|
async executeOffer(offerSpec) {
|
|
554
906
|
const { facets, state } = this;
|
|
555
|
-
const {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
invitationPurse,
|
|
559
|
-
offerToInvitationMakers,
|
|
560
|
-
offerToUsedInvitation,
|
|
561
|
-
offerToPublicSubscriberPaths,
|
|
562
|
-
updateRecorderKit,
|
|
563
|
-
} = this.state;
|
|
564
|
-
const { invitationBrand, zoe, invitationIssuer, registry } = shared;
|
|
907
|
+
const { address, invitationPurse } = state;
|
|
908
|
+
const { zoe, agoricNames } = shared;
|
|
909
|
+
const { invitationBrand, invitationIssuer } = shared;
|
|
565
910
|
|
|
566
911
|
facets.helper.assertUniqueOfferId(String(offerSpec.id));
|
|
567
912
|
|
|
568
|
-
|
|
569
|
-
info: (...args) => console.info('wallet', address, ...args),
|
|
570
|
-
error: (...args) => console.error('wallet', address, ...args),
|
|
571
|
-
};
|
|
913
|
+
await null;
|
|
572
914
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
offerToInvitationMakers.get,
|
|
584
|
-
),
|
|
585
|
-
/**
|
|
586
|
-
* @param {Brand} brand
|
|
587
|
-
* @returns {Promise<RemotePurse>}
|
|
588
|
-
*/
|
|
589
|
-
purseForBrand: async brand => {
|
|
590
|
-
const { helper } = facets;
|
|
591
|
-
if (registry.has(brand)) {
|
|
592
|
-
// @ts-expect-error RemotePurse cast
|
|
593
|
-
return E(bank).getPurse(brand);
|
|
594
|
-
} else if (invitationBrand === brand) {
|
|
595
|
-
// @ts-expect-error RemotePurse cast
|
|
596
|
-
return invitationPurse;
|
|
597
|
-
}
|
|
915
|
+
let seatRef;
|
|
916
|
+
let watcher;
|
|
917
|
+
try {
|
|
918
|
+
const invitationFromSpec = makeInvitationsHelper(
|
|
919
|
+
zoe,
|
|
920
|
+
agoricNames,
|
|
921
|
+
invitationBrand,
|
|
922
|
+
invitationPurse,
|
|
923
|
+
state.offerToInvitationMakers.get,
|
|
924
|
+
);
|
|
598
925
|
|
|
599
|
-
|
|
600
|
-
brand,
|
|
601
|
-
shared.agoricNames,
|
|
602
|
-
);
|
|
603
|
-
if (purse) {
|
|
604
|
-
return purse;
|
|
605
|
-
}
|
|
606
|
-
throw Fail`cannot find/make purse for ${brand}`;
|
|
607
|
-
},
|
|
608
|
-
logger,
|
|
609
|
-
},
|
|
610
|
-
onStatusChange: offerStatus => {
|
|
611
|
-
logger.info('offerStatus', offerStatus);
|
|
926
|
+
facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
|
|
612
927
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
status: offerStatus,
|
|
616
|
-
});
|
|
928
|
+
// 1. Prepare values and validate synchronously.
|
|
929
|
+
const { proposal } = offerSpec;
|
|
617
930
|
|
|
618
|
-
|
|
619
|
-
if (isSeatExited) {
|
|
620
|
-
if (state.liveOfferSeats.has(offerStatus.id)) {
|
|
621
|
-
state.liveOfferSeats.delete(offerStatus.id);
|
|
622
|
-
}
|
|
931
|
+
const invitation = invitationFromSpec(offerSpec.invitationSpec);
|
|
623
932
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
933
|
+
const [paymentKeywordRecord, invitationAmount] = await Promise.all([
|
|
934
|
+
proposal?.give &&
|
|
935
|
+
deeplyFulfilledObject(
|
|
936
|
+
facets.payments.withdrawGive(proposal.give, offerSpec.id),
|
|
937
|
+
),
|
|
938
|
+
E(invitationIssuer).getAmountOf(invitation),
|
|
939
|
+
]);
|
|
940
|
+
|
|
941
|
+
// 2. Begin executing offer
|
|
942
|
+
// No explicit signal to user that we reached here but if anything above
|
|
943
|
+
// failed they'd get an 'error' status update.
|
|
944
|
+
|
|
945
|
+
/** @type {UserSeat} */
|
|
946
|
+
seatRef = await E(zoe).offer(
|
|
947
|
+
invitation,
|
|
948
|
+
proposal,
|
|
949
|
+
paymentKeywordRecord,
|
|
950
|
+
offerSpec.offerArgs,
|
|
951
|
+
);
|
|
952
|
+
facets.helper.logWalletInfo(offerSpec.id, 'seated');
|
|
953
|
+
|
|
954
|
+
watcher = makeOfferWatcher(
|
|
955
|
+
facets.helper,
|
|
956
|
+
facets.deposit,
|
|
957
|
+
offerSpec,
|
|
958
|
+
address,
|
|
633
959
|
invitationAmount,
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
) => {
|
|
637
|
-
offerToUsedInvitation.init(offerId, invitationAmount);
|
|
638
|
-
offerToInvitationMakers.init(offerId, invitationMakers);
|
|
639
|
-
const pathMap = await objectMapStoragePath(publicSubscribers);
|
|
640
|
-
if (pathMap) {
|
|
641
|
-
logger.info('recording pathMap', pathMap);
|
|
642
|
-
offerToPublicSubscriberPaths.init(offerId, pathMap);
|
|
643
|
-
}
|
|
644
|
-
facets.helper.publishCurrentState();
|
|
645
|
-
},
|
|
646
|
-
});
|
|
960
|
+
seatRef,
|
|
961
|
+
);
|
|
647
962
|
|
|
648
|
-
return executor.executeOffer(offerSpec, seatRef => {
|
|
649
963
|
state.liveOffers.init(offerSpec.id, offerSpec);
|
|
650
|
-
facets.helper.publishCurrentState();
|
|
651
964
|
state.liveOfferSeats.init(offerSpec.id, seatRef);
|
|
652
|
-
|
|
965
|
+
|
|
966
|
+
// publish the live offers
|
|
967
|
+
facets.helper.publishCurrentState();
|
|
968
|
+
|
|
969
|
+
// await so that any errors are caught and handled below
|
|
970
|
+
await watchOfferOutcomes(watcher, seatRef);
|
|
971
|
+
} catch (err) {
|
|
972
|
+
facets.helper.logWalletError('OFFER ERROR:', err);
|
|
973
|
+
|
|
974
|
+
// Notify the user
|
|
975
|
+
if (err.upgradeMessage === 'vat upgraded') {
|
|
976
|
+
// The offer watchers will reconnect. Don't reclaim or exit
|
|
977
|
+
return;
|
|
978
|
+
} else if (watcher) {
|
|
979
|
+
watcher.helper.updateStatus({ error: err.toString() });
|
|
980
|
+
} else {
|
|
981
|
+
facets.helper.updateStatus({
|
|
982
|
+
error: err.toString(),
|
|
983
|
+
...offerSpec,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (offerSpec?.proposal?.give) {
|
|
988
|
+
facets.payments
|
|
989
|
+
.tryReclaimingWithdrawnPayments(offerSpec.id)
|
|
990
|
+
.catch(e =>
|
|
991
|
+
facets.helper.logWalletError(
|
|
992
|
+
'recovery failed reclaiming payments',
|
|
993
|
+
e,
|
|
994
|
+
),
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (seatRef) {
|
|
999
|
+
void E.when(E(seatRef).hasExited(), hasExited => {
|
|
1000
|
+
if (!hasExited) {
|
|
1001
|
+
void E(seatRef).tryExit();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
throw err;
|
|
1007
|
+
}
|
|
653
1008
|
},
|
|
654
1009
|
/**
|
|
655
1010
|
* Take an offer's id, look up its seat, try to exit.
|
|
656
1011
|
*
|
|
657
|
-
* @param {
|
|
1012
|
+
* @param {OfferId} offerId
|
|
658
1013
|
* @returns {Promise<void>}
|
|
659
1014
|
* @throws if the seat can't be found or E(seatRef).tryExit() fails.
|
|
660
1015
|
*/
|
|
@@ -672,14 +1027,14 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
672
1027
|
* @returns {Promise<void>}
|
|
673
1028
|
*/
|
|
674
1029
|
handleBridgeAction(actionCapData, canSpend = false) {
|
|
1030
|
+
const { facets } = this;
|
|
1031
|
+
const { offers } = facets;
|
|
675
1032
|
const { publicMarshaller } = shared;
|
|
676
1033
|
|
|
677
|
-
const { offers } = this.facets;
|
|
678
|
-
|
|
679
1034
|
/** @param {Error} err */
|
|
680
1035
|
const recordError = err => {
|
|
681
|
-
const {
|
|
682
|
-
|
|
1036
|
+
const { updateRecorderKit } = this.state;
|
|
1037
|
+
facets.helper.logWalletError('handleBridgeAction error:', err);
|
|
683
1038
|
void updateRecorderKit.recorder.write({
|
|
684
1039
|
updated: 'walletAction',
|
|
685
1040
|
status: { error: err.message },
|
|
@@ -724,14 +1079,18 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
724
1079
|
},
|
|
725
1080
|
/** @deprecated use getPublicTopics */
|
|
726
1081
|
getCurrentSubscriber() {
|
|
727
|
-
|
|
1082
|
+
const { state } = this;
|
|
1083
|
+
return state.currentRecorderKit.subscriber;
|
|
728
1084
|
},
|
|
729
1085
|
/** @deprecated use getPublicTopics */
|
|
730
1086
|
getUpdatesSubscriber() {
|
|
731
|
-
|
|
1087
|
+
const { state } = this;
|
|
1088
|
+
return state.updateRecorderKit.subscriber;
|
|
732
1089
|
},
|
|
733
1090
|
getPublicTopics() {
|
|
734
|
-
const {
|
|
1091
|
+
const { state } = this;
|
|
1092
|
+
const { currentRecorderKit, updateRecorderKit } = state;
|
|
1093
|
+
|
|
735
1094
|
return harden({
|
|
736
1095
|
current: {
|
|
737
1096
|
description: 'Current state of wallet',
|
|
@@ -745,6 +1104,23 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
745
1104
|
},
|
|
746
1105
|
});
|
|
747
1106
|
},
|
|
1107
|
+
/**
|
|
1108
|
+
* one-time use function. Remove this and repairUnwatchedSeats once the
|
|
1109
|
+
* repair has taken place.
|
|
1110
|
+
*
|
|
1111
|
+
* @param {object} key
|
|
1112
|
+
*/
|
|
1113
|
+
repairWalletForIncarnation2(key) {
|
|
1114
|
+
const { state, facets } = this;
|
|
1115
|
+
|
|
1116
|
+
if (key !== shared.secretWalletFactoryKey) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
void facets.helper.repairUnwatchedSeats();
|
|
1121
|
+
void facets.helper.repairUnwatchedPurses();
|
|
1122
|
+
trace(`repaired wallet ${state.address}`);
|
|
1123
|
+
},
|
|
748
1124
|
},
|
|
749
1125
|
},
|
|
750
1126
|
{
|
|
@@ -752,7 +1128,6 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
752
1128
|
const { invitationPurse } = state;
|
|
753
1129
|
const { helper } = facets;
|
|
754
1130
|
|
|
755
|
-
// @ts-expect-error RemotePurse cast
|
|
756
1131
|
void helper.watchPurse(invitationPurse);
|
|
757
1132
|
},
|
|
758
1133
|
},
|