@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.
- package/package.json +35 -26
- package/src/index.d.ts +2 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +2 -0
- package/src/invitations.d.ts +14 -10
- package/src/invitations.d.ts.map +1 -1
- package/src/invitations.js +35 -32
- package/src/marshal-contexts.d.ts +44 -41
- package/src/marshal-contexts.d.ts.map +1 -1
- package/src/marshal-contexts.js +68 -61
- package/src/offerWatcher.d.ts +52 -0
- package/src/offerWatcher.d.ts.map +1 -0
- package/src/offerWatcher.js +329 -0
- package/src/offers.d.ts +7 -31
- package/src/offers.d.ts.map +1 -1
- package/src/offers.js +9 -183
- 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 +60 -0
- package/src/proposals/upgrade-walletFactory-proposal.d.ts +1 -1
- package/src/proposals/upgrade-walletFactory-proposal.d.ts.map +1 -1
- package/src/proposals/upgrade-walletFactory-proposal.js +46 -23
- package/src/smartWallet.d.ts +101 -66
- package/src/smartWallet.d.ts.map +1 -1
- package/src/smartWallet.js +576 -216
- package/src/typeGuards.d.ts +1 -1
- package/src/types-index.d.ts +2 -0
- package/src/types-index.js +2 -0
- package/src/types.d.ts +35 -41
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +90 -0
- package/src/utils.d.ts +17 -14
- package/src/utils.d.ts.map +1 -1
- package/src/utils.js +19 -6
- package/src/walletFactory.d.ts +24 -78
- package/src/walletFactory.d.ts.map +1 -1
- package/src/walletFactory.js +61 -37
- package/CHANGELOG.md +0 -180
- 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,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 {
|
|
12
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
81
|
+
* method: 'executeOffer';
|
|
82
|
+
* offer: OfferSpec;
|
|
49
83
|
* }} ExecuteOfferAction
|
|
50
84
|
*/
|
|
51
85
|
|
|
52
86
|
/**
|
|
53
87
|
* @typedef {{
|
|
54
|
-
* method: 'tryExitOffer'
|
|
55
|
-
* 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
|
|
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
|
|
70
|
-
* We also considered having a vstorage key for each offer but for now
|
|
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
|
-
*
|
|
74
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
84
|
-
* offerToUsedInvitation:
|
|
85
|
-
* offerToPublicSubscriberPaths:
|
|
86
|
-
*
|
|
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'
|
|
133
|
+
* @typedef {{ updated: 'offerStatus'; status: OfferStatus }
|
|
92
134
|
* | { updated: 'balance'; currentAmount: Amount }
|
|
93
|
-
* | { updated: 'walletAction'; status: { error: string } }
|
|
94
|
-
*
|
|
135
|
+
* | { updated: 'walletAction'; status: { error: string } }} UpdateRecord
|
|
136
|
+
* Record of an update to the state of this wallet.
|
|
95
137
|
*
|
|
96
|
-
*
|
|
138
|
+
* Client is responsible for coalescing updates into a current state. See
|
|
139
|
+
* `coalesceUpdates` utility.
|
|
97
140
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
-
*
|
|
102
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* purseBalances
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
-
*
|
|
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
|
|
345
|
-
* This is necessary because the class kit
|
|
346
|
-
*
|
|
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 {
|
|
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<
|
|
559
|
+
/** @type {(purse: ERef<Purse>) => Promise<void>} */
|
|
419
560
|
async watchPurse(purseRef) {
|
|
420
|
-
const {
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
451
|
-
*
|
|
452
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
518
|
-
|
|
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
|
|
881
|
+
* Take an offer description provided in capData, augment it with
|
|
882
|
+
* payments and call zoe.offer()
|
|
548
883
|
*
|
|
549
|
-
* @param {
|
|
550
|
-
* @returns {Promise<void>} after the offer has been both seated and
|
|
551
|
-
*
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
|
|
569
|
-
info: (...args) => console.info('wallet', address, ...args),
|
|
570
|
-
error: (...args) => console.error('wallet', address, ...args),
|
|
571
|
-
};
|
|
898
|
+
await null;
|
|
572
899
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
status: offerStatus,
|
|
616
|
-
});
|
|
914
|
+
// 1. Prepare values and validate synchronously.
|
|
915
|
+
const { proposal } = offerSpec;
|
|
617
916
|
|
|
618
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
635
|
-
|
|
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 {
|
|
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
|
|
1018
|
+
* Umarshals the actionCapData and delegates to the appropriate action
|
|
1019
|
+
* handler.
|
|
669
1020
|
*
|
|
670
|
-
* @param {import('@endo/marshal').CapData<string>} actionCapData
|
|
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 {
|
|
682
|
-
|
|
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
|
-
|
|
1079
|
+
const { state } = this;
|
|
1080
|
+
return state.currentRecorderKit.subscriber;
|
|
728
1081
|
},
|
|
729
1082
|
/** @deprecated use getPublicTopics */
|
|
730
1083
|
getUpdatesSubscriber() {
|
|
731
|
-
|
|
1084
|
+
const { state } = this;
|
|
1085
|
+
return state.updateRecorderKit.subscriber;
|
|
732
1086
|
},
|
|
733
1087
|
getPublicTopics() {
|
|
734
|
-
const {
|
|
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<
|
|
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([
|