@agoric/smart-wallet 0.5.4-other-dev-8f8782b.0 → 0.5.4-other-dev-3eb1a1d.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.
Files changed (41) hide show
  1. package/package.json +35 -26
  2. package/src/index.d.ts +2 -0
  3. package/src/index.d.ts.map +1 -0
  4. package/src/index.js +2 -0
  5. package/src/invitations.d.ts +14 -10
  6. package/src/invitations.d.ts.map +1 -1
  7. package/src/invitations.js +35 -32
  8. package/src/marshal-contexts.d.ts +44 -41
  9. package/src/marshal-contexts.d.ts.map +1 -1
  10. package/src/marshal-contexts.js +68 -61
  11. package/src/offerWatcher.d.ts +52 -0
  12. package/src/offerWatcher.d.ts.map +1 -0
  13. package/src/offerWatcher.js +329 -0
  14. package/src/offers.d.ts +7 -31
  15. package/src/offers.d.ts.map +1 -1
  16. package/src/offers.js +9 -183
  17. package/src/proposals/upgrade-wallet-factory2-proposal.d.ts +23 -0
  18. package/src/proposals/upgrade-wallet-factory2-proposal.d.ts.map +1 -0
  19. package/src/proposals/upgrade-wallet-factory2-proposal.js +60 -0
  20. package/src/proposals/upgrade-walletFactory-proposal.d.ts +1 -1
  21. package/src/proposals/upgrade-walletFactory-proposal.d.ts.map +1 -1
  22. package/src/proposals/upgrade-walletFactory-proposal.js +46 -23
  23. package/src/smartWallet.d.ts +101 -66
  24. package/src/smartWallet.d.ts.map +1 -1
  25. package/src/smartWallet.js +576 -216
  26. package/src/typeGuards.d.ts +1 -1
  27. package/src/types-index.d.ts +2 -0
  28. package/src/types-index.js +2 -0
  29. package/src/types.d.ts +35 -41
  30. package/src/types.d.ts.map +1 -0
  31. package/src/types.ts +90 -0
  32. package/src/utils.d.ts +17 -14
  33. package/src/utils.d.ts.map +1 -1
  34. package/src/utils.js +19 -6
  35. package/src/walletFactory.d.ts +24 -78
  36. package/src/walletFactory.d.ts.map +1 -1
  37. package/src/walletFactory.js +61 -37
  38. package/CHANGELOG.md +0 -180
  39. package/src/payments.d.ts +0 -20
  40. package/src/payments.d.ts.map +0 -1
  41. package/src/payments.js +0 -89
@@ -1,5 +1,5 @@
1
- // backported types are out of sync
2
- // @ts-nocheck
1
+ import { Fail, q } from '@endo/errors';
2
+ import { E } from '@endo/far';
3
3
  import {
4
4
  AmountShape,
5
5
  BrandShape,
@@ -8,8 +8,13 @@ import {
8
8
  PaymentShape,
9
9
  PurseShape,
10
10
  } from '@agoric/ertp';
11
- import { StorageNodeShape, makeTracer } from '@agoric/internal';
12
- import { observeNotifier } from '@agoric/notifier';
11
+ import {
12
+ deeplyFulfilledObject,
13
+ makeTracer,
14
+ objectMap,
15
+ StorageNodeShape,
16
+ } from '@agoric/internal';
17
+ import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';
13
18
  import { M, mustMatch } from '@agoric/store';
14
19
  import {
15
20
  appendToStoredArray,
@@ -18,145 +23,195 @@ import {
18
23
  import {
19
24
  makeScalarBigMapStore,
20
25
  makeScalarBigWeakMapStore,
26
+ prepareExoClass,
21
27
  prepareExoClassKit,
22
28
  provide,
29
+ watchPromise,
23
30
  } from '@agoric/vat-data';
24
31
  import {
32
+ prepareRecorderKit,
25
33
  SubscriberShape,
26
34
  TopicsRecordShape,
27
- prepareRecorderKit,
28
35
  } from '@agoric/zoe/src/contractSupport/index.js';
29
- import { E } from '@endo/far';
36
+ import {
37
+ AmountKeywordRecordShape,
38
+ PaymentPKeywordRecordShape,
39
+ } from '@agoric/zoe/src/typeGuards.js';
40
+ import { prepareVowTools } from '@agoric/vow';
41
+ import { makeDurableZone } from '@agoric/zone/durable.js';
42
+
30
43
  import { makeInvitationsHelper } from './invitations.js';
31
- import { makeOfferExecutor } from './offers.js';
32
44
  import { shape } from './typeGuards.js';
33
45
  import { objectMapStoragePath } from './utils.js';
46
+ import { prepareOfferWatcher, makeWatchOfferOutcomes } from './offerWatcher.js';
34
47
 
35
- const { Fail, quote: q } = assert;
48
+ /** @import {OfferId, OfferStatus} from './offers.js'; */
36
49
 
37
50
  const trace = makeTracer('SmrtWlt');
38
51
 
39
52
  /**
40
53
  * @file Smart wallet module
41
- *
42
- * @see {@link ../README.md}}
54
+ * @see {@link ../README.md} }
55
+ */
56
+
57
+ /** @typedef {number | string} OfferId */
58
+
59
+ /**
60
+ * @typedef {{
61
+ * id: OfferId;
62
+ * invitationSpec: import('./invitations').InvitationSpec;
63
+ * proposal: Proposal;
64
+ * offerArgs?: any;
65
+ * }} OfferSpec
66
+ */
67
+
68
+ /**
69
+ * @typedef {{
70
+ * logger: {
71
+ * info: (...args: any[]) => void;
72
+ * error: (...args: any[]) => void;
73
+ * };
74
+ * makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher;
75
+ * invitationFromSpec: ERef<Invitation>;
76
+ * }} ExecutorPowers
43
77
  */
44
78
 
45
79
  /**
46
80
  * @typedef {{
47
- * method: 'executeOffer'
48
- * offer: import('./offers.js').OfferSpec,
81
+ * method: 'executeOffer';
82
+ * offer: OfferSpec;
49
83
  * }} ExecuteOfferAction
50
84
  */
51
85
 
52
86
  /**
53
87
  * @typedef {{
54
- * method: 'tryExitOffer'
55
- * offerId: import('./offers.js').OfferId,
88
+ * method: 'tryExitOffer';
89
+ * offerId: OfferId;
56
90
  * }} TryExitOfferAction
57
91
  */
58
92
 
59
93
  // Discriminated union. Possible future messages types:
60
94
  // maybe suggestIssuer for https://github.com/Agoric/agoric-sdk/issues/6132
61
95
  // setting petnames and adding brands for https://github.com/Agoric/agoric-sdk/issues/6126
62
- /**
63
- * @typedef { ExecuteOfferAction | TryExitOfferAction } BridgeAction
64
- */
96
+ /** @typedef {ExecuteOfferAction | TryExitOfferAction} BridgeAction */
65
97
 
66
98
  /**
67
- * Purses is an array to support a future requirement of multiple purses per brand.
99
+ * Purses is an array to support a future requirement of multiple purses per
100
+ * brand.
68
101
  *
69
- * Each map is encoded as an array of entries because a Map doesn't serialize directly.
70
- * We also considered having a vstorage key for each offer but for now are sticking with this design.
102
+ * Each map is encoded as an array of entries because a Map doesn't serialize
103
+ * directly. We also considered having a vstorage key for each offer but for now
104
+ * are sticking with this design.
71
105
  *
72
106
  * Cons
73
- * - Reserializes previously written results when a new result is added
74
- * - Optimizes reads though writes are on-chain (~100 machines) and reads are off-chain (to 1 machine)
107
+ *
108
+ * - Reserializes previously written results when a new result is added
109
+ * - Optimizes reads though writes are on-chain (~100 machines) and reads are
110
+ * off-chain (to 1 machine)
75
111
  *
76
112
  * Pros
77
- * - Reading all offer results happens much more (>100) often than storing a new offer result
78
- * - Reserialization and writes are paid in execution gas, whereas reads are not
79
113
  *
80
- * This design should be revisited if ever batch querying across vstorage keys become cheaper or reads be paid.
114
+ * - Reading all offer results happens much more (>100) often than storing a new
115
+ * offer result
116
+ * - Reserialization and writes are paid in execution gas, whereas reads are not
117
+ *
118
+ * This design should be revisited if ever batch querying across vstorage keys
119
+ * become cheaper or reads be paid.
81
120
  *
82
121
  * @typedef {{
83
- * purses: Array<{brand: Brand, balance: Amount}>,
84
- * offerToUsedInvitation: Array<[ offerId: string, usedInvitation: Amount ]>,
85
- * offerToPublicSubscriberPaths: Array<[ offerId: string, publicTopics: { [subscriberName: string]: string } ]>,
86
- * liveOffers: Array<[import('./offers.js').OfferId, import('./offers.js').OfferStatus]>,
122
+ * purses: { brand: Brand; balance: Amount }[];
123
+ * offerToUsedInvitation: [offerId: string, usedInvitation: Amount][];
124
+ * offerToPublicSubscriberPaths: [
125
+ * offerId: string,
126
+ * publicTopics: { [subscriberName: string]: string },
127
+ * ][];
128
+ * liveOffers: [OfferId, OfferStatus][];
87
129
  * }} CurrentWalletRecord
88
130
  */
89
131
 
90
132
  /**
91
- * @typedef {{ updated: 'offerStatus', status: import('./offers.js').OfferStatus }
133
+ * @typedef {{ updated: 'offerStatus'; status: OfferStatus }
92
134
  * | { updated: 'balance'; currentAmount: Amount }
93
- * | { updated: 'walletAction'; status: { error: string } }
94
- * } UpdateRecord Record of an update to the state of this wallet.
135
+ * | { updated: 'walletAction'; status: { error: string } }} UpdateRecord
136
+ * Record of an update to the state of this wallet.
95
137
  *
96
- * Client is responsible for coalescing updates into a current state. See `coalesceUpdates` utility.
138
+ * Client is responsible for coalescing updates into a current state. See
139
+ * `coalesceUpdates` utility.
97
140
  *
98
- * The reason for this burden on the client is that publishing
99
- * the full history of offers with each change is untenable.
141
+ * The reason for this burden on the client is that publishing the full history
142
+ * of offers with each change is untenable.
100
143
  *
101
- * `balance` update supports forward-compatibility for more than one purse per
102
- * brand. An additional key will be needed to disambiguate. For now the brand in
103
- * the amount suffices.
144
+ * `balance` update supports forward-compatibility for more than one purse per
145
+ * brand. An additional key will be needed to disambiguate. For now the brand
146
+ * in the amount suffices.
104
147
  */
105
148
 
106
149
  /**
107
150
  * @typedef {{
108
- * brand: Brand,
109
- * displayInfo: DisplayInfo,
110
- * issuer: Issuer,
111
- * petname: import('./types').Petname
151
+ * brand: Brand;
152
+ * displayInfo: DisplayInfo;
153
+ * issuer: Issuer;
154
+ * petname: import('./types.js').Petname;
112
155
  * }} BrandDescriptor
113
- * For use by clients to describe brands to users. Includes `displayInfo` to save a remote call.
156
+ * For use by clients to describe brands to users. Includes `displayInfo` to
157
+ * save a remote call.
114
158
  */
115
159
 
116
- // imports
117
- /** @typedef {import('./types').RemotePurse} RemotePurse */
118
-
119
160
  /**
120
161
  * @typedef {{
121
- * address: string,
122
- * bank: ERef<import('@agoric/vats/src/vat-bank').Bank>,
123
- * currentStorageNode: StorageNode,
124
- * invitationPurse: Purse<'set'>,
125
- * walletStorageNode: StorageNode,
162
+ * address: string;
163
+ * bank: ERef<import('@agoric/vats/src/vat-bank.js').Bank>;
164
+ * currentStorageNode: StorageNode;
165
+ * invitationPurse: Purse<'set', InvitationDetails>;
166
+ * walletStorageNode: StorageNode;
126
167
  * }} UniqueParams
127
168
  *
169
+ *
128
170
  * @typedef {Pick<MapStore<Brand, BrandDescriptor>, 'has' | 'get' | 'values'>} BrandDescriptorRegistry
171
+ *
172
+ *
129
173
  * @typedef {{
130
- * agoricNames: ERef<import('@agoric/vats').NameHub>,
131
- * registry: BrandDescriptorRegistry,
132
- * invitationIssuer: Issuer<'set'>,
133
- * invitationBrand: Brand<'set'>,
134
- * invitationDisplayInfo: DisplayInfo,
135
- * publicMarshaller: Marshaller,
136
- * zoe: ERef<ZoeService>,
174
+ * agoricNames: ERef<import('@agoric/vats').NameHub>;
175
+ * registry: BrandDescriptorRegistry;
176
+ * invitationIssuer: Issuer<'set'>;
177
+ * invitationBrand: Brand<'set'>;
178
+ * invitationDisplayInfo: DisplayInfo;
179
+ * publicMarshaller: Marshaller;
180
+ * zoe: ERef<ZoeService>;
137
181
  * }} SharedParams
138
182
  *
139
- * @typedef {ImmutableState & MutableState} State
140
- * - `brandPurses` is precious and closely held. defined as late as possible to reduce its scope.
141
- * - `offerToInvitationMakers` is precious and closely held.
142
- * - `offerToPublicSubscriberPaths` is precious and closely held.
143
- * - `purseBalances` is a cache of what we've received from purses. Held so we can publish all balances on change.
144
183
  *
145
- * @typedef {Readonly<UniqueParams & {
146
- * paymentQueues: MapStore<Brand, Array<Payment>>,
147
- * offerToInvitationMakers: MapStore<string, import('./types').InvitationMakers>,
148
- * offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>,
149
- * offerToUsedInvitation: MapStore<string, Amount>,
150
- * purseBalances: MapStore<RemotePurse, Amount>,
151
- * updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>,
152
- * 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>>,
155
- * }>} ImmutableState
184
+ * @typedef {ImmutableState & MutableState} State - `brandPurses` is precious
185
+ * and closely held. defined as late as possible to reduce its scope.
186
+ *
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
+ *
156
211
  *
157
212
  * @typedef {BrandDescriptor & { purse: Purse }} PurseRecord
158
- * @typedef {{
159
- * }} MutableState
213
+ *
214
+ * @typedef {{}} MutableState
160
215
  */
161
216
 
162
217
  /**
@@ -165,7 +220,7 @@ const trace = makeTracer('SmrtWlt');
165
220
  * TODO: consider moving to nameHub.js?
166
221
  *
167
222
  * @param {unknown} target - passable Key
168
- * @param {ERef<NameHub>} nameHub
223
+ * @param {ERef<import('@agoric/vats').NameHub>} nameHub
169
224
  */
170
225
  const namesOf = async (target, nameHub) => {
171
226
  const entries = await E(nameHub).entries();
@@ -225,7 +280,7 @@ export const prepareSmartWallet = (baggage, shared) => {
225
280
  zoe: M.eref(M.remotable('ZoeService')),
226
281
  }),
227
282
  );
228
-
283
+ const zone = makeDurableZone(baggage);
229
284
  const makeRecorderKit = prepareRecorderKit(baggage, shared.publicMarshaller);
230
285
 
231
286
  const walletPurses = provide(baggage, BRAND_TO_PURSES_KEY, () => {
@@ -237,8 +292,65 @@ export const prepareSmartWallet = (baggage, shared) => {
237
292
  return store;
238
293
  });
239
294
 
295
+ const vowTools = prepareVowTools(zone.subZone('vow'));
296
+
297
+ const makeOfferWatcher = prepareOfferWatcher(baggage, vowTools);
298
+ const watchOfferOutcomes = makeWatchOfferOutcomes(vowTools);
299
+
300
+ const updateShape = {
301
+ value: AmountShape,
302
+ updateCount: M.bigint(),
303
+ };
304
+
305
+ const NotifierShape = M.remotable();
306
+ const amountWatcherGuard = M.interface('paymentWatcher', {
307
+ onFulfilled: M.call(updateShape, NotifierShape).returns(),
308
+ onRejected: M.call(M.any(), NotifierShape).returns(M.promise()),
309
+ });
310
+
311
+ const prepareAmountWatcher = () =>
312
+ prepareExoClass(
313
+ baggage,
314
+ 'AmountWatcher',
315
+ amountWatcherGuard,
316
+ /**
317
+ * @param {Purse} purse
318
+ * @param {ReturnType<makeWalletWithResolvedStorageNodes>['helper']} helper
319
+ */
320
+ (purse, helper) => ({ purse, helper }),
321
+ {
322
+ /**
323
+ * @param {{ value: Amount; updateCount: bigint | undefined }} updateRecord
324
+ * @param {Notifier<Amount>} notifier
325
+ * @returns {void}
326
+ */
327
+ onFulfilled(updateRecord, notifier) {
328
+ const { helper, purse } = this.state;
329
+ helper.updateBalance(purse, updateRecord.value);
330
+ helper.watchNextBalance(
331
+ this.self,
332
+ notifier,
333
+ updateRecord.updateCount,
334
+ );
335
+ },
336
+ /**
337
+ * @param {unknown} err
338
+ * @returns {Promise<void>}
339
+ */
340
+ onRejected(err) {
341
+ const { helper, purse } = this.state;
342
+ if (isUpgradeDisconnection(err)) {
343
+ return helper.watchPurse(purse); // retry
344
+ }
345
+ helper.logWalletError(`failed amount observer`, err);
346
+ throw err;
347
+ },
348
+ },
349
+ );
350
+
351
+ const makeAmountWatcher = prepareAmountWatcher();
352
+
240
353
  /**
241
- *
242
354
  * @param {UniqueParams} unique
243
355
  * @returns {State}
244
356
  */
@@ -302,6 +414,9 @@ export const prepareSmartWallet = (baggage, shared) => {
302
414
  liveOfferSeats: makeScalarBigMapStore('live offer seats', {
303
415
  durable: true,
304
416
  }),
417
+ liveOfferPayments: makeScalarBigMapStore('live offer payments', {
418
+ durable: true,
419
+ }),
305
420
  };
306
421
 
307
422
  return {
@@ -320,10 +435,32 @@ export const prepareSmartWallet = (baggage, shared) => {
320
435
  .returns(M.promise()),
321
436
  publishCurrentState: M.call().returns(),
322
437
  watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()),
438
+ watchNextBalance: M.call(M.any(), NotifierShape, M.bigint()).returns(),
439
+ updateStatus: M.call(M.any()).returns(),
440
+ addContinuingOffer: M.call(
441
+ M.or(M.number(), M.string()),
442
+ AmountShape,
443
+ M.remotable('InvitationMaker'),
444
+ M.or(M.record(), M.undefined()),
445
+ ).returns(M.promise()),
446
+ purseForBrand: M.call(BrandShape).returns(M.promise()),
447
+ logWalletInfo: M.call().rest(M.arrayOf(M.any())).returns(),
448
+ logWalletError: M.call().rest(M.arrayOf(M.any())).returns(),
449
+ getLiveOfferPayments: M.call().returns(M.remotable('mapStore')),
323
450
  }),
451
+
324
452
  deposit: M.interface('depositFacetI', {
325
453
  receive: M.callWhen(M.await(M.eref(PaymentShape))).returns(AmountShape),
326
454
  }),
455
+ payments: M.interface('payments support', {
456
+ withdrawGive: M.call(
457
+ AmountKeywordRecordShape,
458
+ M.or(M.number(), M.string()),
459
+ ).returns(PaymentPKeywordRecordShape),
460
+ tryReclaimingWithdrawnPayments: M.call(
461
+ M.or(M.number(), M.string()),
462
+ ).returns(M.promise()),
463
+ }),
327
464
  offers: M.interface('offers facet', {
328
465
  executeOffer: M.call(shape.OfferSpec).returns(M.promise()),
329
466
  tryExitOffer: M.call(M.scalar()).returns(M.promise()),
@@ -340,10 +477,12 @@ export const prepareSmartWallet = (baggage, shared) => {
340
477
  }),
341
478
  };
342
479
 
480
+ // TODO move to top level so its type can be exported
343
481
  /**
344
- * Make the durable object to return, but taking some parameters that are awaited by a wrapping function.
345
- * This is necessary because the class kit construction helpers, `initState` and `finish` run synchronously
346
- * and the child storage node must be awaited until we have durable promises.
482
+ * Make the durable object to return, but taking some parameters that are
483
+ * awaited by a wrapping function. This is necessary because the class kit
484
+ * construction helpers, `initState` and `finish` run synchronously and the
485
+ * child storage node must be awaited until we have durable promises.
347
486
  */
348
487
  const makeWalletWithResolvedStorageNodes = prepareExoClassKit(
349
488
  baggage,
@@ -360,6 +499,7 @@ export const prepareSmartWallet = (baggage, shared) => {
360
499
  * @type {(id: string) => void}
361
500
  */
362
501
  assertUniqueOfferId(id) {
502
+ const { facets } = this;
363
503
  const {
364
504
  liveOffers,
365
505
  liveOfferSeats,
@@ -370,13 +510,14 @@ export const prepareSmartWallet = (baggage, shared) => {
370
510
  const used =
371
511
  liveOffers.has(id) ||
372
512
  liveOfferSeats.has(id) ||
513
+ facets.helper.getLiveOfferPayments().has(id) ||
373
514
  offerToInvitationMakers.has(id) ||
374
515
  offerToPublicSubscriberPaths.has(id) ||
375
516
  offerToUsedInvitation.has(id);
376
517
  !used || Fail`cannot re-use offer id ${id}`;
377
518
  },
378
519
  /**
379
- * @param {RemotePurse} purse
520
+ * @param {Purse} purse
380
521
  * @param {Amount<any>} balance
381
522
  */
382
523
  updateBalance(purse, balance) {
@@ -415,44 +556,38 @@ export const prepareSmartWallet = (baggage, shared) => {
415
556
  });
416
557
  },
417
558
 
418
- /** @type {(purse: ERef<RemotePurse>) => Promise<void>} */
559
+ /** @type {(purse: ERef<Purse>) => Promise<void>} */
419
560
  async watchPurse(purseRef) {
420
- const { address } = this.state;
561
+ const { helper } = this.facets;
562
+
563
+ // This would seem to fit the observeNotifier() pattern,
564
+ // but purse notifiers are not necessarily durable.
565
+ // If there is an error due to upgrade, retry watchPurse().
421
566
 
422
567
  const purse = await purseRef; // promises don't fit in durable storage
568
+ const handler = makeAmountWatcher(purse, helper);
423
569
 
424
- const { helper } = this.facets;
425
570
  // 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
- });
571
+ const notifier = await E(purse).getCurrentAmountNotifier();
572
+ const startP = E(notifier).getUpdateSince(undefined);
573
+ watchPromise(startP, handler, notifier);
574
+ },
575
+
576
+ watchNextBalance(handler, notifier, updateCount) {
577
+ const nextP = E(notifier).getUpdateSince(updateCount);
578
+ watchPromise(nextP, handler, notifier);
444
579
  },
445
580
 
446
581
  /**
447
- * Provide a purse given a NameHub of issuers and their
448
- * brands.
582
+ * Provide a purse given a NameHub of issuers and their brands.
449
583
  *
450
- * We current support only one NameHub, agoricNames, and
451
- * hence one purse per brand. But we store an array of them
452
- * to facilitate a transition to decentralized introductions.
584
+ * We currently support only one NameHub, agoricNames, and hence one
585
+ * purse per brand. But we store an array of them to facilitate a
586
+ * transition to decentralized introductions.
453
587
  *
454
588
  * @param {Brand} brand
455
- * @param {ERef<NameHub>} known - namehub with brand, issuer branches
589
+ * @param {ERef<import('@agoric/vats').NameHub>} known - namehub with
590
+ * brand, issuer branches
456
591
  * @returns {Promise<Purse | undefined>} undefined if brand is not known
457
592
  */
458
593
  async getPurseIfKnownBrand(brand, known) {
@@ -499,9 +634,115 @@ export const prepareSmartWallet = (baggage, shared) => {
499
634
  void helper.watchPurse(purse);
500
635
  return purse;
501
636
  },
637
+
638
+ /** @param {OfferStatus} offerStatus */
639
+ updateStatus(offerStatus) {
640
+ const { state, facets } = this;
641
+ facets.helper.logWalletInfo('offerStatus', offerStatus);
642
+
643
+ void state.updateRecorderKit.recorder.write({
644
+ updated: 'offerStatus',
645
+ status: offerStatus,
646
+ });
647
+
648
+ if ('numWantsSatisfied' in offerStatus) {
649
+ if (state.liveOfferSeats.has(offerStatus.id)) {
650
+ state.liveOfferSeats.delete(offerStatus.id);
651
+ }
652
+
653
+ if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
654
+ facets.helper.getLiveOfferPayments().delete(offerStatus.id);
655
+ }
656
+
657
+ if (state.liveOffers.has(offerStatus.id)) {
658
+ state.liveOffers.delete(offerStatus.id);
659
+ // This might get skipped in subsequent passes, since we .delete()
660
+ // the first time through
661
+ facets.helper.publishCurrentState();
662
+ }
663
+ }
664
+ },
665
+
666
+ /**
667
+ * @param {string} offerId
668
+ * @param {Amount<'set'>} invitationAmount
669
+ * @param {import('./types.js').InvitationMakers} invitationMakers
670
+ * @param {import('./types.js').PublicSubscribers} publicSubscribers
671
+ */
672
+ async addContinuingOffer(
673
+ offerId,
674
+ invitationAmount,
675
+ invitationMakers,
676
+ publicSubscribers,
677
+ ) {
678
+ const { state, facets } = this;
679
+
680
+ state.offerToUsedInvitation.init(offerId, invitationAmount);
681
+ state.offerToInvitationMakers.init(offerId, invitationMakers);
682
+ const pathMap = await objectMapStoragePath(publicSubscribers);
683
+ if (pathMap) {
684
+ facets.helper.logWalletInfo('recording pathMap', pathMap);
685
+ state.offerToPublicSubscriberPaths.init(offerId, pathMap);
686
+ }
687
+ facets.helper.publishCurrentState();
688
+ },
689
+
690
+ /**
691
+ * @param {Brand} brand
692
+ * @returns {Promise<Purse>}
693
+ */
694
+ async purseForBrand(brand) {
695
+ const { state, facets } = this;
696
+ const { registry, invitationBrand } = shared;
697
+
698
+ if (registry.has(brand)) {
699
+ return E(state.bank).getPurse(brand);
700
+ } else if (invitationBrand === brand) {
701
+ return state.invitationPurse;
702
+ }
703
+
704
+ const purse = await facets.helper.getPurseIfKnownBrand(
705
+ brand,
706
+ shared.agoricNames,
707
+ );
708
+ if (purse) {
709
+ return purse;
710
+ }
711
+ throw Fail`cannot find/make purse for ${brand}`;
712
+ },
713
+ logWalletInfo(...args) {
714
+ const { state } = this;
715
+ console.info('wallet', state.address, ...args);
716
+ },
717
+ logWalletError(...args) {
718
+ const { state } = this;
719
+ console.error('wallet', state.address, ...args);
720
+ },
721
+ // In new SmartWallets, this is part of state, but we can't add fields
722
+ // to instance state for older SmartWallets, so put it in baggage.
723
+ getLiveOfferPayments() {
724
+ const { state } = this;
725
+
726
+ if (state.liveOfferPayments) {
727
+ return state.liveOfferPayments;
728
+ }
729
+
730
+ // This will only happen for legacy wallets, before WF incarnation 2
731
+ if (!baggage.has(state.address)) {
732
+ trace(`getLiveOfferPayments adding store for ${state.address}`);
733
+ baggage.init(
734
+ state.address,
735
+ makeScalarBigMapStore('live offer payments', {
736
+ durable: true,
737
+ }),
738
+ );
739
+ }
740
+ return baggage.get(state.address);
741
+ },
502
742
  },
503
743
  /**
504
- * Similar to {DepositFacet} but async because it has to look up the purse.
744
+ * Similar to {DepositFacet} but async because it has to look up the
745
+ * purse.
505
746
  */
506
747
  deposit: {
507
748
  /**
@@ -511,17 +752,23 @@ export const prepareSmartWallet = (baggage, shared) => {
511
752
  *
512
753
  * @param {Payment} payment
513
754
  * @returns {Promise<Amount>}
514
- * @throws if there's not yet a purse, though the payment is held to try again when there is
755
+ * @throws if there's not yet a purse, though the payment is held to try
756
+ * again when there is
515
757
  */
516
758
  async receive(payment) {
517
- const { helper } = this.facets;
518
- const { paymentQueues: queues, bank, invitationPurse } = this.state;
759
+ const {
760
+ state,
761
+ facets: { helper },
762
+ } = this;
763
+ const { paymentQueues: queues, bank, invitationPurse } = state;
519
764
  const { registry, invitationBrand } = shared;
765
+
520
766
  const brand = await E(payment).getAllegedBrand();
521
767
 
522
768
  // When there is a purse deposit into it
523
769
  if (registry.has(brand)) {
524
770
  const purse = E(bank).getPurse(brand);
771
+ // @ts-expect-error narrow assetKind to 'nat'
525
772
  return E(purse).deposit(payment);
526
773
  } else if (invitationBrand === brand) {
527
774
  // @ts-expect-error narrow assetKind to 'set'
@@ -542,144 +789,249 @@ export const prepareSmartWallet = (baggage, shared) => {
542
789
  throw Fail`cannot deposit payment with brand ${brand}: no purse`;
543
790
  },
544
791
  },
792
+
793
+ payments: {
794
+ /**
795
+ * Withdraw the offered amount from the appropriate purse of this
796
+ * wallet.
797
+ *
798
+ * Save its amount in liveOfferPayments in case we need to reclaim the
799
+ * payment.
800
+ *
801
+ * @param {AmountKeywordRecord} give
802
+ * @param {OfferId} offerId
803
+ * @returns {PaymentPKeywordRecord}
804
+ */
805
+ withdrawGive(give, offerId) {
806
+ const { facets } = this;
807
+
808
+ /** @type {MapStore<Brand, Payment>} */
809
+ const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
810
+ durable: true,
811
+ });
812
+ facets.helper
813
+ .getLiveOfferPayments()
814
+ .init(offerId, brandPaymentRecord);
815
+
816
+ // Add each payment amount to brandPaymentRecord as it is withdrawn. If
817
+ // there's an error later, we can use it to redeposit the correct amount.
818
+ return objectMap(give, amount => {
819
+ /** @type {Promise<Purse>} */
820
+ const purseP = facets.helper.purseForBrand(amount.brand);
821
+ const paymentP = E(purseP).withdraw(amount);
822
+ void E.when(
823
+ paymentP,
824
+ payment => brandPaymentRecord.init(amount.brand, payment),
825
+ e => {
826
+ // recovery will be handled by tryReclaimingWithdrawnPayments()
827
+ facets.helper.logWalletInfo(
828
+ `⚠️ Payment withdrawal failed.`,
829
+ offerId,
830
+ e,
831
+ );
832
+ },
833
+ );
834
+ return paymentP;
835
+ });
836
+ },
837
+
838
+ /**
839
+ * Find the live payments for the offer and deposit them back in the
840
+ * appropriate purses.
841
+ *
842
+ * @param {OfferId} offerId
843
+ * @returns {Promise<Amount[]>}
844
+ */
845
+ async tryReclaimingWithdrawnPayments(offerId) {
846
+ const { facets } = this;
847
+
848
+ await null;
849
+
850
+ const liveOfferPayments = facets.helper.getLiveOfferPayments();
851
+ if (liveOfferPayments.has(offerId)) {
852
+ const brandPaymentRecord = liveOfferPayments.get(offerId);
853
+ if (!brandPaymentRecord) {
854
+ return [];
855
+ }
856
+ const out = [];
857
+ // Use allSettled to ensure we attempt all the deposits, regardless of
858
+ // individual rejections.
859
+ await Promise.allSettled(
860
+ Array.from(brandPaymentRecord.entries()).map(([b, p]) => {
861
+ // Wait for the withdrawal to complete. This protects against a
862
+ // race when updating paymentToPurse.
863
+ const purseP = facets.helper.purseForBrand(b);
864
+
865
+ // Now send it back to the purse.
866
+ return E(purseP)
867
+ .deposit(p)
868
+ .then(amt => {
869
+ out.push(amt);
870
+ });
871
+ }),
872
+ );
873
+ return harden(out);
874
+ }
875
+ return [];
876
+ },
877
+ },
878
+
545
879
  offers: {
546
880
  /**
547
- * Take an offer description provided in capData, augment it with payments and call zoe.offer()
881
+ * Take an offer description provided in capData, augment it with
882
+ * payments and call zoe.offer()
548
883
  *
549
- * @param {import('./offers.js').OfferSpec} offerSpec
550
- * @returns {Promise<void>} after the offer has been both seated and exited by Zoe.
551
- * @throws if any parts of the offer can be determined synchronously to be invalid
884
+ * @param {OfferSpec} offerSpec
885
+ * @returns {Promise<void>} after the offer has been both seated and
886
+ * exited by Zoe.
887
+ * @throws if any parts of the offer can be determined synchronously to
888
+ * be invalid
552
889
  */
553
890
  async executeOffer(offerSpec) {
554
891
  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;
892
+ const { address, invitationPurse } = state;
893
+ const { zoe, agoricNames } = shared;
894
+ const { invitationBrand, invitationIssuer } = shared;
565
895
 
566
896
  facets.helper.assertUniqueOfferId(String(offerSpec.id));
567
897
 
568
- const logger = {
569
- info: (...args) => console.info('wallet', address, ...args),
570
- error: (...args) => console.error('wallet', address, ...args),
571
- };
898
+ await null;
572
899
 
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
- }
900
+ /** @type {UserSeat} */
901
+ let seatRef;
902
+ let watcher;
903
+ try {
904
+ const invitationFromSpec = makeInvitationsHelper(
905
+ zoe,
906
+ agoricNames,
907
+ invitationBrand,
908
+ invitationPurse,
909
+ state.offerToInvitationMakers.get,
910
+ );
598
911
 
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);
912
+ facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
612
913
 
613
- void updateRecorderKit.recorder.write({
614
- updated: 'offerStatus',
615
- status: offerStatus,
616
- });
914
+ // 1. Prepare values and validate synchronously.
915
+ const { proposal } = offerSpec;
617
916
 
618
- const isSeatExited = 'numWantsSatisfied' in offerStatus;
619
- if (isSeatExited) {
620
- if (state.liveOfferSeats.has(offerStatus.id)) {
621
- state.liveOfferSeats.delete(offerStatus.id);
622
- }
917
+ const invitation = invitationFromSpec(offerSpec.invitationSpec);
623
918
 
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,
919
+ const invitationAmount =
920
+ await E(invitationIssuer).getAmountOf(invitation);
921
+
922
+ // 2. Begin executing offer
923
+ // No explicit signal to user that we reached here but if anything above
924
+ // failed they'd get an 'error' status update.
925
+
926
+ const withdrawnPayments =
927
+ proposal?.give &&
928
+ (await deeplyFulfilledObject(
929
+ facets.payments.withdrawGive(proposal.give, offerSpec.id),
930
+ ));
931
+
932
+ seatRef = await E(zoe).offer(
933
+ invitation,
934
+ proposal,
935
+ withdrawnPayments,
936
+ offerSpec.offerArgs,
937
+ );
938
+ facets.helper.logWalletInfo(offerSpec.id, 'seated');
939
+
940
+ watcher = makeOfferWatcher(
941
+ facets.helper,
942
+ facets.deposit,
943
+ offerSpec,
944
+ address,
633
945
  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
- });
946
+ seatRef,
947
+ );
647
948
 
648
- return executor.executeOffer(offerSpec, seatRef => {
649
949
  state.liveOffers.init(offerSpec.id, offerSpec);
650
- facets.helper.publishCurrentState();
651
950
  state.liveOfferSeats.init(offerSpec.id, seatRef);
652
- });
951
+
952
+ // publish the live offers
953
+ facets.helper.publishCurrentState();
954
+
955
+ // await so that any errors are caught and handled below
956
+ await watchOfferOutcomes(watcher, seatRef);
957
+ } catch (reason) {
958
+ // This block only runs if the block above fails during one vat incarnation.
959
+ facets.helper.logWalletError('IMMEDIATE OFFER ERROR:', reason);
960
+
961
+ // Update status to observers
962
+ if (isUpgradeDisconnection(reason)) {
963
+ // The offer watchers will reconnect. Don't reclaim or exit
964
+ return;
965
+ } else if (watcher) {
966
+ // The watcher's onRejected will updateStatus()
967
+ } else {
968
+ facets.helper.updateStatus({
969
+ error: reason.toString(),
970
+ ...offerSpec,
971
+ });
972
+ }
973
+
974
+ // Backstop recovery, in case something very basic fails.
975
+ if (offerSpec?.proposal?.give) {
976
+ facets.payments
977
+ .tryReclaimingWithdrawnPayments(offerSpec.id)
978
+ .catch(e =>
979
+ facets.helper.logWalletError(
980
+ 'recovery failed reclaiming payments',
981
+ e,
982
+ ),
983
+ );
984
+ }
985
+
986
+ // XXX tests rely on throwing immediate errors, not covering the
987
+ // error handling in the event the failure is after an upgrade
988
+ throw reason;
989
+ }
653
990
  },
654
991
  /**
655
992
  * Take an offer's id, look up its seat, try to exit.
656
993
  *
657
- * @param {import('./offers.js').OfferId} offerId
994
+ * @param {OfferId} offerId
658
995
  * @returns {Promise<void>}
659
996
  * @throws if the seat can't be found or E(seatRef).tryExit() fails.
660
997
  */
661
998
  async tryExitOffer(offerId) {
999
+ const { facets } = this;
1000
+ const amts = await facets.payments
1001
+ .tryReclaimingWithdrawnPayments(offerId)
1002
+ .catch(e => {
1003
+ facets.helper.logWalletError(
1004
+ 'recovery failed reclaiming payments',
1005
+ e,
1006
+ );
1007
+ return [];
1008
+ });
1009
+ if (amts.length > 0) {
1010
+ facets.helper.logWalletInfo('reclaimed', amts, 'from', offerId);
1011
+ }
662
1012
  const seatRef = this.state.liveOfferSeats.get(offerId);
663
1013
  await E(seatRef).tryExit();
664
1014
  },
665
1015
  },
666
1016
  self: {
667
1017
  /**
668
- * Umarshals the actionCapData and delegates to the appropriate action handler.
1018
+ * Umarshals the actionCapData and delegates to the appropriate action
1019
+ * handler.
669
1020
  *
670
- * @param {import('@endo/marshal').CapData<string>} actionCapData of type BridgeAction
1021
+ * @param {import('@endo/marshal').CapData<string | null>} actionCapData
1022
+ * of type BridgeAction
671
1023
  * @param {boolean} [canSpend]
672
1024
  * @returns {Promise<void>}
673
1025
  */
674
1026
  handleBridgeAction(actionCapData, canSpend = false) {
1027
+ const { facets } = this;
1028
+ const { offers } = facets;
675
1029
  const { publicMarshaller } = shared;
676
1030
 
677
- const { offers } = this.facets;
678
-
679
1031
  /** @param {Error} err */
680
1032
  const recordError = err => {
681
- const { address, updateRecorderKit } = this.state;
682
- console.error('wallet', address, 'handleBridgeAction error:', err);
1033
+ const { updateRecorderKit } = this.state;
1034
+ facets.helper.logWalletError('handleBridgeAction error:', err);
683
1035
  void updateRecorderKit.recorder.write({
684
1036
  updated: 'walletAction',
685
1037
  status: { error: err.message },
@@ -724,14 +1076,18 @@ export const prepareSmartWallet = (baggage, shared) => {
724
1076
  },
725
1077
  /** @deprecated use getPublicTopics */
726
1078
  getCurrentSubscriber() {
727
- return this.state.currentRecorderKit.subscriber;
1079
+ const { state } = this;
1080
+ return state.currentRecorderKit.subscriber;
728
1081
  },
729
1082
  /** @deprecated use getPublicTopics */
730
1083
  getUpdatesSubscriber() {
731
- return this.state.updateRecorderKit.subscriber;
1084
+ const { state } = this;
1085
+ return state.updateRecorderKit.subscriber;
732
1086
  },
733
1087
  getPublicTopics() {
734
- const { currentRecorderKit, updateRecorderKit } = this.state;
1088
+ const { state } = this;
1089
+ const { currentRecorderKit, updateRecorderKit } = state;
1090
+
735
1091
  return harden({
736
1092
  current: {
737
1093
  description: 'Current state of wallet',
@@ -752,14 +1108,18 @@ export const prepareSmartWallet = (baggage, shared) => {
752
1108
  const { invitationPurse } = state;
753
1109
  const { helper } = facets;
754
1110
 
755
- // @ts-expect-error RemotePurse cast
756
1111
  void helper.watchPurse(invitationPurse);
757
1112
  },
758
1113
  },
759
1114
  );
760
1115
 
761
1116
  /**
762
- * @param {Omit<UniqueParams, 'currentStorageNode' | 'walletStorageNode'> & {walletStorageNode: ERef<StorageNode>}} uniqueWithoutChildNodes
1117
+ * @param {Omit<
1118
+ * UniqueParams,
1119
+ * 'currentStorageNode' | 'walletStorageNode'
1120
+ * > & {
1121
+ * walletStorageNode: ERef<StorageNode>;
1122
+ * }} uniqueWithoutChildNodes
763
1123
  */
764
1124
  const makeSmartWallet = async uniqueWithoutChildNodes => {
765
1125
  const [walletStorageNode, currentStorageNode] = await Promise.all([