@agoric/smart-wallet 0.5.4-mainnet1B-dev-b0c1f78.0 → 0.5.4-orchestration-dev-096c4e8.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.
@@ -1,5 +1,4 @@
1
- // backported types are out of sync
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 { StorageNodeShape, makeTracer } from '@agoric/internal';
12
- import { observeNotifier } from '@agoric/notifier';
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 { E } from '@endo/far';
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: import('./offers.js').OfferSpec,
77
+ * offer: OfferSpec,
49
78
  * }} ExecuteOfferAction
50
79
  */
51
80
 
52
81
  /**
53
82
  * @typedef {{
54
83
  * method: 'tryExitOffer'
55
- * offerId: import('./offers.js').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<[import('./offers.js').OfferId, import('./offers.js').OfferStatus]>,
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').InvitationMakers>,
174
+ * offerToInvitationMakers: MapStore<string, import('./types.js').InvitationMakers>,
148
175
  * offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>,
149
- * offerToUsedInvitation: MapStore<string, Amount>,
150
- * purseBalances: MapStore<RemotePurse, Amount>,
176
+ * offerToUsedInvitation: MapStore<string, Amount<'set'>>,
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<import('./offers.js').OfferId, import('./offers.js').OfferStatus>,
154
- * liveOfferSeats: WeakMapStore<import('./offers.js').OfferId, UserSeat<unknown>>,
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,9 +452,11 @@ 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
 
459
+ // TODO move to top level so its type can be exported
343
460
  /**
344
461
  * Make the durable object to return, but taking some parameters that are awaited by a wrapping function.
345
462
  * This is necessary because the class kit construction helpers, `initState` and `finish` run synchronously
@@ -360,6 +477,7 @@ export const prepareSmartWallet = (baggage, shared) => {
360
477
  * @type {(id: string) => void}
361
478
  */
362
479
  assertUniqueOfferId(id) {
480
+ const { facets } = this;
363
481
  const {
364
482
  liveOffers,
365
483
  liveOfferSeats,
@@ -370,13 +488,14 @@ export const prepareSmartWallet = (baggage, shared) => {
370
488
  const used =
371
489
  liveOffers.has(id) ||
372
490
  liveOfferSeats.has(id) ||
491
+ facets.helper.getLiveOfferPayments().has(id) ||
373
492
  offerToInvitationMakers.has(id) ||
374
493
  offerToPublicSubscriberPaths.has(id) ||
375
494
  offerToUsedInvitation.has(id);
376
495
  !used || Fail`cannot re-use offer id ${id}`;
377
496
  },
378
497
  /**
379
- * @param {RemotePurse} purse
498
+ * @param {Purse} purse
380
499
  * @param {Amount<any>} balance
381
500
  */
382
501
  updateBalance(purse, balance) {
@@ -415,44 +534,38 @@ export const prepareSmartWallet = (baggage, shared) => {
415
534
  });
416
535
  },
417
536
 
418
- /** @type {(purse: ERef<RemotePurse>) => Promise<void>} */
537
+ /** @type {(purse: ERef<Purse>) => Promise<void>} */
419
538
  async watchPurse(purseRef) {
420
- const { address } = this.state;
539
+ const { helper } = this.facets;
540
+
541
+ // This would seem to fit the observeNotifier() pattern,
542
+ // but purse notifiers are not necessarily durable.
543
+ // If there is an error due to upgrade, retry watchPurse().
421
544
 
422
545
  const purse = await purseRef; // promises don't fit in durable storage
546
+ const handler = makeAmountWatcher(purse, helper);
423
547
 
424
- const { helper } = this.facets;
425
548
  // publish purse's balance and changes
426
- void E.when(
427
- E(purse).getCurrentAmount(),
428
- balance => helper.updateBalance(purse, balance),
429
- err =>
430
- console.error(
431
- address,
432
- 'initial purse balance publish failed',
433
- err,
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
- });
549
+ const notifier = await E(purse).getCurrentAmountNotifier();
550
+ const startP = E(notifier).getUpdateSince(undefined);
551
+ watchPromise(startP, handler, notifier);
552
+ },
553
+
554
+ watchNextBalance(handler, notifier, updateCount) {
555
+ const nextP = E(notifier).getUpdateSince(updateCount);
556
+ watchPromise(nextP, handler, notifier);
444
557
  },
445
558
 
446
559
  /**
447
560
  * Provide a purse given a NameHub of issuers and their
448
561
  * brands.
449
562
  *
450
- * We current support only one NameHub, agoricNames, and
563
+ * We currently support only one NameHub, agoricNames, and
451
564
  * hence one purse per brand. But we store an array of them
452
565
  * to facilitate a transition to decentralized introductions.
453
566
  *
454
567
  * @param {Brand} brand
455
- * @param {ERef<NameHub>} known - namehub with brand, issuer branches
568
+ * @param {ERef<import('@agoric/vats').NameHub>} known - namehub with brand, issuer branches
456
569
  * @returns {Promise<Purse | undefined>} undefined if brand is not known
457
570
  */
458
571
  async getPurseIfKnownBrand(brand, known) {
@@ -499,6 +612,198 @@ export const prepareSmartWallet = (baggage, shared) => {
499
612
  void helper.watchPurse(purse);
500
613
  return purse;
501
614
  },
615
+
616
+ /**
617
+ * see https://github.com/Agoric/agoric-sdk/issues/8445 and
618
+ * https://github.com/Agoric/agoric-sdk/issues/8286. As originally
619
+ * released, the smartWallet didn't durably monitor the promises for the
620
+ * outcomes of offers, and would have dropped them on upgrade of Zoe or
621
+ * the smartWallet itself. Using watchedPromises, (see offerWatcher.js)
622
+ * we've addressed the problem for new offers. This function will
623
+ * backfill the solution for offers that were outstanding before the
624
+ * transition to incarnation 2 of the smartWallet.
625
+ */
626
+ async repairUnwatchedSeats() {
627
+ const { state, facets } = this;
628
+ const { address, invitationPurse, liveOffers, liveOfferSeats } =
629
+ state;
630
+ const { zoe, agoricNames, invitationBrand, invitationIssuer } =
631
+ shared;
632
+
633
+ const invitationFromSpec = makeInvitationsHelper(
634
+ zoe,
635
+ agoricNames,
636
+ invitationBrand,
637
+ invitationPurse,
638
+ state.offerToInvitationMakers.get,
639
+ );
640
+
641
+ const watcherPromises = [];
642
+ for (const seatId of liveOfferSeats.keys()) {
643
+ facets.helper.logWalletInfo(`repairing ${seatId}`);
644
+ const offerSpec = liveOffers.get(seatId);
645
+ const seat = liveOfferSeats.get(seatId);
646
+
647
+ const watchOutcome = (async () => {
648
+ await null;
649
+ let invitationAmount = state.offerToUsedInvitation.has(
650
+ // @ts-expect-error older type allowed number
651
+ offerSpec.id,
652
+ )
653
+ ? state.offerToUsedInvitation.get(
654
+ // @ts-expect-error older type allowed number
655
+ offerSpec.id,
656
+ )
657
+ : undefined;
658
+ if (invitationAmount) {
659
+ facets.helper.logWalletInfo(
660
+ 'recovered invitation amount for offer',
661
+ offerSpec.id,
662
+ );
663
+ } else {
664
+ facets.helper.logWalletInfo(
665
+ 'inferring invitation amount for offer',
666
+ offerSpec.id,
667
+ );
668
+ const tempInvitation = invitationFromSpec(
669
+ offerSpec.invitationSpec,
670
+ );
671
+ invitationAmount =
672
+ await E(invitationIssuer).getAmountOf(tempInvitation);
673
+ void E(invitationIssuer).burn(tempInvitation);
674
+ }
675
+
676
+ const watcher = makeOfferWatcher(
677
+ facets.helper,
678
+ facets.deposit,
679
+ offerSpec,
680
+ address,
681
+ invitationAmount,
682
+ seat,
683
+ );
684
+ return watchOfferOutcomes(watcher, seat);
685
+ })();
686
+ trace(`Repaired seat ${seatId} for wallet ${address}`);
687
+ watcherPromises.push(watchOutcome);
688
+ }
689
+
690
+ await Promise.all(watcherPromises);
691
+ },
692
+ async repairUnwatchedPurses() {
693
+ const { state, facets } = this;
694
+ const { helper, self } = facets;
695
+ const { invitationPurse, address } = state;
696
+
697
+ const brandToPurses = getBrandToPurses(walletPurses, self);
698
+ trace(`Found ${brandToPurses.values()} purse(s) for ${address}`);
699
+ for (const purses of brandToPurses.values()) {
700
+ for (const record of purses) {
701
+ void helper.watchPurse(record.purse);
702
+ trace(`Repaired purse ${record.petname} of ${address}`);
703
+ }
704
+ }
705
+
706
+ void helper.watchPurse(invitationPurse);
707
+ },
708
+
709
+ /** @param {import('./offers.js').OfferStatus} offerStatus */
710
+ updateStatus(offerStatus) {
711
+ const { state, facets } = this;
712
+ facets.helper.logWalletInfo('offerStatus', offerStatus);
713
+
714
+ void state.updateRecorderKit.recorder.write({
715
+ updated: 'offerStatus',
716
+ status: offerStatus,
717
+ });
718
+
719
+ if ('numWantsSatisfied' in offerStatus) {
720
+ if (state.liveOfferSeats.has(offerStatus.id)) {
721
+ state.liveOfferSeats.delete(offerStatus.id);
722
+ }
723
+
724
+ if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
725
+ facets.helper.getLiveOfferPayments().delete(offerStatus.id);
726
+ }
727
+
728
+ if (state.liveOffers.has(offerStatus.id)) {
729
+ state.liveOffers.delete(offerStatus.id);
730
+ // This might get skipped in subsequent passes, since we .delete()
731
+ // the first time through
732
+ facets.helper.publishCurrentState();
733
+ }
734
+ }
735
+ },
736
+ async addContinuingOffer(
737
+ offerId,
738
+ invitationAmount,
739
+ invitationMakers,
740
+ publicSubscribers,
741
+ ) {
742
+ const { state, facets } = this;
743
+
744
+ state.offerToUsedInvitation.init(offerId, invitationAmount);
745
+ state.offerToInvitationMakers.init(offerId, invitationMakers);
746
+ const pathMap = await objectMapStoragePath(publicSubscribers);
747
+ if (pathMap) {
748
+ facets.helper.logWalletInfo('recording pathMap', pathMap);
749
+ state.offerToPublicSubscriberPaths.init(offerId, pathMap);
750
+ }
751
+ facets.helper.publishCurrentState();
752
+ },
753
+
754
+ /**
755
+ * @param {Brand} brand
756
+ * @returns {Promise<Purse>}
757
+ */
758
+ async purseForBrand(brand) {
759
+ const { state, facets } = this;
760
+ const { registry, invitationBrand } = shared;
761
+
762
+ if (registry.has(brand)) {
763
+ // @ts-expect-error virtual purse
764
+ return E(state.bank).getPurse(brand);
765
+ } else if (invitationBrand === brand) {
766
+ return state.invitationPurse;
767
+ }
768
+
769
+ const purse = await facets.helper.getPurseIfKnownBrand(
770
+ brand,
771
+ shared.agoricNames,
772
+ );
773
+ if (purse) {
774
+ return purse;
775
+ }
776
+ throw Fail`cannot find/make purse for ${brand}`;
777
+ },
778
+ logWalletInfo(...args) {
779
+ const { state } = this;
780
+ console.info('wallet', state.address, ...args);
781
+ },
782
+ logWalletError(...args) {
783
+ const { state } = this;
784
+ console.error('wallet', state.address, ...args);
785
+ },
786
+ // In new SmartWallets, this is part of state, but we can't add fields
787
+ // to instance state for older SmartWallets, so put it in baggage.
788
+ getLiveOfferPayments() {
789
+ const { state } = this;
790
+
791
+ if (state.liveOfferPayments) {
792
+ return state.liveOfferPayments;
793
+ }
794
+
795
+ // This will only happen for legacy wallets, before WF incarnation 2
796
+ if (!baggage.has(state.address)) {
797
+ trace(`getLiveOfferPayments adding store for ${state.address}`);
798
+ baggage.init(
799
+ state.address,
800
+ makeScalarBigMapStore('live offer payments', {
801
+ durable: true,
802
+ }),
803
+ );
804
+ }
805
+ return baggage.get(state.address);
806
+ },
502
807
  },
503
808
  /**
504
809
  * Similar to {DepositFacet} but async because it has to look up the purse.
@@ -514,9 +819,13 @@ export const prepareSmartWallet = (baggage, shared) => {
514
819
  * @throws if there's not yet a purse, though the payment is held to try again when there is
515
820
  */
516
821
  async receive(payment) {
517
- const { helper } = this.facets;
518
- const { paymentQueues: queues, bank, invitationPurse } = this.state;
822
+ const {
823
+ state,
824
+ facets: { helper },
825
+ } = this;
826
+ const { paymentQueues: queues, bank, invitationPurse } = state;
519
827
  const { registry, invitationBrand } = shared;
828
+
520
829
  const brand = await E(payment).getAllegedBrand();
521
830
 
522
831
  // When there is a purse deposit into it
@@ -542,119 +851,195 @@ export const prepareSmartWallet = (baggage, shared) => {
542
851
  throw Fail`cannot deposit payment with brand ${brand}: no purse`;
543
852
  },
544
853
  },
854
+
855
+ payments: {
856
+ /**
857
+ * Withdraw the offered amount from the appropriate purse of this wallet.
858
+ *
859
+ * Save its amount in liveOfferPayments in case we need to reclaim the payment.
860
+ *
861
+ * @param {AmountKeywordRecord} give
862
+ * @param {OfferId} offerId
863
+ * @returns {PaymentPKeywordRecord}
864
+ */
865
+ withdrawGive(give, offerId) {
866
+ const { facets } = this;
867
+
868
+ /** @type {MapStore<Brand, Payment>} */
869
+ const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
870
+ durable: true,
871
+ });
872
+ facets.helper
873
+ .getLiveOfferPayments()
874
+ .init(offerId, brandPaymentRecord);
875
+
876
+ // Add each payment amount to brandPaymentRecord as it is withdrawn. If
877
+ // there's an error later, we can use it to redeposit the correct amount.
878
+ return objectMap(give, amount => {
879
+ /** @type {Promise<Purse>} */
880
+ const purseP = facets.helper.purseForBrand(amount.brand);
881
+ const paymentP = E(purseP).withdraw(amount);
882
+ void E.when(
883
+ paymentP,
884
+ payment => brandPaymentRecord.init(amount.brand, payment),
885
+ e => {
886
+ // recovery will be handled by tryReclaimingWithdrawnPayments()
887
+ facets.helper.logWalletInfo(
888
+ `⚠️ Payment withdrawal failed.`,
889
+ offerId,
890
+ e,
891
+ );
892
+ },
893
+ );
894
+ return paymentP;
895
+ });
896
+ },
897
+
898
+ /**
899
+ * Find the live payments for the offer and deposit them back in the appropriate purses.
900
+ *
901
+ * @param {OfferId} offerId
902
+ * @returns {Promise<void>}
903
+ */
904
+ async tryReclaimingWithdrawnPayments(offerId) {
905
+ const { facets } = this;
906
+
907
+ await null;
908
+
909
+ const liveOfferPayments = facets.helper.getLiveOfferPayments();
910
+ if (liveOfferPayments.has(offerId)) {
911
+ const brandPaymentRecord = liveOfferPayments.get(offerId);
912
+ if (!brandPaymentRecord) {
913
+ return;
914
+ }
915
+ // Use allSettled to ensure we attempt all the deposits, regardless of
916
+ // individual rejections.
917
+ await Promise.allSettled(
918
+ Array.from(brandPaymentRecord.entries()).map(([b, p]) => {
919
+ // Wait for the withdrawal to complete. This protects against a
920
+ // race when updating paymentToPurse.
921
+ const purseP = facets.helper.purseForBrand(b);
922
+
923
+ // Now send it back to the purse.
924
+ return E(purseP).deposit(p);
925
+ }),
926
+ );
927
+ }
928
+ },
929
+ },
930
+
545
931
  offers: {
546
932
  /**
547
933
  * Take an offer description provided in capData, augment it with payments and call zoe.offer()
548
934
  *
549
- * @param {import('./offers.js').OfferSpec} offerSpec
935
+ * @param {OfferSpec} offerSpec
550
936
  * @returns {Promise<void>} after the offer has been both seated and exited by Zoe.
551
937
  * @throws if any parts of the offer can be determined synchronously to be invalid
552
938
  */
553
939
  async executeOffer(offerSpec) {
554
940
  const { facets, state } = this;
555
- const {
556
- address,
557
- bank,
558
- invitationPurse,
559
- offerToInvitationMakers,
560
- offerToUsedInvitation,
561
- offerToPublicSubscriberPaths,
562
- updateRecorderKit,
563
- } = this.state;
564
- const { invitationBrand, zoe, invitationIssuer, registry } = shared;
941
+ const { address, invitationPurse } = state;
942
+ const { zoe, agoricNames } = shared;
943
+ const { invitationBrand, invitationIssuer } = shared;
565
944
 
566
945
  facets.helper.assertUniqueOfferId(String(offerSpec.id));
567
946
 
568
- const logger = {
569
- info: (...args) => console.info('wallet', address, ...args),
570
- error: (...args) => console.error('wallet', address, ...args),
571
- };
947
+ await null;
572
948
 
573
- const executor = makeOfferExecutor({
574
- zoe,
575
- depositFacet: facets.deposit,
576
- invitationIssuer,
577
- powers: {
578
- invitationFromSpec: makeInvitationsHelper(
579
- zoe,
580
- shared.agoricNames,
581
- invitationBrand,
582
- invitationPurse,
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
- }
949
+ let seatRef;
950
+ let watcher;
951
+ try {
952
+ const invitationFromSpec = makeInvitationsHelper(
953
+ zoe,
954
+ agoricNames,
955
+ invitationBrand,
956
+ invitationPurse,
957
+ state.offerToInvitationMakers.get,
958
+ );
598
959
 
599
- const purse = await helper.getPurseIfKnownBrand(
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);
960
+ facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
612
961
 
613
- void updateRecorderKit.recorder.write({
614
- updated: 'offerStatus',
615
- status: offerStatus,
616
- });
962
+ // 1. Prepare values and validate synchronously.
963
+ const { proposal } = offerSpec;
617
964
 
618
- const isSeatExited = 'numWantsSatisfied' in offerStatus;
619
- if (isSeatExited) {
620
- if (state.liveOfferSeats.has(offerStatus.id)) {
621
- state.liveOfferSeats.delete(offerStatus.id);
622
- }
965
+ const invitation = invitationFromSpec(offerSpec.invitationSpec);
623
966
 
624
- if (state.liveOffers.has(offerStatus.id)) {
625
- state.liveOffers.delete(offerStatus.id);
626
- facets.helper.publishCurrentState();
627
- }
628
- }
629
- },
630
- /** @type {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').RemoteInvitationMakers, publicSubscribers?: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord) => Promise<void>} */
631
- onNewContinuingOffer: async (
632
- offerId,
967
+ const [paymentKeywordRecord, invitationAmount] = await Promise.all([
968
+ proposal?.give &&
969
+ deeplyFulfilledObject(
970
+ facets.payments.withdrawGive(proposal.give, offerSpec.id),
971
+ ),
972
+ E(invitationIssuer).getAmountOf(invitation),
973
+ ]);
974
+
975
+ // 2. Begin executing offer
976
+ // No explicit signal to user that we reached here but if anything above
977
+ // failed they'd get an 'error' status update.
978
+
979
+ /** @type {UserSeat} */
980
+ seatRef = await E(zoe).offer(
981
+ invitation,
982
+ proposal,
983
+ paymentKeywordRecord,
984
+ offerSpec.offerArgs,
985
+ );
986
+ facets.helper.logWalletInfo(offerSpec.id, 'seated');
987
+
988
+ watcher = makeOfferWatcher(
989
+ facets.helper,
990
+ facets.deposit,
991
+ offerSpec,
992
+ address,
633
993
  invitationAmount,
634
- invitationMakers,
635
- publicSubscribers,
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
- });
994
+ seatRef,
995
+ );
647
996
 
648
- return executor.executeOffer(offerSpec, seatRef => {
649
997
  state.liveOffers.init(offerSpec.id, offerSpec);
650
- facets.helper.publishCurrentState();
651
998
  state.liveOfferSeats.init(offerSpec.id, seatRef);
652
- });
999
+
1000
+ // publish the live offers
1001
+ facets.helper.publishCurrentState();
1002
+
1003
+ // await so that any errors are caught and handled below
1004
+ await watchOfferOutcomes(watcher, seatRef);
1005
+ } catch (reason) {
1006
+ // This block only runs if the block above fails during one vat incarnation.
1007
+ facets.helper.logWalletError('IMMEDIATE OFFER ERROR:', reason);
1008
+
1009
+ // Update status to observers
1010
+ if (isUpgradeDisconnection(reason)) {
1011
+ // The offer watchers will reconnect. Don't reclaim or exit
1012
+ return;
1013
+ } else if (watcher) {
1014
+ // The watcher's onRejected will updateStatus()
1015
+ } else {
1016
+ facets.helper.updateStatus({
1017
+ error: reason.toString(),
1018
+ ...offerSpec,
1019
+ });
1020
+ }
1021
+
1022
+ // Backstop recovery, in case something very basic fails.
1023
+ if (offerSpec?.proposal?.give) {
1024
+ facets.payments
1025
+ .tryReclaimingWithdrawnPayments(offerSpec.id)
1026
+ .catch(e =>
1027
+ facets.helper.logWalletError(
1028
+ 'recovery failed reclaiming payments',
1029
+ e,
1030
+ ),
1031
+ );
1032
+ }
1033
+
1034
+ // XXX tests rely on throwing immediate errors, not covering the
1035
+ // error handling in the event the failure is after an upgrade
1036
+ throw reason;
1037
+ }
653
1038
  },
654
1039
  /**
655
1040
  * Take an offer's id, look up its seat, try to exit.
656
1041
  *
657
- * @param {import('./offers.js').OfferId} offerId
1042
+ * @param {OfferId} offerId
658
1043
  * @returns {Promise<void>}
659
1044
  * @throws if the seat can't be found or E(seatRef).tryExit() fails.
660
1045
  */
@@ -667,19 +1052,19 @@ export const prepareSmartWallet = (baggage, shared) => {
667
1052
  /**
668
1053
  * Umarshals the actionCapData and delegates to the appropriate action handler.
669
1054
  *
670
- * @param {import('@endo/marshal').CapData<string>} actionCapData of type BridgeAction
1055
+ * @param {import('@endo/marshal').CapData<string | null>} actionCapData of type BridgeAction
671
1056
  * @param {boolean} [canSpend]
672
1057
  * @returns {Promise<void>}
673
1058
  */
674
1059
  handleBridgeAction(actionCapData, canSpend = false) {
1060
+ const { facets } = this;
1061
+ const { offers } = facets;
675
1062
  const { publicMarshaller } = shared;
676
1063
 
677
- const { offers } = this.facets;
678
-
679
1064
  /** @param {Error} err */
680
1065
  const recordError = err => {
681
- const { address, updateRecorderKit } = this.state;
682
- console.error('wallet', address, 'handleBridgeAction error:', err);
1066
+ const { updateRecorderKit } = this.state;
1067
+ facets.helper.logWalletError('handleBridgeAction error:', err);
683
1068
  void updateRecorderKit.recorder.write({
684
1069
  updated: 'walletAction',
685
1070
  status: { error: err.message },
@@ -724,14 +1109,18 @@ export const prepareSmartWallet = (baggage, shared) => {
724
1109
  },
725
1110
  /** @deprecated use getPublicTopics */
726
1111
  getCurrentSubscriber() {
727
- return this.state.currentRecorderKit.subscriber;
1112
+ const { state } = this;
1113
+ return state.currentRecorderKit.subscriber;
728
1114
  },
729
1115
  /** @deprecated use getPublicTopics */
730
1116
  getUpdatesSubscriber() {
731
- return this.state.updateRecorderKit.subscriber;
1117
+ const { state } = this;
1118
+ return state.updateRecorderKit.subscriber;
732
1119
  },
733
1120
  getPublicTopics() {
734
- const { currentRecorderKit, updateRecorderKit } = this.state;
1121
+ const { state } = this;
1122
+ const { currentRecorderKit, updateRecorderKit } = state;
1123
+
735
1124
  return harden({
736
1125
  current: {
737
1126
  description: 'Current state of wallet',
@@ -745,6 +1134,27 @@ export const prepareSmartWallet = (baggage, shared) => {
745
1134
  },
746
1135
  });
747
1136
  },
1137
+ // TODO remove this and repairUnwatchedSeats once the repair has taken place.
1138
+ /**
1139
+ * To be called once ever per wallet.
1140
+ *
1141
+ * @param {object} key
1142
+ */
1143
+ repairWalletForIncarnation2(key) {
1144
+ const { state, facets } = this;
1145
+
1146
+ if (key !== shared.secretWalletFactoryKey) {
1147
+ return;
1148
+ }
1149
+
1150
+ facets.helper.repairUnwatchedSeats().catch(e => {
1151
+ console.error('repairUnwatchedSeats rejection', e);
1152
+ });
1153
+ facets.helper.repairUnwatchedPurses().catch(e => {
1154
+ console.error('repairUnwatchedPurses rejection', e);
1155
+ });
1156
+ trace(`repaired wallet ${state.address}`);
1157
+ },
748
1158
  },
749
1159
  },
750
1160
  {
@@ -752,7 +1162,6 @@ export const prepareSmartWallet = (baggage, shared) => {
752
1162
  const { invitationPurse } = state;
753
1163
  const { helper } = facets;
754
1164
 
755
- // @ts-expect-error RemotePurse cast
756
1165
  void helper.watchPurse(invitationPurse);
757
1166
  },
758
1167
  },