@agoric/smart-wallet 0.5.4-other-dev-8f8782b.0 → 0.5.4-other-dev-fbe72e7.0.fbe72e7

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 (40) hide show
  1. package/package.json +43 -29
  2. package/src/index.d.ts +2 -0
  3. package/src/index.d.ts.map +1 -0
  4. package/src/index.js +6 -0
  5. package/src/invitations.d.ts +14 -10
  6. package/src/invitations.d.ts.map +1 -1
  7. package/src/invitations.js +39 -33
  8. package/src/marshal-contexts.d.ts +45 -41
  9. package/src/marshal-contexts.d.ts.map +1 -1
  10. package/src/marshal-contexts.js +69 -61
  11. package/src/offerWatcher.d.ts +54 -0
  12. package/src/offerWatcher.d.ts.map +1 -0
  13. package/src/offerWatcher.js +343 -0
  14. package/src/offers.d.ts +60 -30
  15. package/src/offers.d.ts.map +1 -1
  16. package/src/offers.js +38 -182
  17. package/src/proposals/upgrade-wallet-factory2-proposal.js +61 -0
  18. package/src/proposals/upgrade-walletFactory-proposal.js +46 -23
  19. package/src/smartWallet.d.ts +132 -68
  20. package/src/smartWallet.d.ts.map +1 -1
  21. package/src/smartWallet.js +718 -217
  22. package/src/typeGuards.d.ts +1 -1
  23. package/src/typeGuards.js +29 -1
  24. package/src/types-index.d.ts +2 -0
  25. package/src/types-index.js +2 -0
  26. package/src/types.d.ts +36 -41
  27. package/src/types.d.ts.map +1 -0
  28. package/src/types.ts +90 -0
  29. package/src/utils.d.ts +17 -14
  30. package/src/utils.d.ts.map +1 -1
  31. package/src/utils.js +19 -6
  32. package/src/walletFactory.d.ts +24 -78
  33. package/src/walletFactory.d.ts.map +1 -1
  34. package/src/walletFactory.js +64 -37
  35. package/CHANGELOG.md +0 -180
  36. package/src/payments.d.ts +0 -20
  37. package/src/payments.d.ts.map +0 -1
  38. package/src/payments.js +0 -89
  39. package/src/proposals/upgrade-walletFactory-proposal.d.ts +0 -17
  40. package/src/proposals/upgrade-walletFactory-proposal.d.ts.map +0 -1
@@ -1,5 +1,5 @@
1
- // backported types are out of sync
2
- // @ts-nocheck
1
+ import { Fail, q } from '@endo/errors';
2
+ import { E, passStyleOf } 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,204 @@ 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
+ /**
49
+ * @import {Amount, Brand, Issuer, Payment, Purse} from '@agoric/ertp';
50
+ * @import {WeakMapStore, MapStore} from '@agoric/store'
51
+ * @import {InvitationDetails, PaymentPKeywordRecord, Proposal, UserSeat} from '@agoric/zoe';
52
+ * @import {CopyRecord} from '@endo/pass-style';
53
+ * @import {EReturn} from '@endo/far';
54
+ * @import {OfferId, OfferStatus, OfferSpec, InvokeEntryMessage, ResultPlan} from './offers.js';
55
+ */
36
56
 
37
57
  const trace = makeTracer('SmrtWlt');
38
58
 
39
59
  /**
40
60
  * @file Smart wallet module
41
- *
42
- * @see {@link ../README.md}}
61
+ * @see {@link ../README.md} }
43
62
  */
44
63
 
45
64
  /**
46
65
  * @typedef {{
47
- * method: 'executeOffer'
48
- * offer: import('./offers.js').OfferSpec,
66
+ * logger: {
67
+ * info: (...args: any[]) => void;
68
+ * error: (...args: any[]) => void;
69
+ * };
70
+ * makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher;
71
+ * invitationFromSpec: ERef<Invitation>;
72
+ * }} ExecutorPowers
73
+ */
74
+
75
+ /**
76
+ * @typedef {{
77
+ * method: 'executeOffer';
78
+ * offer: OfferSpec;
49
79
  * }} ExecuteOfferAction
50
80
  */
51
81
 
52
82
  /**
53
83
  * @typedef {{
54
- * method: 'tryExitOffer'
55
- * offerId: import('./offers.js').OfferId,
84
+ * method: 'tryExitOffer';
85
+ * offerId: OfferId;
56
86
  * }} TryExitOfferAction
57
87
  */
58
88
 
89
+ /**
90
+ * @typedef {object} InvokeStoreEntryAction
91
+ * @property {'invokeEntry'} method BridgeAction discriminator
92
+ * @property {InvokeEntryMessage} message object method call to make
93
+ */
94
+
59
95
  // Discriminated union. Possible future messages types:
60
96
  // maybe suggestIssuer for https://github.com/Agoric/agoric-sdk/issues/6132
61
97
  // setting petnames and adding brands for https://github.com/Agoric/agoric-sdk/issues/6126
62
- /**
63
- * @typedef { ExecuteOfferAction | TryExitOfferAction } BridgeAction
64
- */
98
+ /** @typedef {ExecuteOfferAction | TryExitOfferAction | InvokeStoreEntryAction} BridgeAction */
65
99
 
66
100
  /**
67
- * Purses is an array to support a future requirement of multiple purses per brand.
101
+ * Purses is an array to support a future requirement of multiple purses per
102
+ * brand.
68
103
  *
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.
104
+ * Each map is encoded as an array of entries because a Map doesn't serialize
105
+ * directly. We also considered having a vstorage key for each offer but for now
106
+ * are sticking with this design.
71
107
  *
72
108
  * 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)
109
+ *
110
+ * - Reserializes previously written results when a new result is added
111
+ * - Optimizes reads though writes are on-chain (~100 machines) and reads are
112
+ * off-chain (to 1 machine)
75
113
  *
76
114
  * 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
115
  *
80
- * This design should be revisited if ever batch querying across vstorage keys become cheaper or reads be paid.
116
+ * - Reading all offer results happens much more (>100) often than storing a new
117
+ * offer result
118
+ * - Reserialization and writes are paid in execution gas, whereas reads are not
119
+ *
120
+ * This design should be revisited if ever batch querying across vstorage keys
121
+ * become cheaper or reads be paid.
81
122
  *
82
123
  * @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]>,
124
+ * purses: { brand: Brand; balance: Amount }[];
125
+ * offerToUsedInvitation: [offerId: string, usedInvitation: Amount][];
126
+ * offerToPublicSubscriberPaths: [
127
+ * offerId: string,
128
+ * publicTopics: { [subscriberName: string]: string },
129
+ * ][];
130
+ * liveOffers: [OfferId, OfferStatus][];
87
131
  * }} CurrentWalletRecord
88
132
  */
89
133
 
90
134
  /**
91
- * @typedef {{ updated: 'offerStatus', status: import('./offers.js').OfferStatus }
135
+ * @typedef {{ updated: 'offerStatus'; status: OfferStatus }
92
136
  * | { updated: 'balance'; currentAmount: Amount }
93
137
  * | { updated: 'walletAction'; status: { error: string } }
94
- * } UpdateRecord Record of an update to the state of this wallet.
138
+ * | {
139
+ * updated: 'invocation';
140
+ * id: string | number;
141
+ * error?: string;
142
+ * result?: { name?: string; passStyle: string };
143
+ * }} UpdateRecord
144
+ * Record of an update to the state of this wallet.
95
145
  *
96
- * Client is responsible for coalescing updates into a current state. See `coalesceUpdates` utility.
146
+ * Client is responsible for coalescing updates into a current state. See
147
+ * `coalesceUpdates` utility.
97
148
  *
98
- * The reason for this burden on the client is that publishing
99
- * the full history of offers with each change is untenable.
149
+ * The reason for this burden on the client is that publishing the full history
150
+ * of offers with each change is untenable.
100
151
  *
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.
152
+ * `balance` update supports forward-compatibility for more than one purse per
153
+ * brand. An additional key will be needed to disambiguate. For now the brand
154
+ * in the amount suffices.
104
155
  */
105
156
 
106
157
  /**
107
158
  * @typedef {{
108
- * brand: Brand,
109
- * displayInfo: DisplayInfo,
110
- * issuer: Issuer,
111
- * petname: import('./types').Petname
159
+ * brand: Brand;
160
+ * displayInfo: DisplayInfo;
161
+ * issuer: Issuer;
162
+ * petname: import('./types.js').Petname;
112
163
  * }} BrandDescriptor
113
- * For use by clients to describe brands to users. Includes `displayInfo` to save a remote call.
164
+ * For use by clients to describe brands to users. Includes `displayInfo` to
165
+ * save a remote call.
114
166
  */
115
167
 
116
- // imports
117
- /** @typedef {import('./types').RemotePurse} RemotePurse */
118
-
119
168
  /**
120
169
  * @typedef {{
121
- * address: string,
122
- * bank: ERef<import('@agoric/vats/src/vat-bank').Bank>,
123
- * currentStorageNode: StorageNode,
124
- * invitationPurse: Purse<'set'>,
125
- * walletStorageNode: StorageNode,
170
+ * address: string;
171
+ * bank: ERef<import('@agoric/vats/src/vat-bank.js').Bank>;
172
+ * currentStorageNode: StorageNode;
173
+ * invitationPurse: Purse<'set', InvitationDetails>;
174
+ * walletStorageNode: StorageNode;
126
175
  * }} UniqueParams
127
176
  *
177
+ *
128
178
  * @typedef {Pick<MapStore<Brand, BrandDescriptor>, 'has' | 'get' | 'values'>} BrandDescriptorRegistry
179
+ *
180
+ *
129
181
  * @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>,
182
+ * agoricNames: ERef<import('@agoric/vats').NameHub>;
183
+ * registry: BrandDescriptorRegistry;
184
+ * invitationIssuer: Issuer<'set'>;
185
+ * invitationBrand: Brand<'set'>;
186
+ * invitationDisplayInfo: DisplayInfo;
187
+ * publicMarshaller: Marshaller;
188
+ * zoe: ERef<ZoeService>;
137
189
  * }} SharedParams
138
190
  *
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
191
  *
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
192
+ * @typedef {ImmutableState & MutableState} State - `brandPurses` is precious
193
+ * and closely held. defined as late as possible to reduce its scope.
194
+ *
195
+ * - `offerToInvitationMakers` is precious and closely held.
196
+ * - `offerToPublicSubscriberPaths` is precious and closely held.
197
+ * - `purseBalances` is a cache of what we've received from purses. Held so we can
198
+ * publish all balances on change.
199
+ *
200
+ *
201
+ * @typedef {Readonly<
202
+ * UniqueParams & {
203
+ * paymentQueues: MapStore<Brand, Payment[]>;
204
+ * offerToInvitationMakers: MapStore<
205
+ * string,
206
+ * import('./types.js').InvitationMakers
207
+ * >;
208
+ * offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>;
209
+ * offerToUsedInvitation: MapStore<string, Amount<'set'>>;
210
+ * purseBalances: MapStore<Purse, Amount>;
211
+ * updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>;
212
+ * currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<CurrentWalletRecord>;
213
+ * liveOffers: MapStore<OfferId, OfferStatus>;
214
+ * liveOfferSeats: MapStore<OfferId, UserSeat<unknown>>;
215
+ * liveOfferPayments: MapStore<OfferId, MapStore<Brand, Payment>>;
216
+ * myStore: MapStore;
217
+ * }
218
+ * >} ImmutableState
219
+ *
156
220
  *
157
221
  * @typedef {BrandDescriptor & { purse: Purse }} PurseRecord
158
- * @typedef {{
159
- * }} MutableState
222
+ *
223
+ * @typedef {{}} MutableState
160
224
  */
161
225
 
162
226
  /**
@@ -165,7 +229,7 @@ const trace = makeTracer('SmrtWlt');
165
229
  * TODO: consider moving to nameHub.js?
166
230
  *
167
231
  * @param {unknown} target - passable Key
168
- * @param {ERef<NameHub>} nameHub
232
+ * @param {ERef<import('@agoric/vats').NameHub>} nameHub
169
233
  */
170
234
  const namesOf = async (target, nameHub) => {
171
235
  const entries = await E(nameHub).entries();
@@ -225,7 +289,7 @@ export const prepareSmartWallet = (baggage, shared) => {
225
289
  zoe: M.eref(M.remotable('ZoeService')),
226
290
  }),
227
291
  );
228
-
292
+ const zone = makeDurableZone(baggage);
229
293
  const makeRecorderKit = prepareRecorderKit(baggage, shared.publicMarshaller);
230
294
 
231
295
  const walletPurses = provide(baggage, BRAND_TO_PURSES_KEY, () => {
@@ -237,8 +301,65 @@ export const prepareSmartWallet = (baggage, shared) => {
237
301
  return store;
238
302
  });
239
303
 
304
+ const vowTools = prepareVowTools(zone.subZone('vow'));
305
+
306
+ const makeOfferWatcher = prepareOfferWatcher(baggage, vowTools);
307
+ const watchOfferOutcomes = makeWatchOfferOutcomes(vowTools);
308
+
309
+ const updateShape = {
310
+ value: AmountShape,
311
+ updateCount: M.bigint(),
312
+ };
313
+
314
+ const NotifierShape = M.remotable();
315
+ const amountWatcherGuard = M.interface('paymentWatcher', {
316
+ onFulfilled: M.call(updateShape, NotifierShape).returns(),
317
+ onRejected: M.call(M.any(), NotifierShape).returns(M.promise()),
318
+ });
319
+
320
+ const prepareAmountWatcher = () =>
321
+ prepareExoClass(
322
+ baggage,
323
+ 'AmountWatcher',
324
+ amountWatcherGuard,
325
+ /**
326
+ * @param {Purse} purse
327
+ * @param {ReturnType<makeWalletWithResolvedStorageNodes>['helper']} helper
328
+ */
329
+ (purse, helper) => ({ purse, helper }),
330
+ {
331
+ /**
332
+ * @param {{ value: Amount; updateCount: bigint | undefined }} updateRecord
333
+ * @param {Notifier<Amount>} notifier
334
+ * @returns {void}
335
+ */
336
+ onFulfilled(updateRecord, notifier) {
337
+ const { helper, purse } = this.state;
338
+ helper.updateBalance(purse, updateRecord.value);
339
+ helper.watchNextBalance(
340
+ this.self,
341
+ notifier,
342
+ updateRecord.updateCount,
343
+ );
344
+ },
345
+ /**
346
+ * @param {unknown} err
347
+ * @returns {Promise<void>}
348
+ */
349
+ onRejected(err) {
350
+ const { helper, purse } = this.state;
351
+ if (isUpgradeDisconnection(err)) {
352
+ return helper.watchPurse(purse); // retry
353
+ }
354
+ helper.logWalletError(`failed amount observer`, err);
355
+ throw err;
356
+ },
357
+ },
358
+ );
359
+
360
+ const makeAmountWatcher = prepareAmountWatcher();
361
+
240
362
  /**
241
- *
242
363
  * @param {UniqueParams} unique
243
364
  * @returns {State}
244
365
  */
@@ -282,6 +403,9 @@ export const prepareSmartWallet = (baggage, shared) => {
282
403
  durable: true,
283
404
  },
284
405
  ),
406
+ // NB: Wallets before this state property was added do not support
407
+ // saving results or invoking the saved items.
408
+ myStore: zone.detached().mapStore('my items'),
285
409
  };
286
410
 
287
411
  /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>} */
@@ -302,6 +426,9 @@ export const prepareSmartWallet = (baggage, shared) => {
302
426
  liveOfferSeats: makeScalarBigMapStore('live offer seats', {
303
427
  durable: true,
304
428
  }),
429
+ liveOfferPayments: makeScalarBigMapStore('live offer payments', {
430
+ durable: true,
431
+ }),
305
432
  };
306
433
 
307
434
  return {
@@ -311,6 +438,11 @@ export const prepareSmartWallet = (baggage, shared) => {
311
438
  };
312
439
  };
313
440
 
441
+ const invocationResultShape = M.splitRecord(
442
+ {},
443
+ { id: M.or(M.string(), M.number()), saveResult: shape.ResultPlan },
444
+ );
445
+
314
446
  const behaviorGuards = {
315
447
  helper: M.interface('helperFacetI', {
316
448
  assertUniqueOfferId: M.call(M.string()).returns(),
@@ -320,19 +452,51 @@ export const prepareSmartWallet = (baggage, shared) => {
320
452
  .returns(M.promise()),
321
453
  publishCurrentState: M.call().returns(),
322
454
  watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()),
455
+ watchNextBalance: M.call(M.any(), NotifierShape, M.bigint()).returns(),
456
+ updateStatus: M.call(M.any()).returns(),
457
+ addContinuingOffer: M.call(
458
+ M.or(M.number(), M.string()),
459
+ AmountShape,
460
+ M.remotable('InvitationMaker'),
461
+ M.or(M.record(), M.undefined()),
462
+ ).returns(M.promise()),
463
+ purseForBrand: M.call(BrandShape).returns(M.promise()),
464
+ logWalletInfo: M.call().rest(M.arrayOf(M.any())).returns(),
465
+ logWalletError: M.call().rest(M.arrayOf(M.any())).returns(),
466
+ getLiveOfferPayments: M.call().returns(M.remotable('mapStore')),
467
+ saveEntry: M.call(shape.ResultPlan, M.any()).returns(M.string()),
468
+ findUnusedName: M.call(M.string()).returns(M.string()),
323
469
  }),
470
+
324
471
  deposit: M.interface('depositFacetI', {
325
472
  receive: M.callWhen(M.await(M.eref(PaymentShape))).returns(AmountShape),
326
473
  }),
474
+ payments: M.interface('payments support', {
475
+ withdrawGive: M.call(
476
+ AmountKeywordRecordShape,
477
+ M.or(M.number(), M.string()),
478
+ ).returns(PaymentPKeywordRecordShape),
479
+ tryReclaimingWithdrawnPayments: M.call(
480
+ M.or(M.number(), M.string()),
481
+ ).returns(M.promise()),
482
+ }),
327
483
  offers: M.interface('offers facet', {
328
484
  executeOffer: M.call(shape.OfferSpec).returns(M.promise()),
329
485
  tryExitOffer: M.call(M.scalar()).returns(M.promise()),
330
486
  }),
487
+ invoke: M.interface('invoke', {
488
+ invokeEntry: M.callWhen(shape.InvokeEntryMessage).returns(),
489
+ }),
490
+ resultStepWatcher: M.interface('resultStepWatcher', {
491
+ onFulfilled: M.call(M.any(), invocationResultShape).returns(),
492
+ onRejected: M.call(M.any(), invocationResultShape).returns(),
493
+ }),
331
494
  self: M.interface('selfFacetI', {
332
495
  handleBridgeAction: M.call(shape.StringCapData, M.boolean()).returns(
333
496
  M.promise(),
334
497
  ),
335
498
  getDepositFacet: M.call().returns(M.remotable()),
499
+ getInvokeFacet: M.call().returns(M.remotable()),
336
500
  getOffersFacet: M.call().returns(M.remotable()),
337
501
  getCurrentSubscriber: M.call().returns(SubscriberShape),
338
502
  getUpdatesSubscriber: M.call().returns(SubscriberShape),
@@ -340,10 +504,12 @@ export const prepareSmartWallet = (baggage, shared) => {
340
504
  }),
341
505
  };
342
506
 
507
+ // TODO move to top level so its type can be exported
343
508
  /**
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.
509
+ * Make the durable object to return, but taking some parameters that are
510
+ * awaited by a wrapping function. This is necessary because the class kit
511
+ * construction helpers, `initState` and `finish` run synchronously and the
512
+ * child storage node must be awaited until we have durable promises.
347
513
  */
348
514
  const makeWalletWithResolvedStorageNodes = prepareExoClassKit(
349
515
  baggage,
@@ -360,6 +526,7 @@ export const prepareSmartWallet = (baggage, shared) => {
360
526
  * @type {(id: string) => void}
361
527
  */
362
528
  assertUniqueOfferId(id) {
529
+ const { facets } = this;
363
530
  const {
364
531
  liveOffers,
365
532
  liveOfferSeats,
@@ -370,13 +537,14 @@ export const prepareSmartWallet = (baggage, shared) => {
370
537
  const used =
371
538
  liveOffers.has(id) ||
372
539
  liveOfferSeats.has(id) ||
540
+ facets.helper.getLiveOfferPayments().has(id) ||
373
541
  offerToInvitationMakers.has(id) ||
374
542
  offerToPublicSubscriberPaths.has(id) ||
375
543
  offerToUsedInvitation.has(id);
376
544
  !used || Fail`cannot re-use offer id ${id}`;
377
545
  },
378
546
  /**
379
- * @param {RemotePurse} purse
547
+ * @param {Purse} purse
380
548
  * @param {Amount<any>} balance
381
549
  */
382
550
  updateBalance(purse, balance) {
@@ -415,44 +583,38 @@ export const prepareSmartWallet = (baggage, shared) => {
415
583
  });
416
584
  },
417
585
 
418
- /** @type {(purse: ERef<RemotePurse>) => Promise<void>} */
586
+ /** @type {(purse: ERef<Purse>) => Promise<void>} */
419
587
  async watchPurse(purseRef) {
420
- const { address } = this.state;
588
+ const { helper } = this.facets;
589
+
590
+ // This would seem to fit the observeNotifier() pattern,
591
+ // but purse notifiers are not necessarily durable.
592
+ // If there is an error due to upgrade, retry watchPurse().
421
593
 
422
594
  const purse = await purseRef; // promises don't fit in durable storage
595
+ const handler = makeAmountWatcher(purse, helper);
423
596
 
424
- const { helper } = this.facets;
425
597
  // 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
- });
598
+ const notifier = await E(purse).getCurrentAmountNotifier();
599
+ const startP = E(notifier).getUpdateSince(undefined);
600
+ watchPromise(startP, handler, notifier);
601
+ },
602
+
603
+ watchNextBalance(handler, notifier, updateCount) {
604
+ const nextP = E(notifier).getUpdateSince(updateCount);
605
+ watchPromise(nextP, handler, notifier);
444
606
  },
445
607
 
446
608
  /**
447
- * Provide a purse given a NameHub of issuers and their
448
- * brands.
609
+ * Provide a purse given a NameHub of issuers and their brands.
449
610
  *
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.
611
+ * We currently support only one NameHub, agoricNames, and hence one
612
+ * purse per brand. But we store an array of them to facilitate a
613
+ * transition to decentralized introductions.
453
614
  *
454
615
  * @param {Brand} brand
455
- * @param {ERef<NameHub>} known - namehub with brand, issuer branches
616
+ * @param {ERef<import('@agoric/vats').NameHub>} known - namehub with
617
+ * brand, issuer branches
456
618
  * @returns {Promise<Purse | undefined>} undefined if brand is not known
457
619
  */
458
620
  async getPurseIfKnownBrand(brand, known) {
@@ -499,9 +661,144 @@ export const prepareSmartWallet = (baggage, shared) => {
499
661
  void helper.watchPurse(purse);
500
662
  return purse;
501
663
  },
664
+
665
+ /** @param {OfferStatus} offerStatus */
666
+ updateStatus(offerStatus) {
667
+ const { state, facets } = this;
668
+ facets.helper.logWalletInfo('offerStatus', offerStatus);
669
+
670
+ void state.updateRecorderKit.recorder.write({
671
+ updated: 'offerStatus',
672
+ status: offerStatus,
673
+ });
674
+
675
+ if ('numWantsSatisfied' in offerStatus) {
676
+ if (state.liveOfferSeats.has(offerStatus.id)) {
677
+ state.liveOfferSeats.delete(offerStatus.id);
678
+ }
679
+
680
+ if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
681
+ facets.helper.getLiveOfferPayments().delete(offerStatus.id);
682
+ }
683
+
684
+ if (state.liveOffers.has(offerStatus.id)) {
685
+ state.liveOffers.delete(offerStatus.id);
686
+ // This might get skipped in subsequent passes, since we .delete()
687
+ // the first time through
688
+ facets.helper.publishCurrentState();
689
+ }
690
+ }
691
+ },
692
+
693
+ /**
694
+ * @param {string} offerId
695
+ * @param {Amount<'set'>} invitationAmount
696
+ * @param {import('./types.js').InvitationMakers} invitationMakers
697
+ * @param {import('./types.js').PublicSubscribers} publicSubscribers
698
+ */
699
+ async addContinuingOffer(
700
+ offerId,
701
+ invitationAmount,
702
+ invitationMakers,
703
+ publicSubscribers,
704
+ ) {
705
+ const { state, facets } = this;
706
+
707
+ state.offerToUsedInvitation.init(offerId, invitationAmount);
708
+ state.offerToInvitationMakers.init(offerId, invitationMakers);
709
+ const pathMap = await objectMapStoragePath(publicSubscribers);
710
+ if (pathMap) {
711
+ facets.helper.logWalletInfo('recording pathMap', pathMap);
712
+ state.offerToPublicSubscriberPaths.init(offerId, pathMap);
713
+ }
714
+ facets.helper.publishCurrentState();
715
+ },
716
+
717
+ /**
718
+ * @param {Brand} brand
719
+ * @returns {Promise<Purse>}
720
+ */
721
+ async purseForBrand(brand) {
722
+ const { state, facets } = this;
723
+ const { registry, invitationBrand } = shared;
724
+
725
+ if (registry.has(brand)) {
726
+ return E(state.bank).getPurse(brand);
727
+ } else if (invitationBrand === brand) {
728
+ return state.invitationPurse;
729
+ }
730
+
731
+ const purse = await facets.helper.getPurseIfKnownBrand(
732
+ brand,
733
+ shared.agoricNames,
734
+ );
735
+ if (purse) {
736
+ return purse;
737
+ }
738
+ throw Fail`cannot find/make purse for ${brand}`;
739
+ },
740
+ logWalletInfo(...args) {
741
+ const { state } = this;
742
+ console.info('wallet', state.address, ...args);
743
+ },
744
+ logWalletError(...args) {
745
+ const { state } = this;
746
+ console.error('wallet', state.address, ...args);
747
+ },
748
+ // In new SmartWallets, this is part of state, but we can't add fields
749
+ // to instance state for older SmartWallets, so put it in baggage.
750
+ getLiveOfferPayments() {
751
+ const { state } = this;
752
+
753
+ if (state.liveOfferPayments) {
754
+ return state.liveOfferPayments;
755
+ }
756
+
757
+ // This will only happen for legacy wallets, before WF incarnation 2
758
+ if (!baggage.has(state.address)) {
759
+ trace(`getLiveOfferPayments adding store for ${state.address}`);
760
+ baggage.init(
761
+ state.address,
762
+ makeScalarBigMapStore('live offer payments', {
763
+ durable: true,
764
+ }),
765
+ );
766
+ }
767
+ return baggage.get(state.address);
768
+ },
769
+ /** @param {string} suggestion */
770
+ findUnusedName(suggestion) {
771
+ const { myStore } = this.state;
772
+ let nonce = 0;
773
+ let name = suggestion;
774
+ while (myStore.has(name)) {
775
+ nonce += myStore.getSize(); // avoid linear work
776
+ name = `${suggestion}.${nonce}`;
777
+ }
778
+ return name;
779
+ },
780
+ /**
781
+ * @param {ResultPlan} plan
782
+ * @param {unknown} value
783
+ */
784
+ saveEntry(plan, value) {
785
+ const { myStore } = this.state;
786
+ const name = plan.overwrite
787
+ ? plan.name
788
+ : this.facets.helper.findUnusedName(plan.name);
789
+
790
+ if (myStore.has(name)) {
791
+ myStore.set(name, value);
792
+ } else {
793
+ myStore.init(name, value);
794
+ }
795
+ trace('set', name, '=', value);
796
+ return name;
797
+ },
502
798
  },
503
799
  /**
504
- * Similar to {DepositFacet} but async because it has to look up the purse.
800
+ * Similar to {DepositFacet} but async because it has to look up the
801
+ * purse.
505
802
  */
506
803
  deposit: {
507
804
  /**
@@ -511,17 +808,23 @@ export const prepareSmartWallet = (baggage, shared) => {
511
808
  *
512
809
  * @param {Payment} payment
513
810
  * @returns {Promise<Amount>}
514
- * @throws if there's not yet a purse, though the payment is held to try again when there is
811
+ * @throws if there's not yet a purse, though the payment is held to try
812
+ * again when there is
515
813
  */
516
814
  async receive(payment) {
517
- const { helper } = this.facets;
518
- const { paymentQueues: queues, bank, invitationPurse } = this.state;
815
+ const {
816
+ state,
817
+ facets: { helper },
818
+ } = this;
819
+ const { paymentQueues: queues, bank, invitationPurse } = state;
519
820
  const { registry, invitationBrand } = shared;
821
+
520
822
  const brand = await E(payment).getAllegedBrand();
521
823
 
522
824
  // When there is a purse deposit into it
523
825
  if (registry.has(brand)) {
524
826
  const purse = E(bank).getPurse(brand);
827
+ // @ts-expect-error narrow assetKind to 'nat'
525
828
  return E(purse).deposit(payment);
526
829
  } else if (invitationBrand === brand) {
527
830
  // @ts-expect-error narrow assetKind to 'set'
@@ -542,150 +845,329 @@ export const prepareSmartWallet = (baggage, shared) => {
542
845
  throw Fail`cannot deposit payment with brand ${brand}: no purse`;
543
846
  },
544
847
  },
848
+
849
+ payments: {
850
+ /**
851
+ * Withdraw the offered amount from the appropriate purse of this
852
+ * wallet.
853
+ *
854
+ * Save its amount in liveOfferPayments in case we need to reclaim the
855
+ * payment.
856
+ *
857
+ * @param {AmountKeywordRecord} give
858
+ * @param {OfferId} offerId
859
+ * @returns {PaymentPKeywordRecord}
860
+ */
861
+ withdrawGive(give, offerId) {
862
+ const { facets } = this;
863
+
864
+ /** @type {MapStore<Brand, Payment>} */
865
+ const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
866
+ durable: true,
867
+ });
868
+ facets.helper
869
+ .getLiveOfferPayments()
870
+ .init(offerId, brandPaymentRecord);
871
+
872
+ // Add each payment amount to brandPaymentRecord as it is withdrawn. If
873
+ // there's an error later, we can use it to redeposit the correct amount.
874
+ return objectMap(give, amount => {
875
+ /** @type {Promise<Purse>} */
876
+ const purseP = facets.helper.purseForBrand(amount.brand);
877
+ const paymentP = E(purseP).withdraw(amount);
878
+ void E.when(
879
+ paymentP,
880
+ payment => brandPaymentRecord.init(amount.brand, payment),
881
+ e => {
882
+ // recovery will be handled by tryReclaimingWithdrawnPayments()
883
+ facets.helper.logWalletInfo(
884
+ `⚠️ Payment withdrawal failed.`,
885
+ offerId,
886
+ e,
887
+ );
888
+ },
889
+ );
890
+ return paymentP;
891
+ });
892
+ },
893
+
894
+ /**
895
+ * Find the live payments for the offer and deposit them back in the
896
+ * appropriate purses.
897
+ *
898
+ * @param {OfferId} offerId
899
+ * @returns {Promise<Amount[]>}
900
+ */
901
+ async tryReclaimingWithdrawnPayments(offerId) {
902
+ const { facets } = this;
903
+
904
+ await null;
905
+
906
+ const liveOfferPayments = facets.helper.getLiveOfferPayments();
907
+ if (liveOfferPayments.has(offerId)) {
908
+ const brandPaymentRecord = liveOfferPayments.get(offerId);
909
+ if (!brandPaymentRecord) {
910
+ return [];
911
+ }
912
+ const out = [];
913
+ // Use allSettled to ensure we attempt all the deposits, regardless of
914
+ // individual rejections.
915
+ await Promise.allSettled(
916
+ Array.from(brandPaymentRecord.entries()).map(([b, p]) => {
917
+ // Wait for the withdrawal to complete. This protects against a
918
+ // race when updating paymentToPurse.
919
+ const purseP = facets.helper.purseForBrand(b);
920
+
921
+ // Now send it back to the purse.
922
+ return E(purseP)
923
+ .deposit(p)
924
+ .then(amt => {
925
+ out.push(amt);
926
+ });
927
+ }),
928
+ );
929
+ return harden(out);
930
+ }
931
+ return [];
932
+ },
933
+ },
934
+
545
935
  offers: {
546
936
  /**
547
- * Take an offer description provided in capData, augment it with payments and call zoe.offer()
937
+ * Take an offer description provided in capData, augment it with
938
+ * payments and call zoe.offer()
548
939
  *
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
940
+ * @param {OfferSpec} offerSpec
941
+ * @returns {Promise<void>} after the offer has been both seated and
942
+ * exited by Zoe.
943
+ * @throws if any parts of the offer can be determined synchronously to
944
+ * be invalid
552
945
  */
553
946
  async executeOffer(offerSpec) {
554
947
  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;
948
+ const { address, invitationPurse } = state;
949
+ const { zoe, agoricNames } = shared;
950
+ const { invitationBrand, invitationIssuer } = shared;
565
951
 
566
952
  facets.helper.assertUniqueOfferId(String(offerSpec.id));
567
953
 
568
- const logger = {
569
- info: (...args) => console.info('wallet', address, ...args),
570
- error: (...args) => console.error('wallet', address, ...args),
571
- };
954
+ await null;
572
955
 
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
- }
956
+ /** @type {UserSeat} */
957
+ let seatRef;
958
+ let watcher;
959
+ try {
960
+ const invitationFromSpec = makeInvitationsHelper(
961
+ zoe,
962
+ agoricNames,
963
+ invitationBrand,
964
+ invitationPurse,
965
+ state.offerToInvitationMakers.get,
966
+ );
598
967
 
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);
968
+ facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
612
969
 
613
- void updateRecorderKit.recorder.write({
614
- updated: 'offerStatus',
615
- status: offerStatus,
616
- });
970
+ // 1. Prepare values and validate synchronously.
971
+ const { proposal } = offerSpec;
617
972
 
618
- const isSeatExited = 'numWantsSatisfied' in offerStatus;
619
- if (isSeatExited) {
620
- if (state.liveOfferSeats.has(offerStatus.id)) {
621
- state.liveOfferSeats.delete(offerStatus.id);
622
- }
973
+ const invitation = invitationFromSpec(offerSpec.invitationSpec);
623
974
 
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,
975
+ const invitationAmount =
976
+ await E(invitationIssuer).getAmountOf(invitation);
977
+
978
+ // 2. Begin executing offer
979
+ // No explicit signal to user that we reached here but if anything above
980
+ // failed they'd get an 'error' status update.
981
+
982
+ const withdrawnPayments =
983
+ proposal?.give &&
984
+ (await deeplyFulfilledObject(
985
+ facets.payments.withdrawGive(proposal.give, offerSpec.id),
986
+ ));
987
+
988
+ seatRef = await E(zoe).offer(
989
+ invitation,
990
+ proposal,
991
+ withdrawnPayments,
992
+ offerSpec.offerArgs,
993
+ );
994
+ facets.helper.logWalletInfo(offerSpec.id, 'seated');
995
+
996
+ watcher = makeOfferWatcher(
997
+ facets.helper,
998
+ facets.deposit,
999
+ offerSpec,
1000
+ address,
633
1001
  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
- });
1002
+ seatRef,
1003
+ );
647
1004
 
648
- return executor.executeOffer(offerSpec, seatRef => {
649
1005
  state.liveOffers.init(offerSpec.id, offerSpec);
650
- facets.helper.publishCurrentState();
651
1006
  state.liveOfferSeats.init(offerSpec.id, seatRef);
652
- });
1007
+
1008
+ // publish the live offers
1009
+ facets.helper.publishCurrentState();
1010
+
1011
+ // await so that any errors are caught and handled below
1012
+ await watchOfferOutcomes(watcher, seatRef);
1013
+ } catch (reason) {
1014
+ // This block only runs if the block above fails during one vat incarnation.
1015
+ facets.helper.logWalletError('IMMEDIATE OFFER ERROR:', reason);
1016
+
1017
+ // Update status to observers
1018
+ if (isUpgradeDisconnection(reason)) {
1019
+ // The offer watchers will reconnect. Don't reclaim or exit
1020
+ return;
1021
+ } else if (watcher) {
1022
+ // The watcher's onRejected will updateStatus()
1023
+ } else {
1024
+ facets.helper.updateStatus({
1025
+ error: reason.toString(),
1026
+ ...offerSpec,
1027
+ });
1028
+ }
1029
+
1030
+ // Backstop recovery, in case something very basic fails.
1031
+ if (offerSpec?.proposal?.give) {
1032
+ facets.payments
1033
+ .tryReclaimingWithdrawnPayments(offerSpec.id)
1034
+ .catch(e =>
1035
+ facets.helper.logWalletError(
1036
+ 'recovery failed reclaiming payments',
1037
+ e,
1038
+ ),
1039
+ );
1040
+ }
1041
+
1042
+ // XXX tests rely on throwing immediate errors, not covering the
1043
+ // error handling in the event the failure is after an upgrade
1044
+ throw reason;
1045
+ }
653
1046
  },
654
1047
  /**
655
1048
  * Take an offer's id, look up its seat, try to exit.
656
1049
  *
657
- * @param {import('./offers.js').OfferId} offerId
1050
+ * @param {OfferId} offerId
658
1051
  * @returns {Promise<void>}
659
1052
  * @throws if the seat can't be found or E(seatRef).tryExit() fails.
660
1053
  */
661
1054
  async tryExitOffer(offerId) {
1055
+ const { facets } = this;
1056
+ const amts = await facets.payments
1057
+ .tryReclaimingWithdrawnPayments(offerId)
1058
+ .catch(e => {
1059
+ facets.helper.logWalletError(
1060
+ 'recovery failed reclaiming payments',
1061
+ e,
1062
+ );
1063
+ return [];
1064
+ });
1065
+ if (amts.length > 0) {
1066
+ facets.helper.logWalletInfo('reclaimed', amts, 'from', offerId);
1067
+ }
662
1068
  const seatRef = this.state.liveOfferSeats.get(offerId);
663
1069
  await E(seatRef).tryExit();
664
1070
  },
665
1071
  },
1072
+
1073
+ invoke: {
1074
+ /**
1075
+ * @param {InvokeEntryMessage} message
1076
+ */
1077
+ async invokeEntry(message) {
1078
+ trace('invokeEntry', message);
1079
+ const { myStore } = this.state;
1080
+ const { resultStepWatcher } = this.facets;
1081
+
1082
+ const { targetName: name, method, args, saveResult, id } = message;
1083
+ myStore.has(name) || Fail`cannot invoke ${q(name)}: no such item`;
1084
+ const value = myStore.get(name);
1085
+ trace('entry', name, value);
1086
+ trace('invoke', value, '.', method, '(', args, ')');
1087
+ if (id) {
1088
+ const { updateRecorderKit } = this.state;
1089
+ void updateRecorderKit.recorder.write({
1090
+ updated: 'invocation',
1091
+ id,
1092
+ });
1093
+ }
1094
+ const callP = E(value)[method](...args);
1095
+ if (id || saveResult) {
1096
+ vowTools.watch(callP, resultStepWatcher, { id, saveResult });
1097
+ } else {
1098
+ void callP;
1099
+ }
1100
+ },
1101
+ },
1102
+
1103
+ resultStepWatcher: {
1104
+ /**
1105
+ * @param {unknown} result
1106
+ * @param {{ id?: string | number; saveResult?: ResultPlan }} opts
1107
+ */
1108
+ onFulfilled(result, opts) {
1109
+ trace('resultStepWatcher opts', opts);
1110
+ const { id, saveResult } = opts;
1111
+ if (saveResult) {
1112
+ this.facets.helper.saveEntry(saveResult, result);
1113
+ }
1114
+ const passStyle = passStyleOf(result);
1115
+ const { updateRecorderKit } = this.state;
1116
+ if (id) {
1117
+ void updateRecorderKit.recorder.write({
1118
+ updated: 'invocation',
1119
+ id,
1120
+ result: {
1121
+ ...(saveResult?.name ? { name: saveResult.name } : {}),
1122
+ passStyle,
1123
+ },
1124
+ });
1125
+ }
1126
+ },
1127
+ /**
1128
+ * @param {unknown} reason
1129
+ * @param {{ id: string | number; saveResult?: ResultPlan }} opts
1130
+ */
1131
+ onRejected(reason, opts) {
1132
+ trace('rejected', reason, opts);
1133
+ if (opts.id) {
1134
+ const { updateRecorderKit } = this.state;
1135
+ void updateRecorderKit.recorder.write({
1136
+ updated: 'invocation',
1137
+ id: opts.id,
1138
+ error: String(reason),
1139
+ });
1140
+ }
1141
+ },
1142
+ },
1143
+
666
1144
  self: {
667
1145
  /**
668
- * Umarshals the actionCapData and delegates to the appropriate action handler.
1146
+ * Umarshals the actionCapData and delegates to the appropriate action
1147
+ * handler.
669
1148
  *
670
- * @param {import('@endo/marshal').CapData<string>} actionCapData of type BridgeAction
1149
+ * @param {import('@endo/marshal').CapData<string | null>} actionCapData
1150
+ * of type BridgeAction
671
1151
  * @param {boolean} [canSpend]
672
1152
  * @returns {Promise<void>}
673
1153
  */
674
1154
  handleBridgeAction(actionCapData, canSpend = false) {
1155
+ const { facets, state } = this;
1156
+ const { offers, invoke } = facets;
675
1157
  const { publicMarshaller } = shared;
676
1158
 
677
- const { offers } = this.facets;
678
-
679
1159
  /** @param {Error} err */
680
1160
  const recordError = err => {
681
- const { address, updateRecorderKit } = this.state;
682
- console.error('wallet', address, 'handleBridgeAction error:', err);
1161
+ const { updateRecorderKit } = this.state;
1162
+ facets.helper.logWalletError('handleBridgeAction error:', err);
683
1163
  void updateRecorderKit.recorder.write({
684
1164
  updated: 'walletAction',
685
1165
  status: { error: err.message },
686
1166
  });
687
1167
  };
688
1168
 
1169
+ const walletHasNameHub = 'myStore' in state && state.myStore != null;
1170
+
689
1171
  // use E.when to retain distributed stack trace
690
1172
  return E.when(
691
1173
  E(publicMarshaller).fromCapData(actionCapData),
@@ -695,12 +1177,21 @@ export const prepareSmartWallet = (baggage, shared) => {
695
1177
  switch (action.method) {
696
1178
  case 'executeOffer': {
697
1179
  canSpend || Fail`executeOffer requires spend authority`;
1180
+ if (action.offer.saveResult != null && !walletHasNameHub) {
1181
+ Fail`executeOffer saveResult requires a new smart wallet with myStore`;
1182
+ }
1183
+
698
1184
  return offers.executeOffer(action.offer);
699
1185
  }
700
1186
  case 'tryExitOffer': {
701
1187
  assert(canSpend, 'tryExitOffer requires spend authority');
702
1188
  return offers.tryExitOffer(action.offerId);
703
1189
  }
1190
+ case 'invokeEntry': {
1191
+ walletHasNameHub ||
1192
+ Fail`invokeEntry requires a new smart wallet with myStore`;
1193
+ return invoke.invokeEntry(action.message);
1194
+ }
704
1195
  default: {
705
1196
  throw Fail`invalid handle bridge action ${q(action)}`;
706
1197
  }
@@ -719,19 +1210,26 @@ export const prepareSmartWallet = (baggage, shared) => {
719
1210
  getDepositFacet() {
720
1211
  return this.facets.deposit;
721
1212
  },
1213
+ getInvokeFacet() {
1214
+ return this.facets.invoke;
1215
+ },
722
1216
  getOffersFacet() {
723
1217
  return this.facets.offers;
724
1218
  },
725
1219
  /** @deprecated use getPublicTopics */
726
1220
  getCurrentSubscriber() {
727
- return this.state.currentRecorderKit.subscriber;
1221
+ const { state } = this;
1222
+ return state.currentRecorderKit.subscriber;
728
1223
  },
729
1224
  /** @deprecated use getPublicTopics */
730
1225
  getUpdatesSubscriber() {
731
- return this.state.updateRecorderKit.subscriber;
1226
+ const { state } = this;
1227
+ return state.updateRecorderKit.subscriber;
732
1228
  },
733
1229
  getPublicTopics() {
734
- const { currentRecorderKit, updateRecorderKit } = this.state;
1230
+ const { state } = this;
1231
+ const { currentRecorderKit, updateRecorderKit } = state;
1232
+
735
1233
  return harden({
736
1234
  current: {
737
1235
  description: 'Current state of wallet',
@@ -752,14 +1250,18 @@ export const prepareSmartWallet = (baggage, shared) => {
752
1250
  const { invitationPurse } = state;
753
1251
  const { helper } = facets;
754
1252
 
755
- // @ts-expect-error RemotePurse cast
756
1253
  void helper.watchPurse(invitationPurse);
757
1254
  },
758
1255
  },
759
1256
  );
760
1257
 
761
1258
  /**
762
- * @param {Omit<UniqueParams, 'currentStorageNode' | 'walletStorageNode'> & {walletStorageNode: ERef<StorageNode>}} uniqueWithoutChildNodes
1259
+ * @param {Omit<
1260
+ * UniqueParams,
1261
+ * 'currentStorageNode' | 'walletStorageNode'
1262
+ * > & {
1263
+ * walletStorageNode: ERef<StorageNode>;
1264
+ * }} uniqueWithoutChildNodes
763
1265
  */
764
1266
  const makeSmartWallet = async uniqueWithoutChildNodes => {
765
1267
  const [walletStorageNode, currentStorageNode] = await Promise.all([
@@ -778,5 +1280,4 @@ export const prepareSmartWallet = (baggage, shared) => {
778
1280
  return makeSmartWallet;
779
1281
  };
780
1282
  harden(prepareSmartWallet);
781
-
782
- /** @typedef {Awaited<ReturnType<ReturnType<typeof prepareSmartWallet>>>} SmartWallet */
1283
+ /** @typedef {EReturn<EReturn<typeof prepareSmartWallet>>} SmartWallet */