@agoric/smart-wallet 0.5.3 → 0.5.4-calypso-dev-84eb287.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,5 @@
1
+ import { E } from '@endo/far';
1
2
  import {
2
- AmountMath,
3
3
  AmountShape,
4
4
  BrandShape,
5
5
  DisplayInfoShape,
@@ -7,152 +7,268 @@ import {
7
7
  PaymentShape,
8
8
  PurseShape,
9
9
  } from '@agoric/ertp';
10
- import { StorageNodeShape } from '@agoric/internal';
11
- 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';
12
17
  import { M, mustMatch } from '@agoric/store';
13
- import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
14
- import { makeScalarBigMapStore, prepareExoClassKit } from '@agoric/vat-data';
18
+ import {
19
+ appendToStoredArray,
20
+ provideLazy,
21
+ } from '@agoric/store/src/stores/store-utils.js';
22
+ import {
23
+ makeScalarBigMapStore,
24
+ makeScalarBigWeakMapStore,
25
+ prepareExoClass,
26
+ prepareExoClassKit,
27
+ provide,
28
+ watchPromise,
29
+ } from '@agoric/vat-data';
15
30
  import {
16
31
  prepareRecorderKit,
17
32
  SubscriberShape,
18
33
  TopicsRecordShape,
19
34
  } from '@agoric/zoe/src/contractSupport/index.js';
20
- import { E } from '@endo/far';
35
+ import {
36
+ AmountKeywordRecordShape,
37
+ PaymentPKeywordRecordShape,
38
+ } from '@agoric/zoe/src/typeGuards.js';
39
+
21
40
  import { makeInvitationsHelper } from './invitations.js';
22
- import { makeOfferExecutor } from './offers.js';
23
41
  import { shape } from './typeGuards.js';
24
42
  import { objectMapStoragePath } from './utils.js';
43
+ import { prepareOfferWatcher, watchOfferOutcomes } from './offerWatcher.js';
44
+
45
+ /** @import {OfferId, OfferStatus} from './offers.js'; */
25
46
 
26
47
  const { Fail, quote: q } = assert;
27
48
 
49
+ const trace = makeTracer('SmrtWlt');
50
+
28
51
  /**
29
52
  * @file Smart wallet module
30
- *
31
- * @see {@link ../README.md}}
53
+ * @see {@link ../README.md} }
54
+ */
55
+
56
+ /** @typedef {number | string} OfferId */
57
+
58
+ /**
59
+ * @typedef {{
60
+ * id: OfferId;
61
+ * invitationSpec: import('./invitations').InvitationSpec;
62
+ * proposal: Proposal;
63
+ * offerArgs?: any;
64
+ * }} OfferSpec
32
65
  */
33
66
 
34
67
  /**
35
68
  * @typedef {{
36
- * method: 'executeOffer'
37
- * offer: import('./offers.js').OfferSpec,
69
+ * logger: {
70
+ * info: (...args: any[]) => void;
71
+ * error: (...args: any[]) => void;
72
+ * };
73
+ * makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher;
74
+ * invitationFromSpec: ERef<Invitation>;
75
+ * }} ExecutorPowers
76
+ */
77
+
78
+ /**
79
+ * @typedef {{
80
+ * method: 'executeOffer';
81
+ * offer: OfferSpec;
38
82
  * }} ExecuteOfferAction
39
83
  */
40
84
 
41
85
  /**
42
86
  * @typedef {{
43
- * method: 'tryExitOffer'
44
- * offerId: import('./offers.js').OfferId,
87
+ * method: 'tryExitOffer';
88
+ * offerId: OfferId;
45
89
  * }} TryExitOfferAction
46
90
  */
47
91
 
48
92
  // Discriminated union. Possible future messages types:
49
93
  // maybe suggestIssuer for https://github.com/Agoric/agoric-sdk/issues/6132
50
94
  // setting petnames and adding brands for https://github.com/Agoric/agoric-sdk/issues/6126
51
- /**
52
- * @typedef { ExecuteOfferAction | TryExitOfferAction } BridgeAction
53
- */
95
+ /** @typedef {ExecuteOfferAction | TryExitOfferAction} BridgeAction */
54
96
 
55
97
  /**
56
- * Purses is an array to support a future requirement of multiple purses per brand.
98
+ * Purses is an array to support a future requirement of multiple purses per
99
+ * brand.
57
100
  *
58
- * Each map is encoded as an array of entries because a Map doesn't serialize directly.
59
- * We also considered having a vstorage key for each offer but for now are sticking with this design.
101
+ * Each map is encoded as an array of entries because a Map doesn't serialize
102
+ * directly. We also considered having a vstorage key for each offer but for now
103
+ * are sticking with this design.
60
104
  *
61
105
  * Cons
62
- * - Reserializes previously written results when a new result is added
63
- * - Optimizes reads though writes are on-chain (~100 machines) and reads are off-chain (to 1 machine)
106
+ *
107
+ * - Reserializes previously written results when a new result is added
108
+ * - Optimizes reads though writes are on-chain (~100 machines) and reads are
109
+ * off-chain (to 1 machine)
64
110
  *
65
111
  * Pros
66
- * - Reading all offer results happens much more (>100) often than storing a new offer result
67
- * - Reserialization and writes are paid in execution gas, whereas reads are not
68
112
  *
69
- * This design should be revisited if ever batch querying across vstorage keys become cheaper or reads be paid.
113
+ * - Reading all offer results happens much more (>100) often than storing a new
114
+ * offer result
115
+ * - Reserialization and writes are paid in execution gas, whereas reads are not
116
+ *
117
+ * This design should be revisited if ever batch querying across vstorage keys
118
+ * become cheaper or reads be paid.
70
119
  *
71
120
  * @typedef {{
72
- * purses: Array<{brand: Brand, balance: Amount}>,
73
- * offerToUsedInvitation: Array<[ offerId: string, usedInvitation: Amount ]>,
74
- * offerToPublicSubscriberPaths: Array<[ offerId: string, publicTopics: { [subscriberName: string]: string } ]>,
75
- * liveOffers: Array<[import('./offers.js').OfferId, import('./offers.js').OfferStatus]>,
121
+ * purses: { brand: Brand; balance: Amount }[];
122
+ * offerToUsedInvitation: [offerId: string, usedInvitation: Amount][];
123
+ * offerToPublicSubscriberPaths: [
124
+ * offerId: string,
125
+ * publicTopics: { [subscriberName: string]: string },
126
+ * ][];
127
+ * liveOffers: [OfferId, OfferStatus][];
76
128
  * }} CurrentWalletRecord
77
129
  */
78
130
 
79
131
  /**
80
- * @typedef {{ updated: 'offerStatus', status: import('./offers.js').OfferStatus }
132
+ * @typedef {{ updated: 'offerStatus'; status: OfferStatus }
81
133
  * | { updated: 'balance'; currentAmount: Amount }
82
- * | { updated: 'walletAction'; status: { error: string } }
83
- * } UpdateRecord Record of an update to the state of this wallet.
134
+ * | { updated: 'walletAction'; status: { error: string } }} UpdateRecord
135
+ * Record of an update to the state of this wallet.
84
136
  *
85
- * Client is responsible for coalescing updates into a current state. See `coalesceUpdates` utility.
137
+ * Client is responsible for coalescing updates into a current state. See
138
+ * `coalesceUpdates` utility.
86
139
  *
87
- * The reason for this burden on the client is that publishing
88
- * the full history of offers with each change is untenable.
140
+ * The reason for this burden on the client is that publishing the full history
141
+ * of offers with each change is untenable.
89
142
  *
90
- * `balance` update supports forward-compatibility for more than one purse per
91
- * brand. An additional key will be needed to disambiguate. For now the brand in
92
- * the amount suffices.
143
+ * `balance` update supports forward-compatibility for more than one purse per
144
+ * brand. An additional key will be needed to disambiguate. For now the brand
145
+ * in the amount suffices.
93
146
  */
94
147
 
95
148
  /**
96
149
  * @typedef {{
97
- * brand: Brand,
98
- * displayInfo: DisplayInfo,
99
- * issuer: Issuer,
100
- * petname: import('./types').Petname
150
+ * brand: Brand;
151
+ * displayInfo: DisplayInfo;
152
+ * issuer: Issuer;
153
+ * petname: import('./types.js').Petname;
101
154
  * }} BrandDescriptor
102
- * For use by clients to describe brands to users. Includes `displayInfo` to save a remote call.
155
+ * For use by clients to describe brands to users. Includes `displayInfo` to
156
+ * save a remote call.
103
157
  */
104
158
 
105
- // imports
106
- /** @typedef {import('./types').RemotePurse} RemotePurse */
107
-
108
159
  /**
109
160
  * @typedef {{
110
- * address: string,
111
- * bank: ERef<import('@agoric/vats/src/vat-bank').Bank>,
112
- * currentStorageNode: StorageNode,
113
- * invitationPurse: Purse<'set'>,
114
- * walletStorageNode: StorageNode,
161
+ * address: string;
162
+ * bank: ERef<import('@agoric/vats/src/vat-bank.js').Bank>;
163
+ * currentStorageNode: StorageNode;
164
+ * invitationPurse: Purse<'set', InvitationDetails>;
165
+ * walletStorageNode: StorageNode;
115
166
  * }} UniqueParams
116
167
  *
168
+ *
117
169
  * @typedef {Pick<MapStore<Brand, BrandDescriptor>, 'has' | 'get' | 'values'>} BrandDescriptorRegistry
170
+ *
171
+ *
118
172
  * @typedef {{
119
- * agoricNames: ERef<import('@agoric/vats').NameHub>,
120
- * registry: BrandDescriptorRegistry,
121
- * invitationIssuer: Issuer<'set'>,
122
- * invitationBrand: Brand<'set'>,
123
- * invitationDisplayInfo: DisplayInfo,
124
- * publicMarshaller: Marshaller,
125
- * zoe: ERef<ZoeService>,
173
+ * agoricNames: ERef<import('@agoric/vats').NameHub>;
174
+ * registry: BrandDescriptorRegistry;
175
+ * invitationIssuer: Issuer<'set'>;
176
+ * invitationBrand: Brand<'set'>;
177
+ * invitationDisplayInfo: DisplayInfo;
178
+ * publicMarshaller: Marshaller;
179
+ * zoe: ERef<ZoeService>;
180
+ * secretWalletFactoryKey: any;
126
181
  * }} SharedParams
127
182
  *
128
- * @typedef {ImmutableState & MutableState} State
129
- * - `brandPurses` is precious and closely held. defined as late as possible to reduce its scope.
130
- * - `offerToInvitationMakers` is precious and closely held.
131
- * - `offerToPublicSubscriberPaths` is precious and closely held.
132
- * - `purseBalances` is a cache of what we've received from purses. Held so we can publish all balances on change.
133
183
  *
134
- * @typedef {Readonly<UniqueParams & {
135
- * paymentQueues: MapStore<Brand, Array<import('@endo/far').FarRef<Payment>>>,
136
- * offerToInvitationMakers: MapStore<string, import('./types').RemoteInvitationMakers>,
137
- * offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>,
138
- * offerToUsedInvitation: MapStore<string, Amount>,
139
- * purseBalances: MapStore<RemotePurse, Amount>,
140
- * updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>,
141
- * currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<CurrentWalletRecord>,
142
- * liveOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>,
143
- * liveOfferSeats: WeakMapStore<import('./offers.js').OfferId, UserSeat<unknown>>,
144
- * }>} ImmutableState
184
+ * @typedef {ImmutableState & MutableState} State - `brandPurses` is precious
185
+ * and closely held. defined as late as possible to reduce its scope.
145
186
  *
146
- * @typedef {{
147
- * }} MutableState
187
+ * - `offerToInvitationMakers` is precious and closely held.
188
+ * - `offerToPublicSubscriberPaths` is precious and closely held.
189
+ * - `purseBalances` is a cache of what we've received from purses. Held so we can
190
+ * publish all balances on change.
191
+ *
192
+ *
193
+ * @typedef {Readonly<
194
+ * UniqueParams & {
195
+ * paymentQueues: MapStore<Brand, Payment[]>;
196
+ * offerToInvitationMakers: MapStore<
197
+ * string,
198
+ * import('./types.js').InvitationMakers
199
+ * >;
200
+ * offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>;
201
+ * offerToUsedInvitation: MapStore<string, Amount<'set'>>;
202
+ * purseBalances: MapStore<Purse, Amount>;
203
+ * updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>;
204
+ * currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<CurrentWalletRecord>;
205
+ * liveOffers: MapStore<OfferId, OfferStatus>;
206
+ * liveOfferSeats: MapStore<OfferId, UserSeat<unknown>>;
207
+ * liveOfferPayments: MapStore<OfferId, MapStore<Brand, Payment>>;
208
+ * }
209
+ * >} ImmutableState
210
+ *
211
+ *
212
+ * @typedef {BrandDescriptor & { purse: Purse }} PurseRecord
213
+ *
214
+ * @typedef {{}} MutableState
215
+ */
216
+
217
+ /**
218
+ * NameHub reverse-lookup, finding 0 or more names for a target value
219
+ *
220
+ * TODO: consider moving to nameHub.js?
221
+ *
222
+ * @param {unknown} target - passable Key
223
+ * @param {ERef<import('@agoric/vats').NameHub>} nameHub
224
+ */
225
+ const namesOf = async (target, nameHub) => {
226
+ const entries = await E(nameHub).entries();
227
+ const matches = [];
228
+ for (const [name, candidate] of entries) {
229
+ if (candidate === target) {
230
+ matches.push(name);
231
+ }
232
+ }
233
+ return harden(matches);
234
+ };
235
+
236
+ /**
237
+ * Check that an issuer and its brand belong to each other.
238
+ *
239
+ * TODO: move to ERTP?
240
+ *
241
+ * @param {Issuer} issuer
242
+ * @param {Brand} brand
243
+ * @returns {Promise<boolean>} true iff the the brand and issuer match
148
244
  */
245
+ const checkMutual = (issuer, brand) =>
246
+ Promise.all([
247
+ E(issuer)
248
+ .getBrand()
249
+ .then(b => b === brand),
250
+ E(brand).isMyIssuer(issuer),
251
+ ]).then(checks => checks.every(Boolean));
252
+
253
+ export const BRAND_TO_PURSES_KEY = 'brandToPurses';
254
+
255
+ const getBrandToPurses = (walletPurses, key) => {
256
+ const brandToPurses = provideLazy(walletPurses, key, _k => {
257
+ /** @type {MapStore<Brand, PurseRecord[]>} */
258
+ const store = makeScalarBigMapStore('purses by brand', {
259
+ durable: true,
260
+ });
261
+ return store;
262
+ });
263
+ return brandToPurses;
264
+ };
149
265
 
150
266
  /**
151
267
  * @param {import('@agoric/vat-data').Baggage} baggage
152
268
  * @param {SharedParams} shared
153
269
  */
154
270
  export const prepareSmartWallet = (baggage, shared) => {
155
- const { registry: _, ...passableShared } = shared;
271
+ const { registry: _r, ...passableShared } = shared;
156
272
  mustMatch(
157
273
  harden(passableShared),
158
274
  harden({
@@ -162,13 +278,82 @@ export const prepareSmartWallet = (baggage, shared) => {
162
278
  invitationDisplayInfo: DisplayInfoShape,
163
279
  publicMarshaller: M.remotable('Marshaller'),
164
280
  zoe: M.eref(M.remotable('ZoeService')),
281
+
282
+ // known only to smartWallets and walletFactory, this allows the
283
+ // walletFactory to invoke functions on the self facet that no one else
284
+ // can. Used to protect the upgrade-to-incarnation 2 repair. This can be
285
+ // dropped once the repair has taken place.
286
+ secretWalletFactoryKey: M.any(),
165
287
  }),
166
288
  );
167
289
 
168
290
  const makeRecorderKit = prepareRecorderKit(baggage, shared.publicMarshaller);
169
291
 
292
+ const walletPurses = provide(baggage, BRAND_TO_PURSES_KEY, () => {
293
+ trace('make purses by wallet and save in baggage at', BRAND_TO_PURSES_KEY);
294
+ /** @type {WeakMapStore<unknown, MapStore<Brand, PurseRecord[]>>} */
295
+ const store = makeScalarBigWeakMapStore('purses by wallet', {
296
+ durable: true,
297
+ });
298
+ return store;
299
+ });
300
+
301
+ const makeOfferWatcher = prepareOfferWatcher(baggage);
302
+
303
+ const updateShape = {
304
+ value: AmountShape,
305
+ updateCount: M.bigint(),
306
+ };
307
+
308
+ const NotifierShape = M.remotable();
309
+ const amountWatcherGuard = M.interface('paymentWatcher', {
310
+ onFulfilled: M.call(updateShape, NotifierShape).returns(),
311
+ onRejected: M.call(M.any(), NotifierShape).returns(M.promise()),
312
+ });
313
+
314
+ const prepareAmountWatcher = () =>
315
+ prepareExoClass(
316
+ baggage,
317
+ 'AmountWatcher',
318
+ amountWatcherGuard,
319
+ /**
320
+ * @param {Purse} purse
321
+ * @param {ReturnType<makeWalletWithResolvedStorageNodes>['helper']} helper
322
+ */
323
+ (purse, helper) => ({ purse, helper }),
324
+ {
325
+ /**
326
+ * @param {{ value: Amount; updateCount: bigint | undefined }} updateRecord
327
+ * @param {Notifier<Amount>} notifier
328
+ * @returns {void}
329
+ */
330
+ onFulfilled(updateRecord, notifier) {
331
+ const { helper, purse } = this.state;
332
+ helper.updateBalance(purse, updateRecord.value);
333
+ helper.watchNextBalance(
334
+ this.self,
335
+ notifier,
336
+ updateRecord.updateCount,
337
+ );
338
+ },
339
+ /**
340
+ * @param {unknown} err
341
+ * @returns {Promise<void>}
342
+ */
343
+ onRejected(err) {
344
+ const { helper, purse } = this.state;
345
+ if (isUpgradeDisconnection(err)) {
346
+ return helper.watchPurse(purse); // retry
347
+ }
348
+ helper.logWalletError(`failed amount observer`, err);
349
+ throw err;
350
+ },
351
+ },
352
+ );
353
+
354
+ const makeAmountWatcher = prepareAmountWatcher();
355
+
170
356
  /**
171
- *
172
357
  * @param {UniqueParams} unique
173
358
  * @returns {State}
174
359
  */
@@ -232,6 +417,9 @@ export const prepareSmartWallet = (baggage, shared) => {
232
417
  liveOfferSeats: makeScalarBigMapStore('live offer seats', {
233
418
  durable: true,
234
419
  }),
420
+ liveOfferPayments: makeScalarBigMapStore('live offer payments', {
421
+ durable: true,
422
+ }),
235
423
  };
236
424
 
237
425
  return {
@@ -245,12 +433,39 @@ export const prepareSmartWallet = (baggage, shared) => {
245
433
  helper: M.interface('helperFacetI', {
246
434
  assertUniqueOfferId: M.call(M.string()).returns(),
247
435
  updateBalance: M.call(PurseShape, AmountShape).optional('init').returns(),
436
+ getPurseIfKnownBrand: M.call(BrandShape)
437
+ .optional(M.eref(M.remotable()))
438
+ .returns(M.promise()),
248
439
  publishCurrentState: M.call().returns(),
249
440
  watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()),
441
+ watchNextBalance: M.call(M.any(), NotifierShape, M.bigint()).returns(),
442
+ repairUnwatchedSeats: M.call().returns(M.promise()),
443
+ repairUnwatchedPurses: M.call().returns(M.promise()),
444
+ updateStatus: M.call(M.any()).returns(),
445
+ addContinuingOffer: M.call(
446
+ M.or(M.number(), M.string()),
447
+ AmountShape,
448
+ M.remotable('InvitationMaker'),
449
+ M.or(M.record(), M.undefined()),
450
+ ).returns(M.promise()),
451
+ purseForBrand: M.call(BrandShape).returns(M.promise()),
452
+ logWalletInfo: M.call().rest(M.arrayOf(M.any())).returns(),
453
+ logWalletError: M.call().rest(M.arrayOf(M.any())).returns(),
454
+ getLiveOfferPayments: M.call().returns(M.remotable('mapStore')),
250
455
  }),
456
+
251
457
  deposit: M.interface('depositFacetI', {
252
458
  receive: M.callWhen(M.await(M.eref(PaymentShape))).returns(AmountShape),
253
459
  }),
460
+ payments: M.interface('payments support', {
461
+ withdrawGive: M.call(
462
+ AmountKeywordRecordShape,
463
+ M.or(M.number(), M.string()),
464
+ ).returns(PaymentPKeywordRecordShape),
465
+ tryReclaimingWithdrawnPayments: M.call(
466
+ M.or(M.number(), M.string()),
467
+ ).returns(M.promise()),
468
+ }),
254
469
  offers: M.interface('offers facet', {
255
470
  executeOffer: M.call(shape.OfferSpec).returns(M.promise()),
256
471
  tryExitOffer: M.call(M.scalar()).returns(M.promise()),
@@ -264,13 +479,16 @@ export const prepareSmartWallet = (baggage, shared) => {
264
479
  getCurrentSubscriber: M.call().returns(SubscriberShape),
265
480
  getUpdatesSubscriber: M.call().returns(SubscriberShape),
266
481
  getPublicTopics: M.call().returns(TopicsRecordShape),
482
+ repairWalletForIncarnation2: M.call(M.any()).returns(),
267
483
  }),
268
484
  };
269
485
 
486
+ // TODO move to top level so its type can be exported
270
487
  /**
271
- * Make the durable object to return, but taking some parameters that are awaited by a wrapping function.
272
- * This is necessary because the class kit construction helpers, `initState` and `finish` run synchronously
273
- * and the child storage node must be awaited until we have durable promises.
488
+ * Make the durable object to return, but taking some parameters that are
489
+ * awaited by a wrapping function. This is necessary because the class kit
490
+ * construction helpers, `initState` and `finish` run synchronously and the
491
+ * child storage node must be awaited until we have durable promises.
274
492
  */
275
493
  const makeWalletWithResolvedStorageNodes = prepareExoClassKit(
276
494
  baggage,
@@ -287,6 +505,7 @@ export const prepareSmartWallet = (baggage, shared) => {
287
505
  * @type {(id: string) => void}
288
506
  */
289
507
  assertUniqueOfferId(id) {
508
+ const { facets } = this;
290
509
  const {
291
510
  liveOffers,
292
511
  liveOfferSeats,
@@ -297,13 +516,14 @@ export const prepareSmartWallet = (baggage, shared) => {
297
516
  const used =
298
517
  liveOffers.has(id) ||
299
518
  liveOfferSeats.has(id) ||
519
+ facets.helper.getLiveOfferPayments().has(id) ||
300
520
  offerToInvitationMakers.has(id) ||
301
521
  offerToPublicSubscriberPaths.has(id) ||
302
522
  offerToUsedInvitation.has(id);
303
523
  !used || Fail`cannot re-use offer id ${id}`;
304
524
  },
305
525
  /**
306
- * @param {RemotePurse} purse
526
+ * @param {Purse} purse
307
527
  * @param {Amount<any>} balance
308
528
  */
309
529
  updateBalance(purse, balance) {
@@ -342,36 +562,286 @@ export const prepareSmartWallet = (baggage, shared) => {
342
562
  });
343
563
  },
344
564
 
345
- /** @type {(purse: ERef<RemotePurse>) => Promise<void>} */
565
+ /** @type {(purse: ERef<Purse>) => Promise<void>} */
346
566
  async watchPurse(purseRef) {
347
- const { address } = this.state;
567
+ const { helper } = this.facets;
568
+
569
+ // This would seem to fit the observeNotifier() pattern,
570
+ // but purse notifiers are not necessarily durable.
571
+ // If there is an error due to upgrade, retry watchPurse().
348
572
 
349
573
  const purse = await purseRef; // promises don't fit in durable storage
574
+ const handler = makeAmountWatcher(purse, helper);
350
575
 
351
- const { helper } = this.facets;
352
576
  // publish purse's balance and changes
353
- void E.when(
354
- E(purse).getCurrentAmount(),
355
- balance => helper.updateBalance(purse, balance),
356
- err =>
357
- console.error(
358
- address,
359
- 'initial purse balance publish failed',
360
- err,
361
- ),
577
+ const notifier = await E(purse).getCurrentAmountNotifier();
578
+ const startP = E(notifier).getUpdateSince(undefined);
579
+ watchPromise(startP, handler, notifier);
580
+ },
581
+
582
+ watchNextBalance(handler, notifier, updateCount) {
583
+ const nextP = E(notifier).getUpdateSince(updateCount);
584
+ watchPromise(nextP, handler, notifier);
585
+ },
586
+
587
+ /**
588
+ * Provide a purse given a NameHub of issuers and their brands.
589
+ *
590
+ * We currently support only one NameHub, agoricNames, and hence one
591
+ * purse per brand. But we store an array of them to facilitate a
592
+ * transition to decentralized introductions.
593
+ *
594
+ * @param {Brand} brand
595
+ * @param {ERef<import('@agoric/vats').NameHub>} known - namehub with
596
+ * brand, issuer branches
597
+ * @returns {Promise<Purse | undefined>} undefined if brand is not known
598
+ */
599
+ async getPurseIfKnownBrand(brand, known) {
600
+ const { helper, self } = this.facets;
601
+ const brandToPurses = getBrandToPurses(walletPurses, self);
602
+
603
+ if (brandToPurses.has(brand)) {
604
+ const purses = brandToPurses.get(brand);
605
+ if (purses.length > 0) {
606
+ // UNTIL https://github.com/Agoric/agoric-sdk/issues/6126
607
+ // multiple purses
608
+ return purses[0].purse;
609
+ }
610
+ }
611
+
612
+ const found = await namesOf(brand, E(known).lookup('brand'));
613
+ if (found.length === 0) {
614
+ return undefined;
615
+ }
616
+ const [edgeName] = found;
617
+ const issuer = await E(known).lookup('issuer', edgeName);
618
+
619
+ // Even though we rely on this nameHub, double-check
620
+ // that the issuer and the brand belong to each other.
621
+ if (!(await checkMutual(issuer, brand))) {
622
+ // if they don't, it's not a "known" brand in a coherent way
623
+ return undefined;
624
+ }
625
+
626
+ // Accept the issuer; rely on it in future offers.
627
+ const [displayInfo, purse] = await Promise.all([
628
+ E(issuer).getDisplayInfo(),
629
+ E(issuer).makeEmptyPurse(),
630
+ ]);
631
+
632
+ // adopt edgeName as petname
633
+ // NOTE: for decentralized introductions, qualify edgename by nameHub petname
634
+ const petname = edgeName;
635
+ const assetInfo = { petname, brand, issuer, purse, displayInfo };
636
+ appendToStoredArray(brandToPurses, brand, assetInfo);
637
+ // NOTE: when we decentralize introduction of issuers,
638
+ // process queued payments for this brand.
639
+
640
+ void helper.watchPurse(purse);
641
+ return purse;
642
+ },
643
+
644
+ /**
645
+ * see https://github.com/Agoric/agoric-sdk/issues/8445 and
646
+ * https://github.com/Agoric/agoric-sdk/issues/8286. As originally
647
+ * released, the smartWallet didn't durably monitor the promises for the
648
+ * outcomes of offers, and would have dropped them on upgrade of Zoe or
649
+ * the smartWallet itself. Using watchedPromises, (see offerWatcher.js)
650
+ * we've addressed the problem for new offers. This function will
651
+ * backfill the solution for offers that were outstanding before the
652
+ * transition to incarnation 2 of the smartWallet.
653
+ */
654
+ async repairUnwatchedSeats() {
655
+ const { state, facets } = this;
656
+ const { address, invitationPurse, liveOffers, liveOfferSeats } =
657
+ state;
658
+ const { zoe, agoricNames, invitationBrand, invitationIssuer } =
659
+ shared;
660
+
661
+ const invitationFromSpec = makeInvitationsHelper(
662
+ zoe,
663
+ agoricNames,
664
+ invitationBrand,
665
+ invitationPurse,
666
+ state.offerToInvitationMakers.get,
362
667
  );
363
- void observeNotifier(E(purse).getCurrentAmountNotifier(), {
364
- updateState(balance) {
365
- helper.updateBalance(purse, balance);
366
- },
367
- fail(reason) {
368
- console.error(address, `failed updateState observer`, reason);
369
- },
668
+
669
+ const watcherPromises = [];
670
+ for (const seatId of liveOfferSeats.keys()) {
671
+ facets.helper.logWalletInfo(`repairing ${seatId}`);
672
+ const offerSpec = liveOffers.get(seatId);
673
+ const seat = liveOfferSeats.get(seatId);
674
+
675
+ const watchOutcome = (async () => {
676
+ await null;
677
+ let invitationAmount = state.offerToUsedInvitation.has(
678
+ // @ts-expect-error older type allowed number
679
+ offerSpec.id,
680
+ )
681
+ ? state.offerToUsedInvitation.get(
682
+ // @ts-expect-error older type allowed number
683
+ offerSpec.id,
684
+ )
685
+ : undefined;
686
+ if (invitationAmount) {
687
+ facets.helper.logWalletInfo(
688
+ 'recovered invitation amount for offer',
689
+ offerSpec.id,
690
+ );
691
+ } else {
692
+ facets.helper.logWalletInfo(
693
+ 'inferring invitation amount for offer',
694
+ offerSpec.id,
695
+ );
696
+ const tempInvitation = invitationFromSpec(
697
+ offerSpec.invitationSpec,
698
+ );
699
+ invitationAmount =
700
+ await E(invitationIssuer).getAmountOf(tempInvitation);
701
+ void E(invitationIssuer).burn(tempInvitation);
702
+ }
703
+
704
+ const watcher = makeOfferWatcher(
705
+ facets.helper,
706
+ facets.deposit,
707
+ offerSpec,
708
+ address,
709
+ invitationAmount,
710
+ seat,
711
+ );
712
+ return watchOfferOutcomes(watcher, seat);
713
+ })();
714
+ trace(`Repaired seat ${seatId} for wallet ${address}`);
715
+ watcherPromises.push(watchOutcome);
716
+ }
717
+
718
+ await Promise.all(watcherPromises);
719
+ },
720
+ async repairUnwatchedPurses() {
721
+ const { state, facets } = this;
722
+ const { helper, self } = facets;
723
+ const { invitationPurse, address } = state;
724
+
725
+ const brandToPurses = getBrandToPurses(walletPurses, self);
726
+ trace(`Found ${brandToPurses.values()} purse(s) for ${address}`);
727
+ for (const purses of brandToPurses.values()) {
728
+ for (const record of purses) {
729
+ void helper.watchPurse(record.purse);
730
+ trace(`Repaired purse ${record.petname} of ${address}`);
731
+ }
732
+ }
733
+
734
+ void helper.watchPurse(invitationPurse);
735
+ },
736
+
737
+ /** @param {OfferStatus} offerStatus */
738
+ updateStatus(offerStatus) {
739
+ const { state, facets } = this;
740
+ facets.helper.logWalletInfo('offerStatus', offerStatus);
741
+
742
+ void state.updateRecorderKit.recorder.write({
743
+ updated: 'offerStatus',
744
+ status: offerStatus,
370
745
  });
746
+
747
+ if ('numWantsSatisfied' in offerStatus) {
748
+ if (state.liveOfferSeats.has(offerStatus.id)) {
749
+ state.liveOfferSeats.delete(offerStatus.id);
750
+ }
751
+
752
+ if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
753
+ facets.helper.getLiveOfferPayments().delete(offerStatus.id);
754
+ }
755
+
756
+ if (state.liveOffers.has(offerStatus.id)) {
757
+ state.liveOffers.delete(offerStatus.id);
758
+ // This might get skipped in subsequent passes, since we .delete()
759
+ // the first time through
760
+ facets.helper.publishCurrentState();
761
+ }
762
+ }
763
+ },
764
+
765
+ /**
766
+ * @param {string} offerId
767
+ * @param {Amount<'set'>} invitationAmount
768
+ * @param {import('./types.js').InvitationMakers} invitationMakers
769
+ * @param {import('./types.js').PublicSubscribers} publicSubscribers
770
+ */
771
+ async addContinuingOffer(
772
+ offerId,
773
+ invitationAmount,
774
+ invitationMakers,
775
+ publicSubscribers,
776
+ ) {
777
+ const { state, facets } = this;
778
+
779
+ state.offerToUsedInvitation.init(offerId, invitationAmount);
780
+ state.offerToInvitationMakers.init(offerId, invitationMakers);
781
+ const pathMap = await objectMapStoragePath(publicSubscribers);
782
+ if (pathMap) {
783
+ facets.helper.logWalletInfo('recording pathMap', pathMap);
784
+ state.offerToPublicSubscriberPaths.init(offerId, pathMap);
785
+ }
786
+ facets.helper.publishCurrentState();
787
+ },
788
+
789
+ /**
790
+ * @param {Brand} brand
791
+ * @returns {Promise<Purse>}
792
+ */
793
+ async purseForBrand(brand) {
794
+ const { state, facets } = this;
795
+ const { registry, invitationBrand } = shared;
796
+
797
+ if (registry.has(brand)) {
798
+ return E(state.bank).getPurse(brand);
799
+ } else if (invitationBrand === brand) {
800
+ return state.invitationPurse;
801
+ }
802
+
803
+ const purse = await facets.helper.getPurseIfKnownBrand(
804
+ brand,
805
+ shared.agoricNames,
806
+ );
807
+ if (purse) {
808
+ return purse;
809
+ }
810
+ throw Fail`cannot find/make purse for ${brand}`;
811
+ },
812
+ logWalletInfo(...args) {
813
+ const { state } = this;
814
+ console.info('wallet', state.address, ...args);
815
+ },
816
+ logWalletError(...args) {
817
+ const { state } = this;
818
+ console.error('wallet', state.address, ...args);
819
+ },
820
+ // In new SmartWallets, this is part of state, but we can't add fields
821
+ // to instance state for older SmartWallets, so put it in baggage.
822
+ getLiveOfferPayments() {
823
+ const { state } = this;
824
+
825
+ if (state.liveOfferPayments) {
826
+ return state.liveOfferPayments;
827
+ }
828
+
829
+ // This will only happen for legacy wallets, before WF incarnation 2
830
+ if (!baggage.has(state.address)) {
831
+ trace(`getLiveOfferPayments adding store for ${state.address}`);
832
+ baggage.init(
833
+ state.address,
834
+ makeScalarBigMapStore('live offer payments', {
835
+ durable: true,
836
+ }),
837
+ );
838
+ }
839
+ return baggage.get(state.address);
371
840
  },
372
841
  },
373
842
  /**
374
- * Similar to {DepositFacet} but async because it has to look up the purse.
843
+ * Similar to {DepositFacet} but async because it has to look up the
844
+ * purse.
375
845
  */
376
846
  deposit: {
377
847
  /**
@@ -379,159 +849,288 @@ export const prepareSmartWallet = (baggage, shared) => {
379
849
  *
380
850
  * If the purse doesn't exist, we hold the payment in durable storage.
381
851
  *
382
- * @param {import('@endo/far').FarRef<Payment>} payment
383
- * @returns {Promise<Amount>} amounts for deferred deposits will be empty
852
+ * @param {Payment} payment
853
+ * @returns {Promise<Amount>}
854
+ * @throws if there's not yet a purse, though the payment is held to try
855
+ * again when there is
384
856
  */
385
857
  async receive(payment) {
386
- const { paymentQueues: queues, bank, invitationPurse } = this.state;
858
+ const {
859
+ state,
860
+ facets: { helper },
861
+ } = this;
862
+ const { paymentQueues: queues, bank, invitationPurse } = state;
387
863
  const { registry, invitationBrand } = shared;
864
+
388
865
  const brand = await E(payment).getAllegedBrand();
389
866
 
390
867
  // When there is a purse deposit into it
391
868
  if (registry.has(brand)) {
392
869
  const purse = E(bank).getPurse(brand);
393
- // @ts-expect-error deposit does take a FarRef<Payment>
870
+ // @ts-expect-error narrow assetKind to 'nat'
394
871
  return E(purse).deposit(payment);
395
872
  } else if (invitationBrand === brand) {
396
- // @ts-expect-error deposit does take a FarRef<Payment>
873
+ // @ts-expect-error narrow assetKind to 'set'
397
874
  return E(invitationPurse).deposit(payment);
398
875
  }
399
876
 
877
+ const purse = await helper.getPurseIfKnownBrand(
878
+ brand,
879
+ shared.agoricNames,
880
+ );
881
+ if (purse) {
882
+ return E(purse).deposit(payment);
883
+ }
884
+
400
885
  // When there is no purse, save the payment into a queue.
401
886
  // It's not yet ever read but a future version of the contract can
402
887
  appendToStoredArray(queues, brand, payment);
403
- return AmountMath.makeEmpty(brand);
888
+ throw Fail`cannot deposit payment with brand ${brand}: no purse`;
404
889
  },
405
890
  },
891
+
892
+ payments: {
893
+ /**
894
+ * Withdraw the offered amount from the appropriate purse of this
895
+ * wallet.
896
+ *
897
+ * Save its amount in liveOfferPayments in case we need to reclaim the
898
+ * payment.
899
+ *
900
+ * @param {AmountKeywordRecord} give
901
+ * @param {OfferId} offerId
902
+ * @returns {PaymentPKeywordRecord}
903
+ */
904
+ withdrawGive(give, offerId) {
905
+ const { facets } = this;
906
+
907
+ /** @type {MapStore<Brand, Payment>} */
908
+ const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
909
+ durable: true,
910
+ });
911
+ facets.helper
912
+ .getLiveOfferPayments()
913
+ .init(offerId, brandPaymentRecord);
914
+
915
+ // Add each payment amount to brandPaymentRecord as it is withdrawn. If
916
+ // there's an error later, we can use it to redeposit the correct amount.
917
+ return objectMap(give, amount => {
918
+ /** @type {Promise<Purse>} */
919
+ const purseP = facets.helper.purseForBrand(amount.brand);
920
+ const paymentP = E(purseP).withdraw(amount);
921
+ void E.when(
922
+ paymentP,
923
+ payment => brandPaymentRecord.init(amount.brand, payment),
924
+ e => {
925
+ // recovery will be handled by tryReclaimingWithdrawnPayments()
926
+ facets.helper.logWalletInfo(
927
+ `⚠️ Payment withdrawal failed.`,
928
+ offerId,
929
+ e,
930
+ );
931
+ },
932
+ );
933
+ return paymentP;
934
+ });
935
+ },
936
+
937
+ /**
938
+ * Find the live payments for the offer and deposit them back in the
939
+ * appropriate purses.
940
+ *
941
+ * @param {OfferId} offerId
942
+ * @returns {Promise<Amount[]>}
943
+ */
944
+ async tryReclaimingWithdrawnPayments(offerId) {
945
+ const { facets } = this;
946
+
947
+ await null;
948
+
949
+ const liveOfferPayments = facets.helper.getLiveOfferPayments();
950
+ if (liveOfferPayments.has(offerId)) {
951
+ const brandPaymentRecord = liveOfferPayments.get(offerId);
952
+ if (!brandPaymentRecord) {
953
+ return [];
954
+ }
955
+ const out = [];
956
+ // Use allSettled to ensure we attempt all the deposits, regardless of
957
+ // individual rejections.
958
+ await Promise.allSettled(
959
+ Array.from(brandPaymentRecord.entries()).map(([b, p]) => {
960
+ // Wait for the withdrawal to complete. This protects against a
961
+ // race when updating paymentToPurse.
962
+ const purseP = facets.helper.purseForBrand(b);
963
+
964
+ // Now send it back to the purse.
965
+ return E(purseP)
966
+ .deposit(p)
967
+ .then(amt => {
968
+ out.push(amt);
969
+ });
970
+ }),
971
+ );
972
+ return harden(out);
973
+ }
974
+ return [];
975
+ },
976
+ },
977
+
406
978
  offers: {
407
979
  /**
408
- * Take an offer description provided in capData, augment it with payments and call zoe.offer()
980
+ * Take an offer description provided in capData, augment it with
981
+ * payments and call zoe.offer()
409
982
  *
410
- * @param {import('./offers.js').OfferSpec} offerSpec
411
- * @returns {Promise<void>} after the offer has been both seated and exited by Zoe.
412
- * @throws if any parts of the offer can be determined synchronously to be invalid
983
+ * @param {OfferSpec} offerSpec
984
+ * @returns {Promise<void>} after the offer has been both seated and
985
+ * exited by Zoe.
986
+ * @throws if any parts of the offer can be determined synchronously to
987
+ * be invalid
413
988
  */
414
989
  async executeOffer(offerSpec) {
415
990
  const { facets, state } = this;
416
- const {
417
- address,
418
- bank,
419
- invitationPurse,
420
- offerToInvitationMakers,
421
- offerToUsedInvitation,
422
- offerToPublicSubscriberPaths,
423
- updateRecorderKit,
424
- } = this.state;
425
- const { invitationBrand, zoe, invitationIssuer, registry } = shared;
991
+ const { address, invitationPurse } = state;
992
+ const { zoe, agoricNames } = shared;
993
+ const { invitationBrand, invitationIssuer } = shared;
426
994
 
427
995
  facets.helper.assertUniqueOfferId(String(offerSpec.id));
428
996
 
429
- const logger = {
430
- info: (...args) => console.info('wallet', address, ...args),
431
- error: (...args) => console.error('wallet', address, ...args),
432
- };
997
+ await null;
433
998
 
434
- const executor = makeOfferExecutor({
435
- zoe,
436
- depositFacet: facets.deposit,
437
- invitationIssuer,
438
- powers: {
439
- invitationFromSpec: makeInvitationsHelper(
440
- zoe,
441
- shared.agoricNames,
442
- invitationBrand,
443
- invitationPurse,
444
- offerToInvitationMakers.get,
445
- ),
446
- /**
447
- * @param {Brand} brand
448
- * @returns {Promise<RemotePurse>}
449
- */
450
- purseForBrand: async brand => {
451
- if (registry.has(brand)) {
452
- // @ts-expect-error RemotePurse cast
453
- return E(bank).getPurse(brand);
454
- } else if (invitationBrand === brand) {
455
- // @ts-expect-error RemotePurse cast
456
- return invitationPurse;
457
- }
458
- throw Fail`cannot find/make purse for ${brand}`;
459
- },
460
- logger,
461
- },
462
- onStatusChange: offerStatus => {
463
- logger.info('offerStatus', offerStatus);
999
+ /** @type {UserSeat} */
1000
+ let seatRef;
1001
+ let watcher;
1002
+ try {
1003
+ const invitationFromSpec = makeInvitationsHelper(
1004
+ zoe,
1005
+ agoricNames,
1006
+ invitationBrand,
1007
+ invitationPurse,
1008
+ state.offerToInvitationMakers.get,
1009
+ );
464
1010
 
465
- void updateRecorderKit.recorder.write({
466
- updated: 'offerStatus',
467
- status: offerStatus,
468
- });
1011
+ facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
469
1012
 
470
- const isSeatExited = 'numWantsSatisfied' in offerStatus;
471
- if (isSeatExited) {
472
- if (state.liveOfferSeats.has(offerStatus.id)) {
473
- state.liveOfferSeats.delete(offerStatus.id);
474
- }
1013
+ // 1. Prepare values and validate synchronously.
1014
+ const { proposal } = offerSpec;
475
1015
 
476
- if (state.liveOffers.has(offerStatus.id)) {
477
- state.liveOffers.delete(offerStatus.id);
478
- facets.helper.publishCurrentState();
479
- }
480
- }
481
- },
482
- /** @type {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').RemoteInvitationMakers, publicSubscribers?: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord) => Promise<void>} */
483
- onNewContinuingOffer: async (
484
- offerId,
1016
+ const invitation = invitationFromSpec(offerSpec.invitationSpec);
1017
+
1018
+ const invitationAmount =
1019
+ await E(invitationIssuer).getAmountOf(invitation);
1020
+
1021
+ // 2. Begin executing offer
1022
+ // No explicit signal to user that we reached here but if anything above
1023
+ // failed they'd get an 'error' status update.
1024
+
1025
+ const withdrawnPayments =
1026
+ proposal?.give &&
1027
+ (await deeplyFulfilledObject(
1028
+ facets.payments.withdrawGive(proposal.give, offerSpec.id),
1029
+ ));
1030
+
1031
+ seatRef = await E(zoe).offer(
1032
+ invitation,
1033
+ proposal,
1034
+ withdrawnPayments,
1035
+ offerSpec.offerArgs,
1036
+ );
1037
+ facets.helper.logWalletInfo(offerSpec.id, 'seated');
1038
+
1039
+ watcher = makeOfferWatcher(
1040
+ facets.helper,
1041
+ facets.deposit,
1042
+ offerSpec,
1043
+ address,
485
1044
  invitationAmount,
486
- invitationMakers,
487
- publicSubscribers,
488
- ) => {
489
- offerToUsedInvitation.init(offerId, invitationAmount);
490
- offerToInvitationMakers.init(offerId, invitationMakers);
491
- const pathMap = await objectMapStoragePath(publicSubscribers);
492
- if (pathMap) {
493
- logger.info('recording pathMap', pathMap);
494
- offerToPublicSubscriberPaths.init(offerId, pathMap);
495
- }
496
- facets.helper.publishCurrentState();
497
- },
498
- });
1045
+ seatRef,
1046
+ );
499
1047
 
500
- return executor.executeOffer(offerSpec, seatRef => {
501
1048
  state.liveOffers.init(offerSpec.id, offerSpec);
502
- facets.helper.publishCurrentState();
503
1049
  state.liveOfferSeats.init(offerSpec.id, seatRef);
504
- });
1050
+
1051
+ // publish the live offers
1052
+ facets.helper.publishCurrentState();
1053
+
1054
+ // await so that any errors are caught and handled below
1055
+ await watchOfferOutcomes(watcher, seatRef);
1056
+ } catch (reason) {
1057
+ // This block only runs if the block above fails during one vat incarnation.
1058
+ facets.helper.logWalletError('IMMEDIATE OFFER ERROR:', reason);
1059
+
1060
+ // Update status to observers
1061
+ if (isUpgradeDisconnection(reason)) {
1062
+ // The offer watchers will reconnect. Don't reclaim or exit
1063
+ return;
1064
+ } else if (watcher) {
1065
+ // The watcher's onRejected will updateStatus()
1066
+ } else {
1067
+ facets.helper.updateStatus({
1068
+ error: reason.toString(),
1069
+ ...offerSpec,
1070
+ });
1071
+ }
1072
+
1073
+ // Backstop recovery, in case something very basic fails.
1074
+ if (offerSpec?.proposal?.give) {
1075
+ facets.payments
1076
+ .tryReclaimingWithdrawnPayments(offerSpec.id)
1077
+ .catch(e =>
1078
+ facets.helper.logWalletError(
1079
+ 'recovery failed reclaiming payments',
1080
+ e,
1081
+ ),
1082
+ );
1083
+ }
1084
+
1085
+ // XXX tests rely on throwing immediate errors, not covering the
1086
+ // error handling in the event the failure is after an upgrade
1087
+ throw reason;
1088
+ }
505
1089
  },
506
1090
  /**
507
1091
  * Take an offer's id, look up its seat, try to exit.
508
1092
  *
509
- * @param {import('./offers.js').OfferId} offerId
1093
+ * @param {OfferId} offerId
510
1094
  * @returns {Promise<void>}
511
1095
  * @throws if the seat can't be found or E(seatRef).tryExit() fails.
512
1096
  */
513
1097
  async tryExitOffer(offerId) {
1098
+ const { facets } = this;
1099
+ const amts = await facets.payments
1100
+ .tryReclaimingWithdrawnPayments(offerId)
1101
+ .catch(e => {
1102
+ facets.helper.logWalletError(
1103
+ 'recovery failed reclaiming payments',
1104
+ e,
1105
+ );
1106
+ return [];
1107
+ });
1108
+ if (amts.length > 0) {
1109
+ facets.helper.logWalletInfo('reclaimed', amts, 'from', offerId);
1110
+ }
514
1111
  const seatRef = this.state.liveOfferSeats.get(offerId);
515
1112
  await E(seatRef).tryExit();
516
1113
  },
517
1114
  },
518
1115
  self: {
519
1116
  /**
520
- * Umarshals the actionCapData and delegates to the appropriate action handler.
1117
+ * Umarshals the actionCapData and delegates to the appropriate action
1118
+ * handler.
521
1119
  *
522
- * @param {import('@endo/marshal').CapData<string>} actionCapData of type BridgeAction
523
- * @param {boolean} [canSpend=false]
1120
+ * @param {import('@endo/marshal').CapData<string | null>} actionCapData
1121
+ * of type BridgeAction
1122
+ * @param {boolean} [canSpend]
524
1123
  * @returns {Promise<void>}
525
1124
  */
526
1125
  handleBridgeAction(actionCapData, canSpend = false) {
1126
+ const { facets } = this;
1127
+ const { offers } = facets;
527
1128
  const { publicMarshaller } = shared;
528
1129
 
529
- const { offers } = this.facets;
530
-
531
1130
  /** @param {Error} err */
532
1131
  const recordError = err => {
533
- const { address, updateRecorderKit } = this.state;
534
- console.error('wallet', address, 'handleBridgeAction error:', err);
1132
+ const { updateRecorderKit } = this.state;
1133
+ facets.helper.logWalletError('handleBridgeAction error:', err);
535
1134
  void updateRecorderKit.recorder.write({
536
1135
  updated: 'walletAction',
537
1136
  status: { error: err.message },
@@ -576,14 +1175,18 @@ export const prepareSmartWallet = (baggage, shared) => {
576
1175
  },
577
1176
  /** @deprecated use getPublicTopics */
578
1177
  getCurrentSubscriber() {
579
- return this.state.currentRecorderKit.subscriber;
1178
+ const { state } = this;
1179
+ return state.currentRecorderKit.subscriber;
580
1180
  },
581
1181
  /** @deprecated use getPublicTopics */
582
1182
  getUpdatesSubscriber() {
583
- return this.state.updateRecorderKit.subscriber;
1183
+ const { state } = this;
1184
+ return state.updateRecorderKit.subscriber;
584
1185
  },
585
1186
  getPublicTopics() {
586
- const { currentRecorderKit, updateRecorderKit } = this.state;
1187
+ const { state } = this;
1188
+ const { currentRecorderKit, updateRecorderKit } = state;
1189
+
587
1190
  return harden({
588
1191
  current: {
589
1192
  description: 'Current state of wallet',
@@ -597,6 +1200,27 @@ export const prepareSmartWallet = (baggage, shared) => {
597
1200
  },
598
1201
  });
599
1202
  },
1203
+ // TODO remove this and repairUnwatchedSeats once the repair has taken place.
1204
+ /**
1205
+ * To be called once ever per wallet.
1206
+ *
1207
+ * @param {object} key
1208
+ */
1209
+ repairWalletForIncarnation2(key) {
1210
+ const { state, facets } = this;
1211
+
1212
+ if (key !== shared.secretWalletFactoryKey) {
1213
+ return;
1214
+ }
1215
+
1216
+ facets.helper.repairUnwatchedSeats().catch(e => {
1217
+ console.error('repairUnwatchedSeats rejection', e);
1218
+ });
1219
+ facets.helper.repairUnwatchedPurses().catch(e => {
1220
+ console.error('repairUnwatchedPurses rejection', e);
1221
+ });
1222
+ trace(`repaired wallet ${state.address}`);
1223
+ },
600
1224
  },
601
1225
  },
602
1226
  {
@@ -604,14 +1228,18 @@ export const prepareSmartWallet = (baggage, shared) => {
604
1228
  const { invitationPurse } = state;
605
1229
  const { helper } = facets;
606
1230
 
607
- // @ts-expect-error RemotePurse cast
608
1231
  void helper.watchPurse(invitationPurse);
609
1232
  },
610
1233
  },
611
1234
  );
612
1235
 
613
1236
  /**
614
- * @param {Omit<UniqueParams, 'currentStorageNode' | 'walletStorageNode'> & {walletStorageNode: ERef<StorageNode>}} uniqueWithoutChildNodes
1237
+ * @param {Omit<
1238
+ * UniqueParams,
1239
+ * 'currentStorageNode' | 'walletStorageNode'
1240
+ * > & {
1241
+ * walletStorageNode: ERef<StorageNode>;
1242
+ * }} uniqueWithoutChildNodes
615
1243
  */
616
1244
  const makeSmartWallet = async uniqueWithoutChildNodes => {
617
1245
  const [walletStorageNode, currentStorageNode] = await Promise.all([