@agoric/smart-wallet 0.5.4-dev-8ecb5d7.0 → 0.5.4-dev-376e47d.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,3 +1,4 @@
1
+ import { E } from '@endo/far';
1
2
  import {
2
3
  AmountShape,
3
4
  BrandShape,
@@ -6,7 +7,12 @@ import {
6
7
  PaymentShape,
7
8
  PurseShape,
8
9
  } from '@agoric/ertp';
9
- import { StorageNodeShape, makeTracer } from '@agoric/internal';
10
+ import {
11
+ deeplyFulfilledObject,
12
+ makeTracer,
13
+ objectMap,
14
+ StorageNodeShape,
15
+ } from '@agoric/internal';
10
16
  import { observeNotifier } from '@agoric/notifier';
11
17
  import { M, mustMatch } from '@agoric/store';
12
18
  import {
@@ -20,15 +26,19 @@ import {
20
26
  provide,
21
27
  } from '@agoric/vat-data';
22
28
  import {
29
+ prepareRecorderKit,
23
30
  SubscriberShape,
24
31
  TopicsRecordShape,
25
- prepareRecorderKit,
26
32
  } from '@agoric/zoe/src/contractSupport/index.js';
27
- import { E } from '@endo/far';
33
+ import {
34
+ AmountKeywordRecordShape,
35
+ PaymentPKeywordRecordShape,
36
+ } from '@agoric/zoe/src/typeGuards.js';
37
+
28
38
  import { makeInvitationsHelper } from './invitations.js';
29
- import { makeOfferExecutor } from './offers.js';
30
39
  import { shape } from './typeGuards.js';
31
40
  import { objectMapStoragePath } from './utils.js';
41
+ import { prepareOfferWatcher, watchOfferOutcomes } from './offerWatcher.js';
32
42
 
33
43
  const { Fail, quote: q } = assert;
34
44
 
@@ -40,17 +50,36 @@ const trace = makeTracer('SmrtWlt');
40
50
  * @see {@link ../README.md}}
41
51
  */
42
52
 
53
+ /** @typedef {number | string} OfferId */
54
+
55
+ /**
56
+ * @typedef {{
57
+ * id: OfferId,
58
+ * invitationSpec: import('./invitations').InvitationSpec,
59
+ * proposal: Proposal,
60
+ * offerArgs?: unknown
61
+ * }} OfferSpec
62
+ */
63
+
64
+ /**
65
+ * @typedef {{
66
+ * logger: {info: (...args: any[]) => void, error: (...args: any[]) => void},
67
+ * makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher,
68
+ * invitationFromSpec: ERef<Invitation>,
69
+ * }} ExecutorPowers
70
+ */
71
+
43
72
  /**
44
73
  * @typedef {{
45
74
  * method: 'executeOffer'
46
- * offer: import('./offers.js').OfferSpec,
75
+ * offer: OfferSpec,
47
76
  * }} ExecuteOfferAction
48
77
  */
49
78
 
50
79
  /**
51
80
  * @typedef {{
52
81
  * method: 'tryExitOffer'
53
- * offerId: import('./offers.js').OfferId,
82
+ * offerId: OfferId,
54
83
  * }} TryExitOfferAction
55
84
  */
56
85
 
@@ -81,7 +110,7 @@ const trace = makeTracer('SmrtWlt');
81
110
  * purses: Array<{brand: Brand, balance: Amount}>,
82
111
  * offerToUsedInvitation: Array<[ offerId: string, usedInvitation: Amount ]>,
83
112
  * offerToPublicSubscriberPaths: Array<[ offerId: string, publicTopics: { [subscriberName: string]: string } ]>,
84
- * liveOffers: Array<[import('./offers.js').OfferId, import('./offers.js').OfferStatus]>,
113
+ * liveOffers: Array<[OfferId, import('./offers.js').OfferStatus]>,
85
114
  * }} CurrentWalletRecord
86
115
  */
87
116
 
@@ -129,6 +158,7 @@ const trace = makeTracer('SmrtWlt');
129
158
  * invitationDisplayInfo: DisplayInfo,
130
159
  * publicMarshaller: Marshaller,
131
160
  * zoe: ERef<ZoeService>,
161
+ * secretWalletFactoryKey: any,
132
162
  * }} SharedParams
133
163
  *
134
164
  * @typedef {ImmutableState & MutableState} State
@@ -145,8 +175,9 @@ const trace = makeTracer('SmrtWlt');
145
175
  * purseBalances: MapStore<Purse, Amount>,
146
176
  * updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>,
147
177
  * currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<CurrentWalletRecord>,
148
- * liveOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>,
149
- * liveOfferSeats: WeakMapStore<import('./offers.js').OfferId, UserSeat<unknown>>,
178
+ * liveOffers: MapStore<OfferId, import('./offers.js').OfferStatus>,
179
+ * liveOfferSeats: MapStore<OfferId, UserSeat<unknown>>,
180
+ * liveOfferPayments: MapStore<OfferId, MapStore<Brand, Payment>>,
150
181
  * }>} ImmutableState
151
182
  *
152
183
  * @typedef {BrandDescriptor & { purse: Purse }} PurseRecord
@@ -218,6 +249,12 @@ export const prepareSmartWallet = (baggage, shared) => {
218
249
  invitationDisplayInfo: DisplayInfoShape,
219
250
  publicMarshaller: M.remotable('Marshaller'),
220
251
  zoe: M.eref(M.remotable('ZoeService')),
252
+
253
+ // known only to smartWallets and walletFactory, this allows the
254
+ // walletFactory to invoke functions on the self facet that no one else
255
+ // can. Used to protect the upgrade-to-incarnation 2 repair. This can be
256
+ // dropped once the repair has taken place.
257
+ secretWalletFactoryKey: M.any(),
221
258
  }),
222
259
  );
223
260
 
@@ -232,8 +269,9 @@ export const prepareSmartWallet = (baggage, shared) => {
232
269
  return store;
233
270
  });
234
271
 
272
+ const makeOfferWatcher = prepareOfferWatcher(baggage);
273
+
235
274
  /**
236
- *
237
275
  * @param {UniqueParams} unique
238
276
  * @returns {State}
239
277
  */
@@ -297,6 +335,9 @@ export const prepareSmartWallet = (baggage, shared) => {
297
335
  liveOfferSeats: makeScalarBigMapStore('live offer seats', {
298
336
  durable: true,
299
337
  }),
338
+ liveOfferPayments: makeScalarBigMapStore('live offer payments', {
339
+ durable: true,
340
+ }),
300
341
  };
301
342
 
302
343
  return {
@@ -315,10 +356,32 @@ export const prepareSmartWallet = (baggage, shared) => {
315
356
  .returns(M.promise()),
316
357
  publishCurrentState: M.call().returns(),
317
358
  watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()),
359
+ repairUnwatchedSeats: M.call().returns(),
360
+ updateStatus: M.call(M.any()).returns(),
361
+ addContinuingOffer: M.call(
362
+ M.or(M.number(), M.string()),
363
+ AmountShape,
364
+ M.remotable('InvitationMaker'),
365
+ M.or(M.record(), M.undefined()),
366
+ ).returns(M.promise()),
367
+ purseForBrand: M.call(BrandShape).returns(M.promise()),
368
+ logWalletInfo: M.call().rest(M.arrayOf(M.any())).returns(),
369
+ logWalletError: M.call().rest(M.arrayOf(M.any())).returns(),
370
+ getLiveOfferPayments: M.call().returns(M.remotable('mapStore')),
318
371
  }),
372
+
319
373
  deposit: M.interface('depositFacetI', {
320
374
  receive: M.callWhen(M.await(M.eref(PaymentShape))).returns(AmountShape),
321
375
  }),
376
+ payments: M.interface('payments support', {
377
+ withdrawGive: M.call(
378
+ AmountKeywordRecordShape,
379
+ M.or(M.number(), M.string()),
380
+ ).returns(PaymentPKeywordRecordShape),
381
+ tryReclaimingWithdrawnPayments: M.call(
382
+ M.or(M.number(), M.string()),
383
+ ).returns(M.promise()),
384
+ }),
322
385
  offers: M.interface('offers facet', {
323
386
  executeOffer: M.call(shape.OfferSpec).returns(M.promise()),
324
387
  tryExitOffer: M.call(M.scalar()).returns(M.promise()),
@@ -332,6 +395,7 @@ export const prepareSmartWallet = (baggage, shared) => {
332
395
  getCurrentSubscriber: M.call().returns(SubscriberShape),
333
396
  getUpdatesSubscriber: M.call().returns(SubscriberShape),
334
397
  getPublicTopics: M.call().returns(TopicsRecordShape),
398
+ repairWalletForIncarnation2: M.call(M.any()).returns(),
335
399
  }),
336
400
  };
337
401
 
@@ -355,6 +419,7 @@ export const prepareSmartWallet = (baggage, shared) => {
355
419
  * @type {(id: string) => void}
356
420
  */
357
421
  assertUniqueOfferId(id) {
422
+ const { facets } = this;
358
423
  const {
359
424
  liveOffers,
360
425
  liveOfferSeats,
@@ -365,6 +430,7 @@ export const prepareSmartWallet = (baggage, shared) => {
365
430
  const used =
366
431
  liveOffers.has(id) ||
367
432
  liveOfferSeats.has(id) ||
433
+ facets.helper.getLiveOfferPayments().has(id) ||
368
434
  offerToInvitationMakers.has(id) ||
369
435
  offerToPublicSubscriberPaths.has(id) ||
370
436
  offerToUsedInvitation.has(id);
@@ -412,7 +478,7 @@ export const prepareSmartWallet = (baggage, shared) => {
412
478
 
413
479
  /** @type {(purse: ERef<Purse>) => Promise<void>} */
414
480
  async watchPurse(purseRef) {
415
- const { address } = this.state;
481
+ const { facets } = this;
416
482
 
417
483
  const purse = await purseRef; // promises don't fit in durable storage
418
484
 
@@ -422,8 +488,7 @@ export const prepareSmartWallet = (baggage, shared) => {
422
488
  E(purse).getCurrentAmount(),
423
489
  balance => helper.updateBalance(purse, balance),
424
490
  err =>
425
- console.error(
426
- address,
491
+ facets.helper.logWalletError(
427
492
  'initial purse balance publish failed',
428
493
  err,
429
494
  ),
@@ -433,7 +498,10 @@ export const prepareSmartWallet = (baggage, shared) => {
433
498
  helper.updateBalance(purse, balance);
434
499
  },
435
500
  fail(reason) {
436
- console.error(address, `failed updateState observer`, reason);
501
+ facets.helper.logWalletError(
502
+ '⚠️ failed updateState observer',
503
+ reason,
504
+ );
437
505
  },
438
506
  });
439
507
  },
@@ -442,7 +510,7 @@ export const prepareSmartWallet = (baggage, shared) => {
442
510
  * Provide a purse given a NameHub of issuers and their
443
511
  * brands.
444
512
  *
445
- * We current support only one NameHub, agoricNames, and
513
+ * We currently support only one NameHub, agoricNames, and
446
514
  * hence one purse per brand. But we store an array of them
447
515
  * to facilitate a transition to decentralized introductions.
448
516
  *
@@ -494,6 +562,154 @@ export const prepareSmartWallet = (baggage, shared) => {
494
562
  void helper.watchPurse(purse);
495
563
  return purse;
496
564
  },
565
+
566
+ /**
567
+ * see https://github.com/Agoric/agoric-sdk/issues/8445 and
568
+ * https://github.com/Agoric/agoric-sdk/issues/8286. As originally
569
+ * released, the smartWallet didn't durably monitor the promises for the
570
+ * outcomes of offers, and would have dropped them on upgrade of Zoe or
571
+ * the smartWallet itself. Using watchedPromises, (see offerWatcher.js)
572
+ * we've addressed the problem for new offers. This function will
573
+ * backfill the solution for offers that were outstanding before the
574
+ * transition to incarnation 2 of the smartWallet.
575
+ */
576
+ async repairUnwatchedSeats() {
577
+ const { state, facets } = this;
578
+ const { address, invitationPurse, liveOffers, liveOfferSeats } =
579
+ state;
580
+ const { zoe, agoricNames, invitationBrand, invitationIssuer } =
581
+ shared;
582
+
583
+ await null;
584
+
585
+ const invitationFromSpec = makeInvitationsHelper(
586
+ zoe,
587
+ agoricNames,
588
+ invitationBrand,
589
+ invitationPurse,
590
+ state.offerToInvitationMakers.get,
591
+ );
592
+
593
+ for (const seatId of liveOfferSeats.keys()) {
594
+ facets.helper.logWalletInfo(`repairing ${seatId}`);
595
+ const offerSpec = liveOffers.get(seatId);
596
+ const seat = liveOfferSeats.get(seatId);
597
+
598
+ const invitation = invitationFromSpec(offerSpec.invitationSpec);
599
+ const invitationAmount =
600
+ await E(invitationIssuer).getAmountOf(invitation);
601
+ const watcher = makeOfferWatcher(
602
+ facets.helper,
603
+ facets.deposit,
604
+ offerSpec,
605
+ address,
606
+ invitationAmount,
607
+ seat,
608
+ );
609
+
610
+ void watchOfferOutcomes(watcher, seat);
611
+ trace(`Repaired seat ${seatId} for wallet ${address}`);
612
+ }
613
+ },
614
+
615
+ /** @param {import('./offers.js').OfferStatus} offerStatus */
616
+ updateStatus(offerStatus) {
617
+ const { state, facets } = this;
618
+ facets.helper.logWalletInfo('offerStatus', offerStatus);
619
+
620
+ void state.updateRecorderKit.recorder.write({
621
+ updated: 'offerStatus',
622
+ status: offerStatus,
623
+ });
624
+
625
+ if ('numWantsSatisfied' in offerStatus) {
626
+ if (state.liveOfferSeats.has(offerStatus.id)) {
627
+ state.liveOfferSeats.delete(offerStatus.id);
628
+ }
629
+
630
+ if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
631
+ facets.helper.getLiveOfferPayments().delete(offerStatus.id);
632
+ }
633
+
634
+ if (state.liveOffers.has(offerStatus.id)) {
635
+ state.liveOffers.delete(offerStatus.id);
636
+ // This might get skipped in subsequent passes, since we .delete()
637
+ // the first time through
638
+ facets.helper.publishCurrentState();
639
+ }
640
+ }
641
+ },
642
+ async addContinuingOffer(
643
+ offerId,
644
+ invitationAmount,
645
+ invitationMakers,
646
+ publicSubscribers,
647
+ ) {
648
+ const { state, facets } = this;
649
+
650
+ state.offerToUsedInvitation.init(offerId, invitationAmount);
651
+ state.offerToInvitationMakers.init(offerId, invitationMakers);
652
+ const pathMap = await objectMapStoragePath(publicSubscribers);
653
+ if (pathMap) {
654
+ facets.helper.logWalletInfo('recording pathMap', pathMap);
655
+ state.offerToPublicSubscriberPaths.init(offerId, pathMap);
656
+ }
657
+ facets.helper.publishCurrentState();
658
+ },
659
+
660
+ /**
661
+ * @param {Brand} brand
662
+ * @returns {Promise<Purse>}
663
+ */
664
+ async purseForBrand(brand) {
665
+ const { state, facets } = this;
666
+ const { registry, invitationBrand } = shared;
667
+
668
+ if (registry.has(brand)) {
669
+ // @ts-expect-error virtual purse
670
+ return E(state.bank).getPurse(brand);
671
+ } else if (invitationBrand === brand) {
672
+ return state.invitationPurse;
673
+ }
674
+
675
+ const purse = await facets.helper.getPurseIfKnownBrand(
676
+ brand,
677
+ shared.agoricNames,
678
+ );
679
+ if (purse) {
680
+ return purse;
681
+ }
682
+ throw Fail`cannot find/make purse for ${brand}`;
683
+ },
684
+ logWalletInfo(...args) {
685
+ const { state } = this;
686
+ console.info('wallet', state.address, ...args);
687
+ },
688
+ logWalletError(...args) {
689
+ const { state } = this;
690
+ console.error('wallet', state.address, ...args);
691
+ },
692
+ // In new SmartWallets, this is part of state, but we can't add fields
693
+ // to instance state for older SmartWallets, so put it in baggage.
694
+ getLiveOfferPayments() {
695
+ const { state } = this;
696
+
697
+ if (state.liveOfferPayments) {
698
+ return state.liveOfferPayments;
699
+ }
700
+
701
+ // This will only happen for legacy wallets, before WF incarnation 2
702
+ if (!baggage.has(state.address)) {
703
+ trace(`getLiveOfferPayments adding store for ${state.address}`);
704
+ baggage.init(
705
+ state.address,
706
+ makeScalarBigMapStore('live offer payments', {
707
+ durable: true,
708
+ }),
709
+ );
710
+ }
711
+ return baggage.get(state.address);
712
+ },
497
713
  },
498
714
  /**
499
715
  * Similar to {DepositFacet} but async because it has to look up the purse.
@@ -509,9 +725,13 @@ export const prepareSmartWallet = (baggage, shared) => {
509
725
  * @throws if there's not yet a purse, though the payment is held to try again when there is
510
726
  */
511
727
  async receive(payment) {
512
- const { helper } = this.facets;
513
- const { paymentQueues: queues, bank, invitationPurse } = this.state;
728
+ const {
729
+ state,
730
+ facets: { helper },
731
+ } = this;
732
+ const { paymentQueues: queues, bank, invitationPurse } = state;
514
733
  const { registry, invitationBrand } = shared;
734
+
515
735
  const brand = await E(payment).getAllegedBrand();
516
736
 
517
737
  // When there is a purse deposit into it
@@ -537,118 +757,183 @@ export const prepareSmartWallet = (baggage, shared) => {
537
757
  throw Fail`cannot deposit payment with brand ${brand}: no purse`;
538
758
  },
539
759
  },
760
+
761
+ payments: {
762
+ /**
763
+ * @param {AmountKeywordRecord} give
764
+ * @param {OfferId} offerId
765
+ * @returns {PaymentPKeywordRecord}
766
+ */
767
+ withdrawGive(give, offerId) {
768
+ const { facets } = this;
769
+
770
+ /** @type {MapStore<Brand, Payment>} */
771
+ const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
772
+ durable: true,
773
+ });
774
+ facets.helper
775
+ .getLiveOfferPayments()
776
+ .init(offerId, brandPaymentRecord);
777
+
778
+ // Add each payment to liveOfferPayments as it is withdrawn. If
779
+ // there's an error partway through, we can recover the withdrawals.
780
+ return objectMap(give, amount => {
781
+ /** @type {Promise<Purse>} */
782
+ const purseP = facets.helper.purseForBrand(amount.brand);
783
+ const paymentP = E(purseP).withdraw(amount);
784
+ void E.when(
785
+ paymentP,
786
+ payment => brandPaymentRecord.init(amount.brand, payment),
787
+ e => {
788
+ // recovery will be handled by tryReclaimingWithdrawnPayments()
789
+ facets.helper.logWalletInfo(
790
+ `⚠️ Payment withdrawal failed.`,
791
+ offerId,
792
+ e,
793
+ );
794
+ },
795
+ );
796
+ return paymentP;
797
+ });
798
+ },
799
+
800
+ async tryReclaimingWithdrawnPayments(offerId) {
801
+ const { facets } = this;
802
+
803
+ const liveOfferPayments = facets.helper.getLiveOfferPayments();
804
+ if (liveOfferPayments.has(offerId)) {
805
+ const brandPaymentRecord = liveOfferPayments.get(offerId);
806
+ if (!brandPaymentRecord) {
807
+ return Promise.resolve(undefined);
808
+ }
809
+ // Use allSettled to ensure we attempt all the deposits, regardless of
810
+ // individual rejections.
811
+ return Promise.allSettled(
812
+ Array.from(brandPaymentRecord.entries()).map(async ([b, p]) => {
813
+ // Wait for the withdrawal to complete. This protects against a
814
+ // race when updating paymentToPurse.
815
+ const purseP = facets.helper.purseForBrand(b);
816
+
817
+ // Now send it back to the purse.
818
+ return E(purseP).deposit(p);
819
+ }),
820
+ );
821
+ }
822
+ },
823
+ },
824
+
540
825
  offers: {
541
826
  /**
542
827
  * Take an offer description provided in capData, augment it with payments and call zoe.offer()
543
828
  *
544
- * @param {import('./offers.js').OfferSpec} offerSpec
829
+ * @param {OfferSpec} offerSpec
545
830
  * @returns {Promise<void>} after the offer has been both seated and exited by Zoe.
546
831
  * @throws if any parts of the offer can be determined synchronously to be invalid
547
832
  */
548
833
  async executeOffer(offerSpec) {
549
834
  const { facets, state } = this;
550
- const {
551
- address,
552
- bank,
553
- invitationPurse,
554
- offerToInvitationMakers,
555
- offerToUsedInvitation,
556
- offerToPublicSubscriberPaths,
557
- updateRecorderKit,
558
- } = this.state;
559
- const { invitationBrand, zoe, invitationIssuer, registry } = shared;
835
+ const { address, invitationPurse } = state;
836
+ const { zoe, agoricNames } = shared;
837
+ const { invitationBrand, invitationIssuer } = shared;
560
838
 
561
839
  facets.helper.assertUniqueOfferId(String(offerSpec.id));
562
840
 
563
- const logger = {
564
- info: (...args) => console.info('wallet', address, ...args),
565
- error: (...args) => console.error('wallet', address, ...args),
566
- };
841
+ await null;
567
842
 
568
- const executor = makeOfferExecutor({
569
- zoe,
570
- depositFacet: facets.deposit,
571
- invitationIssuer,
572
- powers: {
573
- invitationFromSpec: makeInvitationsHelper(
574
- zoe,
575
- shared.agoricNames,
576
- invitationBrand,
577
- invitationPurse,
578
- offerToInvitationMakers.get,
579
- ),
580
- /**
581
- * @param {Brand} brand
582
- * @returns {Promise<Purse>}
583
- */
584
- purseForBrand: async brand => {
585
- const { helper } = facets;
586
- if (registry.has(brand)) {
587
- // @ts-expect-error virtual purse
588
- return E(bank).getPurse(brand);
589
- } else if (invitationBrand === brand) {
590
- return invitationPurse;
591
- }
843
+ let seatRef;
844
+ let watcher;
845
+ try {
846
+ const invitationFromSpec = makeInvitationsHelper(
847
+ zoe,
848
+ agoricNames,
849
+ invitationBrand,
850
+ invitationPurse,
851
+ state.offerToInvitationMakers.get,
852
+ );
592
853
 
593
- const purse = await helper.getPurseIfKnownBrand(
594
- brand,
595
- shared.agoricNames,
596
- );
597
- if (purse) {
598
- return purse;
599
- }
600
- throw Fail`cannot find/make purse for ${brand}`;
601
- },
602
- logger,
603
- },
604
- onStatusChange: offerStatus => {
605
- logger.info('offerStatus', offerStatus);
854
+ facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
606
855
 
607
- void updateRecorderKit.recorder.write({
608
- updated: 'offerStatus',
609
- status: offerStatus,
610
- });
856
+ // 1. Prepare values and validate synchronously.
857
+ const { proposal } = offerSpec;
611
858
 
612
- const isSeatExited = 'numWantsSatisfied' in offerStatus;
613
- if (isSeatExited) {
614
- if (state.liveOfferSeats.has(offerStatus.id)) {
615
- state.liveOfferSeats.delete(offerStatus.id);
616
- }
859
+ const invitation = invitationFromSpec(offerSpec.invitationSpec);
617
860
 
618
- if (state.liveOffers.has(offerStatus.id)) {
619
- state.liveOffers.delete(offerStatus.id);
620
- facets.helper.publishCurrentState();
621
- }
622
- }
623
- },
624
- /** @type {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types.js').InvitationMakers, publicSubscribers?: import('./types.js').PublicSubscribers | import('@agoric/zoe/src/contractSupport/index.js').TopicsRecord) => Promise<void>} */
625
- onNewContinuingOffer: async (
626
- offerId,
861
+ const [paymentKeywordRecord, invitationAmount] = await Promise.all([
862
+ proposal?.give &&
863
+ deeplyFulfilledObject(
864
+ facets.payments.withdrawGive(proposal.give, offerSpec.id),
865
+ ),
866
+ E(invitationIssuer).getAmountOf(invitation),
867
+ ]);
868
+
869
+ // 2. Begin executing offer
870
+ // No explicit signal to user that we reached here but if anything above
871
+ // failed they'd get an 'error' status update.
872
+
873
+ /** @type {UserSeat} */
874
+ seatRef = await E(zoe).offer(
875
+ invitation,
876
+ proposal,
877
+ paymentKeywordRecord,
878
+ offerSpec.offerArgs,
879
+ );
880
+ facets.helper.logWalletInfo(offerSpec.id, 'seated');
881
+
882
+ watcher = makeOfferWatcher(
883
+ facets.helper,
884
+ facets.deposit,
885
+ offerSpec,
886
+ address,
627
887
  invitationAmount,
628
- invitationMakers,
629
- publicSubscribers,
630
- ) => {
631
- offerToUsedInvitation.init(offerId, invitationAmount);
632
- offerToInvitationMakers.init(offerId, invitationMakers);
633
- const pathMap = await objectMapStoragePath(publicSubscribers);
634
- if (pathMap) {
635
- logger.info('recording pathMap', pathMap);
636
- offerToPublicSubscriberPaths.init(offerId, pathMap);
637
- }
638
- facets.helper.publishCurrentState();
639
- },
640
- });
888
+ seatRef,
889
+ );
641
890
 
642
- return executor.executeOffer(offerSpec, seatRef => {
643
891
  state.liveOffers.init(offerSpec.id, offerSpec);
644
- facets.helper.publishCurrentState();
645
892
  state.liveOfferSeats.init(offerSpec.id, seatRef);
646
- });
893
+
894
+ // publish the live offers
895
+ facets.helper.publishCurrentState();
896
+
897
+ // await so that any errors are caught and handled below
898
+ await watchOfferOutcomes(watcher, seatRef);
899
+ } catch (err) {
900
+ facets.helper.logWalletError('OFFER ERROR:', err);
901
+ // Notify the user
902
+ if (watcher) {
903
+ watcher.helper.updateStatus({ error: err.toString() });
904
+ } else {
905
+ facets.helper.updateStatus({
906
+ error: err.toString(),
907
+ ...offerSpec,
908
+ });
909
+ }
910
+
911
+ if (offerSpec?.proposal?.give) {
912
+ facets.payments
913
+ .tryReclaimingWithdrawnPayments(offerSpec.id)
914
+ .catch(e =>
915
+ facets.helper.logWalletError(
916
+ 'recovery failed reclaiming payments',
917
+ e,
918
+ ),
919
+ );
920
+ }
921
+
922
+ if (seatRef) {
923
+ void E.when(E(seatRef).hasExited(), hasExited => {
924
+ if (!hasExited) {
925
+ void E(seatRef).tryExit();
926
+ }
927
+ });
928
+ }
929
+
930
+ throw err;
931
+ }
647
932
  },
648
933
  /**
649
934
  * Take an offer's id, look up its seat, try to exit.
650
935
  *
651
- * @param {import('./offers.js').OfferId} offerId
936
+ * @param {OfferId} offerId
652
937
  * @returns {Promise<void>}
653
938
  * @throws if the seat can't be found or E(seatRef).tryExit() fails.
654
939
  */
@@ -666,14 +951,14 @@ export const prepareSmartWallet = (baggage, shared) => {
666
951
  * @returns {Promise<void>}
667
952
  */
668
953
  handleBridgeAction(actionCapData, canSpend = false) {
954
+ const { facets } = this;
955
+ const { offers } = facets;
669
956
  const { publicMarshaller } = shared;
670
957
 
671
- const { offers } = this.facets;
672
-
673
958
  /** @param {Error} err */
674
959
  const recordError = err => {
675
- const { address, updateRecorderKit } = this.state;
676
- console.error('wallet', address, 'handleBridgeAction error:', err);
960
+ const { updateRecorderKit } = this.state;
961
+ facets.helper.logWalletError('handleBridgeAction error:', err);
677
962
  void updateRecorderKit.recorder.write({
678
963
  updated: 'walletAction',
679
964
  status: { error: err.message },
@@ -718,14 +1003,18 @@ export const prepareSmartWallet = (baggage, shared) => {
718
1003
  },
719
1004
  /** @deprecated use getPublicTopics */
720
1005
  getCurrentSubscriber() {
721
- return this.state.currentRecorderKit.subscriber;
1006
+ const { state } = this;
1007
+ return state.currentRecorderKit.subscriber;
722
1008
  },
723
1009
  /** @deprecated use getPublicTopics */
724
1010
  getUpdatesSubscriber() {
725
- return this.state.updateRecorderKit.subscriber;
1011
+ const { state } = this;
1012
+ return state.updateRecorderKit.subscriber;
726
1013
  },
727
1014
  getPublicTopics() {
728
- const { currentRecorderKit, updateRecorderKit } = this.state;
1015
+ const { state } = this;
1016
+ const { currentRecorderKit, updateRecorderKit } = state;
1017
+
729
1018
  return harden({
730
1019
  current: {
731
1020
  description: 'Current state of wallet',
@@ -739,6 +1028,21 @@ export const prepareSmartWallet = (baggage, shared) => {
739
1028
  },
740
1029
  });
741
1030
  },
1031
+ /**
1032
+ * one-time use function. Remove this and repairUnwatchedSeats once the
1033
+ * repair has taken place.
1034
+ *
1035
+ * @param {object} key
1036
+ */
1037
+ repairWalletForIncarnation2(key) {
1038
+ const { facets } = this;
1039
+
1040
+ if (key !== shared.secretWalletFactoryKey) {
1041
+ return;
1042
+ }
1043
+
1044
+ void facets.helper.repairUnwatchedSeats();
1045
+ },
742
1046
  },
743
1047
  },
744
1048
  {