@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.
- package/package.json +43 -29
- package/src/index.d.ts +2 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +6 -0
- package/src/invitations.d.ts +14 -10
- package/src/invitations.d.ts.map +1 -1
- package/src/invitations.js +39 -33
- package/src/marshal-contexts.d.ts +45 -41
- package/src/marshal-contexts.d.ts.map +1 -1
- package/src/marshal-contexts.js +69 -61
- package/src/offerWatcher.d.ts +54 -0
- package/src/offerWatcher.d.ts.map +1 -0
- package/src/offerWatcher.js +343 -0
- package/src/offers.d.ts +60 -30
- package/src/offers.d.ts.map +1 -1
- package/src/offers.js +38 -182
- package/src/proposals/upgrade-wallet-factory2-proposal.js +61 -0
- package/src/proposals/upgrade-walletFactory-proposal.js +46 -23
- package/src/smartWallet.d.ts +132 -68
- package/src/smartWallet.d.ts.map +1 -1
- package/src/smartWallet.js +718 -217
- package/src/typeGuards.d.ts +1 -1
- package/src/typeGuards.js +29 -1
- package/src/types-index.d.ts +2 -0
- package/src/types-index.js +2 -0
- package/src/types.d.ts +36 -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 +64 -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/proposals/upgrade-walletFactory-proposal.d.ts +0 -17
- package/src/proposals/upgrade-walletFactory-proposal.d.ts.map +0 -1
package/src/smartWallet.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 {
|
|
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,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 {
|
|
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
|
+
/**
|
|
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
|
-
*
|
|
48
|
-
*
|
|
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:
|
|
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
|
|
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
|
|
70
|
-
* We also considered having a vstorage key for each offer but for now
|
|
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
|
-
*
|
|
74
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
84
|
-
* offerToUsedInvitation:
|
|
85
|
-
* offerToPublicSubscriberPaths:
|
|
86
|
-
*
|
|
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'
|
|
135
|
+
* @typedef {{ updated: 'offerStatus'; status: OfferStatus }
|
|
92
136
|
* | { updated: 'balance'; currentAmount: Amount }
|
|
93
137
|
* | { updated: 'walletAction'; status: { error: string } }
|
|
94
|
-
*
|
|
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
|
-
*
|
|
146
|
+
* Client is responsible for coalescing updates into a current state. See
|
|
147
|
+
* `coalesceUpdates` utility.
|
|
97
148
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
-
*
|
|
102
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* purseBalances
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
-
*
|
|
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
|
|
345
|
-
* This is necessary because the class kit
|
|
346
|
-
*
|
|
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 {
|
|
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<
|
|
586
|
+
/** @type {(purse: ERef<Purse>) => Promise<void>} */
|
|
419
587
|
async watchPurse(purseRef) {
|
|
420
|
-
const {
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
|
451
|
-
*
|
|
452
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
518
|
-
|
|
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
|
|
937
|
+
* Take an offer description provided in capData, augment it with
|
|
938
|
+
* payments and call zoe.offer()
|
|
548
939
|
*
|
|
549
|
-
* @param {
|
|
550
|
-
* @returns {Promise<void>} after the offer has been both seated and
|
|
551
|
-
*
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
|
|
569
|
-
info: (...args) => console.info('wallet', address, ...args),
|
|
570
|
-
error: (...args) => console.error('wallet', address, ...args),
|
|
571
|
-
};
|
|
954
|
+
await null;
|
|
572
955
|
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
status: offerStatus,
|
|
616
|
-
});
|
|
970
|
+
// 1. Prepare values and validate synchronously.
|
|
971
|
+
const { proposal } = offerSpec;
|
|
617
972
|
|
|
618
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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 {
|
|
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
|
|
1146
|
+
* Umarshals the actionCapData and delegates to the appropriate action
|
|
1147
|
+
* handler.
|
|
669
1148
|
*
|
|
670
|
-
* @param {import('@endo/marshal').CapData<string>} actionCapData
|
|
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 {
|
|
682
|
-
|
|
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
|
-
|
|
1221
|
+
const { state } = this;
|
|
1222
|
+
return state.currentRecorderKit.subscriber;
|
|
728
1223
|
},
|
|
729
1224
|
/** @deprecated use getPublicTopics */
|
|
730
1225
|
getUpdatesSubscriber() {
|
|
731
|
-
|
|
1226
|
+
const { state } = this;
|
|
1227
|
+
return state.updateRecorderKit.subscriber;
|
|
732
1228
|
},
|
|
733
1229
|
getPublicTopics() {
|
|
734
|
-
const {
|
|
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<
|
|
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 */
|