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