@agoric/smart-wallet 0.5.3 → 0.5.4-calypso-dev-84eb287.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 +34 -24
- package/src/invitations.d.ts +14 -10
- package/src/invitations.d.ts.map +1 -1
- package/src/invitations.js +32 -28
- package/src/marshal-contexts.d.ts +44 -41
- package/src/marshal-contexts.d.ts.map +1 -1
- package/src/marshal-contexts.js +65 -58
- package/src/offerWatcher.d.ts +48 -0
- package/src/offerWatcher.d.ts.map +1 -0
- package/src/offerWatcher.js +311 -0
- package/src/offers.d.ts +7 -31
- package/src/offers.d.ts.map +1 -1
- package/src/offers.js +9 -182
- 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 +17 -0
- package/src/proposals/upgrade-walletFactory-proposal.d.ts.map +1 -0
- package/src/proposals/upgrade-walletFactory-proposal.js +127 -0
- package/src/smartWallet.d.ts +116 -69
- package/src/smartWallet.d.ts.map +1 -1
- package/src/smartWallet.js +830 -202
- package/src/typeGuards.d.ts +1 -1
- package/src/types.d.ts +24 -13
- package/src/utils.d.ts +17 -14
- package/src/utils.d.ts.map +1 -1
- package/src/utils.js +18 -6
- package/src/walletFactory.d.ts +26 -79
- package/src/walletFactory.d.ts.map +1 -1
- package/src/walletFactory.js +103 -42
- package/CHANGELOG.md +0 -172
- 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
|
+
import { E } from '@endo/far';
|
|
1
2
|
import {
|
|
2
|
-
AmountMath,
|
|
3
3
|
AmountShape,
|
|
4
4
|
BrandShape,
|
|
5
5
|
DisplayInfoShape,
|
|
@@ -7,152 +7,268 @@ import {
|
|
|
7
7
|
PaymentShape,
|
|
8
8
|
PurseShape,
|
|
9
9
|
} from '@agoric/ertp';
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
deeplyFulfilledObject,
|
|
12
|
+
makeTracer,
|
|
13
|
+
objectMap,
|
|
14
|
+
StorageNodeShape,
|
|
15
|
+
} from '@agoric/internal';
|
|
16
|
+
import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';
|
|
12
17
|
import { M, mustMatch } from '@agoric/store';
|
|
13
|
-
import {
|
|
14
|
-
|
|
18
|
+
import {
|
|
19
|
+
appendToStoredArray,
|
|
20
|
+
provideLazy,
|
|
21
|
+
} from '@agoric/store/src/stores/store-utils.js';
|
|
22
|
+
import {
|
|
23
|
+
makeScalarBigMapStore,
|
|
24
|
+
makeScalarBigWeakMapStore,
|
|
25
|
+
prepareExoClass,
|
|
26
|
+
prepareExoClassKit,
|
|
27
|
+
provide,
|
|
28
|
+
watchPromise,
|
|
29
|
+
} from '@agoric/vat-data';
|
|
15
30
|
import {
|
|
16
31
|
prepareRecorderKit,
|
|
17
32
|
SubscriberShape,
|
|
18
33
|
TopicsRecordShape,
|
|
19
34
|
} from '@agoric/zoe/src/contractSupport/index.js';
|
|
20
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
AmountKeywordRecordShape,
|
|
37
|
+
PaymentPKeywordRecordShape,
|
|
38
|
+
} from '@agoric/zoe/src/typeGuards.js';
|
|
39
|
+
|
|
21
40
|
import { makeInvitationsHelper } from './invitations.js';
|
|
22
|
-
import { makeOfferExecutor } from './offers.js';
|
|
23
41
|
import { shape } from './typeGuards.js';
|
|
24
42
|
import { objectMapStoragePath } from './utils.js';
|
|
43
|
+
import { prepareOfferWatcher, watchOfferOutcomes } from './offerWatcher.js';
|
|
44
|
+
|
|
45
|
+
/** @import {OfferId, OfferStatus} from './offers.js'; */
|
|
25
46
|
|
|
26
47
|
const { Fail, quote: q } = assert;
|
|
27
48
|
|
|
49
|
+
const trace = makeTracer('SmrtWlt');
|
|
50
|
+
|
|
28
51
|
/**
|
|
29
52
|
* @file Smart wallet module
|
|
30
|
-
*
|
|
31
|
-
|
|
53
|
+
* @see {@link ../README.md} }
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/** @typedef {number | string} OfferId */
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {{
|
|
60
|
+
* id: OfferId;
|
|
61
|
+
* invitationSpec: import('./invitations').InvitationSpec;
|
|
62
|
+
* proposal: Proposal;
|
|
63
|
+
* offerArgs?: any;
|
|
64
|
+
* }} OfferSpec
|
|
32
65
|
*/
|
|
33
66
|
|
|
34
67
|
/**
|
|
35
68
|
* @typedef {{
|
|
36
|
-
*
|
|
37
|
-
*
|
|
69
|
+
* logger: {
|
|
70
|
+
* info: (...args: any[]) => void;
|
|
71
|
+
* error: (...args: any[]) => void;
|
|
72
|
+
* };
|
|
73
|
+
* makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher;
|
|
74
|
+
* invitationFromSpec: ERef<Invitation>;
|
|
75
|
+
* }} ExecutorPowers
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {{
|
|
80
|
+
* method: 'executeOffer';
|
|
81
|
+
* offer: OfferSpec;
|
|
38
82
|
* }} ExecuteOfferAction
|
|
39
83
|
*/
|
|
40
84
|
|
|
41
85
|
/**
|
|
42
86
|
* @typedef {{
|
|
43
|
-
* method: 'tryExitOffer'
|
|
44
|
-
* offerId:
|
|
87
|
+
* method: 'tryExitOffer';
|
|
88
|
+
* offerId: OfferId;
|
|
45
89
|
* }} TryExitOfferAction
|
|
46
90
|
*/
|
|
47
91
|
|
|
48
92
|
// Discriminated union. Possible future messages types:
|
|
49
93
|
// maybe suggestIssuer for https://github.com/Agoric/agoric-sdk/issues/6132
|
|
50
94
|
// setting petnames and adding brands for https://github.com/Agoric/agoric-sdk/issues/6126
|
|
51
|
-
/**
|
|
52
|
-
* @typedef { ExecuteOfferAction | TryExitOfferAction } BridgeAction
|
|
53
|
-
*/
|
|
95
|
+
/** @typedef {ExecuteOfferAction | TryExitOfferAction} BridgeAction */
|
|
54
96
|
|
|
55
97
|
/**
|
|
56
|
-
* Purses is an array to support a future requirement of multiple purses per
|
|
98
|
+
* Purses is an array to support a future requirement of multiple purses per
|
|
99
|
+
* brand.
|
|
57
100
|
*
|
|
58
|
-
* Each map is encoded as an array of entries because a Map doesn't serialize
|
|
59
|
-
* We also considered having a vstorage key for each offer but for now
|
|
101
|
+
* Each map is encoded as an array of entries because a Map doesn't serialize
|
|
102
|
+
* directly. We also considered having a vstorage key for each offer but for now
|
|
103
|
+
* are sticking with this design.
|
|
60
104
|
*
|
|
61
105
|
* Cons
|
|
62
|
-
*
|
|
63
|
-
*
|
|
106
|
+
*
|
|
107
|
+
* - Reserializes previously written results when a new result is added
|
|
108
|
+
* - Optimizes reads though writes are on-chain (~100 machines) and reads are
|
|
109
|
+
* off-chain (to 1 machine)
|
|
64
110
|
*
|
|
65
111
|
* Pros
|
|
66
|
-
* - Reading all offer results happens much more (>100) often than storing a new offer result
|
|
67
|
-
* - Reserialization and writes are paid in execution gas, whereas reads are not
|
|
68
112
|
*
|
|
69
|
-
*
|
|
113
|
+
* - Reading all offer results happens much more (>100) often than storing a new
|
|
114
|
+
* offer result
|
|
115
|
+
* - Reserialization and writes are paid in execution gas, whereas reads are not
|
|
116
|
+
*
|
|
117
|
+
* This design should be revisited if ever batch querying across vstorage keys
|
|
118
|
+
* become cheaper or reads be paid.
|
|
70
119
|
*
|
|
71
120
|
* @typedef {{
|
|
72
|
-
* purses:
|
|
73
|
-
* offerToUsedInvitation:
|
|
74
|
-
* offerToPublicSubscriberPaths:
|
|
75
|
-
*
|
|
121
|
+
* purses: { brand: Brand; balance: Amount }[];
|
|
122
|
+
* offerToUsedInvitation: [offerId: string, usedInvitation: Amount][];
|
|
123
|
+
* offerToPublicSubscriberPaths: [
|
|
124
|
+
* offerId: string,
|
|
125
|
+
* publicTopics: { [subscriberName: string]: string },
|
|
126
|
+
* ][];
|
|
127
|
+
* liveOffers: [OfferId, OfferStatus][];
|
|
76
128
|
* }} CurrentWalletRecord
|
|
77
129
|
*/
|
|
78
130
|
|
|
79
131
|
/**
|
|
80
|
-
* @typedef {{ updated: 'offerStatus'
|
|
132
|
+
* @typedef {{ updated: 'offerStatus'; status: OfferStatus }
|
|
81
133
|
* | { updated: 'balance'; currentAmount: Amount }
|
|
82
|
-
* | { updated: 'walletAction'; status: { error: string } }
|
|
83
|
-
*
|
|
134
|
+
* | { updated: 'walletAction'; status: { error: string } }} UpdateRecord
|
|
135
|
+
* Record of an update to the state of this wallet.
|
|
84
136
|
*
|
|
85
|
-
*
|
|
137
|
+
* Client is responsible for coalescing updates into a current state. See
|
|
138
|
+
* `coalesceUpdates` utility.
|
|
86
139
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
140
|
+
* The reason for this burden on the client is that publishing the full history
|
|
141
|
+
* of offers with each change is untenable.
|
|
89
142
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* the amount suffices.
|
|
143
|
+
* `balance` update supports forward-compatibility for more than one purse per
|
|
144
|
+
* brand. An additional key will be needed to disambiguate. For now the brand
|
|
145
|
+
* in the amount suffices.
|
|
93
146
|
*/
|
|
94
147
|
|
|
95
148
|
/**
|
|
96
149
|
* @typedef {{
|
|
97
|
-
* brand: Brand
|
|
98
|
-
* displayInfo: DisplayInfo
|
|
99
|
-
* issuer: Issuer
|
|
100
|
-
* petname: import('./types').Petname
|
|
150
|
+
* brand: Brand;
|
|
151
|
+
* displayInfo: DisplayInfo;
|
|
152
|
+
* issuer: Issuer;
|
|
153
|
+
* petname: import('./types.js').Petname;
|
|
101
154
|
* }} BrandDescriptor
|
|
102
|
-
*
|
|
155
|
+
* For use by clients to describe brands to users. Includes `displayInfo` to
|
|
156
|
+
* save a remote call.
|
|
103
157
|
*/
|
|
104
158
|
|
|
105
|
-
// imports
|
|
106
|
-
/** @typedef {import('./types').RemotePurse} RemotePurse */
|
|
107
|
-
|
|
108
159
|
/**
|
|
109
160
|
* @typedef {{
|
|
110
|
-
* address: string
|
|
111
|
-
* bank: ERef<import('@agoric/vats/src/vat-bank').Bank
|
|
112
|
-
* currentStorageNode: StorageNode
|
|
113
|
-
* invitationPurse: Purse<'set'
|
|
114
|
-
* walletStorageNode: StorageNode
|
|
161
|
+
* address: string;
|
|
162
|
+
* bank: ERef<import('@agoric/vats/src/vat-bank.js').Bank>;
|
|
163
|
+
* currentStorageNode: StorageNode;
|
|
164
|
+
* invitationPurse: Purse<'set', InvitationDetails>;
|
|
165
|
+
* walletStorageNode: StorageNode;
|
|
115
166
|
* }} UniqueParams
|
|
116
167
|
*
|
|
168
|
+
*
|
|
117
169
|
* @typedef {Pick<MapStore<Brand, BrandDescriptor>, 'has' | 'get' | 'values'>} BrandDescriptorRegistry
|
|
170
|
+
*
|
|
171
|
+
*
|
|
118
172
|
* @typedef {{
|
|
119
|
-
* agoricNames: ERef<import('@agoric/vats').NameHub
|
|
120
|
-
* registry: BrandDescriptorRegistry
|
|
121
|
-
* invitationIssuer: Issuer<'set'
|
|
122
|
-
* invitationBrand: Brand<'set'
|
|
123
|
-
* invitationDisplayInfo: DisplayInfo
|
|
124
|
-
* publicMarshaller: Marshaller
|
|
125
|
-
* zoe: ERef<ZoeService
|
|
173
|
+
* agoricNames: ERef<import('@agoric/vats').NameHub>;
|
|
174
|
+
* registry: BrandDescriptorRegistry;
|
|
175
|
+
* invitationIssuer: Issuer<'set'>;
|
|
176
|
+
* invitationBrand: Brand<'set'>;
|
|
177
|
+
* invitationDisplayInfo: DisplayInfo;
|
|
178
|
+
* publicMarshaller: Marshaller;
|
|
179
|
+
* zoe: ERef<ZoeService>;
|
|
180
|
+
* secretWalletFactoryKey: any;
|
|
126
181
|
* }} SharedParams
|
|
127
182
|
*
|
|
128
|
-
* @typedef {ImmutableState & MutableState} State
|
|
129
|
-
* - `brandPurses` is precious and closely held. defined as late as possible to reduce its scope.
|
|
130
|
-
* - `offerToInvitationMakers` is precious and closely held.
|
|
131
|
-
* - `offerToPublicSubscriberPaths` is precious and closely held.
|
|
132
|
-
* - `purseBalances` is a cache of what we've received from purses. Held so we can publish all balances on change.
|
|
133
183
|
*
|
|
134
|
-
* @typedef {
|
|
135
|
-
*
|
|
136
|
-
* offerToInvitationMakers: MapStore<string, import('./types').RemoteInvitationMakers>,
|
|
137
|
-
* offerToPublicSubscriberPaths: MapStore<string, Record<string, string>>,
|
|
138
|
-
* offerToUsedInvitation: MapStore<string, Amount>,
|
|
139
|
-
* purseBalances: MapStore<RemotePurse, Amount>,
|
|
140
|
-
* updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<UpdateRecord>,
|
|
141
|
-
* currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<CurrentWalletRecord>,
|
|
142
|
-
* liveOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>,
|
|
143
|
-
* liveOfferSeats: WeakMapStore<import('./offers.js').OfferId, UserSeat<unknown>>,
|
|
144
|
-
* }>} ImmutableState
|
|
184
|
+
* @typedef {ImmutableState & MutableState} State - `brandPurses` is precious
|
|
185
|
+
* and closely held. defined as late as possible to reduce its scope.
|
|
145
186
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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
|
+
*
|
|
211
|
+
*
|
|
212
|
+
* @typedef {BrandDescriptor & { purse: Purse }} PurseRecord
|
|
213
|
+
*
|
|
214
|
+
* @typedef {{}} MutableState
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* NameHub reverse-lookup, finding 0 or more names for a target value
|
|
219
|
+
*
|
|
220
|
+
* TODO: consider moving to nameHub.js?
|
|
221
|
+
*
|
|
222
|
+
* @param {unknown} target - passable Key
|
|
223
|
+
* @param {ERef<import('@agoric/vats').NameHub>} nameHub
|
|
224
|
+
*/
|
|
225
|
+
const namesOf = async (target, nameHub) => {
|
|
226
|
+
const entries = await E(nameHub).entries();
|
|
227
|
+
const matches = [];
|
|
228
|
+
for (const [name, candidate] of entries) {
|
|
229
|
+
if (candidate === target) {
|
|
230
|
+
matches.push(name);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return harden(matches);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check that an issuer and its brand belong to each other.
|
|
238
|
+
*
|
|
239
|
+
* TODO: move to ERTP?
|
|
240
|
+
*
|
|
241
|
+
* @param {Issuer} issuer
|
|
242
|
+
* @param {Brand} brand
|
|
243
|
+
* @returns {Promise<boolean>} true iff the the brand and issuer match
|
|
148
244
|
*/
|
|
245
|
+
const checkMutual = (issuer, brand) =>
|
|
246
|
+
Promise.all([
|
|
247
|
+
E(issuer)
|
|
248
|
+
.getBrand()
|
|
249
|
+
.then(b => b === brand),
|
|
250
|
+
E(brand).isMyIssuer(issuer),
|
|
251
|
+
]).then(checks => checks.every(Boolean));
|
|
252
|
+
|
|
253
|
+
export const BRAND_TO_PURSES_KEY = 'brandToPurses';
|
|
254
|
+
|
|
255
|
+
const getBrandToPurses = (walletPurses, key) => {
|
|
256
|
+
const brandToPurses = provideLazy(walletPurses, key, _k => {
|
|
257
|
+
/** @type {MapStore<Brand, PurseRecord[]>} */
|
|
258
|
+
const store = makeScalarBigMapStore('purses by brand', {
|
|
259
|
+
durable: true,
|
|
260
|
+
});
|
|
261
|
+
return store;
|
|
262
|
+
});
|
|
263
|
+
return brandToPurses;
|
|
264
|
+
};
|
|
149
265
|
|
|
150
266
|
/**
|
|
151
267
|
* @param {import('@agoric/vat-data').Baggage} baggage
|
|
152
268
|
* @param {SharedParams} shared
|
|
153
269
|
*/
|
|
154
270
|
export const prepareSmartWallet = (baggage, shared) => {
|
|
155
|
-
const { registry:
|
|
271
|
+
const { registry: _r, ...passableShared } = shared;
|
|
156
272
|
mustMatch(
|
|
157
273
|
harden(passableShared),
|
|
158
274
|
harden({
|
|
@@ -162,13 +278,82 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
162
278
|
invitationDisplayInfo: DisplayInfoShape,
|
|
163
279
|
publicMarshaller: M.remotable('Marshaller'),
|
|
164
280
|
zoe: M.eref(M.remotable('ZoeService')),
|
|
281
|
+
|
|
282
|
+
// known only to smartWallets and walletFactory, this allows the
|
|
283
|
+
// walletFactory to invoke functions on the self facet that no one else
|
|
284
|
+
// can. Used to protect the upgrade-to-incarnation 2 repair. This can be
|
|
285
|
+
// dropped once the repair has taken place.
|
|
286
|
+
secretWalletFactoryKey: M.any(),
|
|
165
287
|
}),
|
|
166
288
|
);
|
|
167
289
|
|
|
168
290
|
const makeRecorderKit = prepareRecorderKit(baggage, shared.publicMarshaller);
|
|
169
291
|
|
|
292
|
+
const walletPurses = provide(baggage, BRAND_TO_PURSES_KEY, () => {
|
|
293
|
+
trace('make purses by wallet and save in baggage at', BRAND_TO_PURSES_KEY);
|
|
294
|
+
/** @type {WeakMapStore<unknown, MapStore<Brand, PurseRecord[]>>} */
|
|
295
|
+
const store = makeScalarBigWeakMapStore('purses by wallet', {
|
|
296
|
+
durable: true,
|
|
297
|
+
});
|
|
298
|
+
return store;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const makeOfferWatcher = prepareOfferWatcher(baggage);
|
|
302
|
+
|
|
303
|
+
const updateShape = {
|
|
304
|
+
value: AmountShape,
|
|
305
|
+
updateCount: M.bigint(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const NotifierShape = M.remotable();
|
|
309
|
+
const amountWatcherGuard = M.interface('paymentWatcher', {
|
|
310
|
+
onFulfilled: M.call(updateShape, NotifierShape).returns(),
|
|
311
|
+
onRejected: M.call(M.any(), NotifierShape).returns(M.promise()),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const prepareAmountWatcher = () =>
|
|
315
|
+
prepareExoClass(
|
|
316
|
+
baggage,
|
|
317
|
+
'AmountWatcher',
|
|
318
|
+
amountWatcherGuard,
|
|
319
|
+
/**
|
|
320
|
+
* @param {Purse} purse
|
|
321
|
+
* @param {ReturnType<makeWalletWithResolvedStorageNodes>['helper']} helper
|
|
322
|
+
*/
|
|
323
|
+
(purse, helper) => ({ purse, helper }),
|
|
324
|
+
{
|
|
325
|
+
/**
|
|
326
|
+
* @param {{ value: Amount; updateCount: bigint | undefined }} updateRecord
|
|
327
|
+
* @param {Notifier<Amount>} notifier
|
|
328
|
+
* @returns {void}
|
|
329
|
+
*/
|
|
330
|
+
onFulfilled(updateRecord, notifier) {
|
|
331
|
+
const { helper, purse } = this.state;
|
|
332
|
+
helper.updateBalance(purse, updateRecord.value);
|
|
333
|
+
helper.watchNextBalance(
|
|
334
|
+
this.self,
|
|
335
|
+
notifier,
|
|
336
|
+
updateRecord.updateCount,
|
|
337
|
+
);
|
|
338
|
+
},
|
|
339
|
+
/**
|
|
340
|
+
* @param {unknown} err
|
|
341
|
+
* @returns {Promise<void>}
|
|
342
|
+
*/
|
|
343
|
+
onRejected(err) {
|
|
344
|
+
const { helper, purse } = this.state;
|
|
345
|
+
if (isUpgradeDisconnection(err)) {
|
|
346
|
+
return helper.watchPurse(purse); // retry
|
|
347
|
+
}
|
|
348
|
+
helper.logWalletError(`failed amount observer`, err);
|
|
349
|
+
throw err;
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const makeAmountWatcher = prepareAmountWatcher();
|
|
355
|
+
|
|
170
356
|
/**
|
|
171
|
-
*
|
|
172
357
|
* @param {UniqueParams} unique
|
|
173
358
|
* @returns {State}
|
|
174
359
|
*/
|
|
@@ -232,6 +417,9 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
232
417
|
liveOfferSeats: makeScalarBigMapStore('live offer seats', {
|
|
233
418
|
durable: true,
|
|
234
419
|
}),
|
|
420
|
+
liveOfferPayments: makeScalarBigMapStore('live offer payments', {
|
|
421
|
+
durable: true,
|
|
422
|
+
}),
|
|
235
423
|
};
|
|
236
424
|
|
|
237
425
|
return {
|
|
@@ -245,12 +433,39 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
245
433
|
helper: M.interface('helperFacetI', {
|
|
246
434
|
assertUniqueOfferId: M.call(M.string()).returns(),
|
|
247
435
|
updateBalance: M.call(PurseShape, AmountShape).optional('init').returns(),
|
|
436
|
+
getPurseIfKnownBrand: M.call(BrandShape)
|
|
437
|
+
.optional(M.eref(M.remotable()))
|
|
438
|
+
.returns(M.promise()),
|
|
248
439
|
publishCurrentState: M.call().returns(),
|
|
249
440
|
watchPurse: M.call(M.eref(PurseShape)).returns(M.promise()),
|
|
441
|
+
watchNextBalance: M.call(M.any(), NotifierShape, M.bigint()).returns(),
|
|
442
|
+
repairUnwatchedSeats: M.call().returns(M.promise()),
|
|
443
|
+
repairUnwatchedPurses: M.call().returns(M.promise()),
|
|
444
|
+
updateStatus: M.call(M.any()).returns(),
|
|
445
|
+
addContinuingOffer: M.call(
|
|
446
|
+
M.or(M.number(), M.string()),
|
|
447
|
+
AmountShape,
|
|
448
|
+
M.remotable('InvitationMaker'),
|
|
449
|
+
M.or(M.record(), M.undefined()),
|
|
450
|
+
).returns(M.promise()),
|
|
451
|
+
purseForBrand: M.call(BrandShape).returns(M.promise()),
|
|
452
|
+
logWalletInfo: M.call().rest(M.arrayOf(M.any())).returns(),
|
|
453
|
+
logWalletError: M.call().rest(M.arrayOf(M.any())).returns(),
|
|
454
|
+
getLiveOfferPayments: M.call().returns(M.remotable('mapStore')),
|
|
250
455
|
}),
|
|
456
|
+
|
|
251
457
|
deposit: M.interface('depositFacetI', {
|
|
252
458
|
receive: M.callWhen(M.await(M.eref(PaymentShape))).returns(AmountShape),
|
|
253
459
|
}),
|
|
460
|
+
payments: M.interface('payments support', {
|
|
461
|
+
withdrawGive: M.call(
|
|
462
|
+
AmountKeywordRecordShape,
|
|
463
|
+
M.or(M.number(), M.string()),
|
|
464
|
+
).returns(PaymentPKeywordRecordShape),
|
|
465
|
+
tryReclaimingWithdrawnPayments: M.call(
|
|
466
|
+
M.or(M.number(), M.string()),
|
|
467
|
+
).returns(M.promise()),
|
|
468
|
+
}),
|
|
254
469
|
offers: M.interface('offers facet', {
|
|
255
470
|
executeOffer: M.call(shape.OfferSpec).returns(M.promise()),
|
|
256
471
|
tryExitOffer: M.call(M.scalar()).returns(M.promise()),
|
|
@@ -264,13 +479,16 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
264
479
|
getCurrentSubscriber: M.call().returns(SubscriberShape),
|
|
265
480
|
getUpdatesSubscriber: M.call().returns(SubscriberShape),
|
|
266
481
|
getPublicTopics: M.call().returns(TopicsRecordShape),
|
|
482
|
+
repairWalletForIncarnation2: M.call(M.any()).returns(),
|
|
267
483
|
}),
|
|
268
484
|
};
|
|
269
485
|
|
|
486
|
+
// TODO move to top level so its type can be exported
|
|
270
487
|
/**
|
|
271
|
-
* Make the durable object to return, but taking some parameters that are
|
|
272
|
-
* This is necessary because the class kit
|
|
273
|
-
*
|
|
488
|
+
* Make the durable object to return, but taking some parameters that are
|
|
489
|
+
* awaited by a wrapping function. This is necessary because the class kit
|
|
490
|
+
* construction helpers, `initState` and `finish` run synchronously and the
|
|
491
|
+
* child storage node must be awaited until we have durable promises.
|
|
274
492
|
*/
|
|
275
493
|
const makeWalletWithResolvedStorageNodes = prepareExoClassKit(
|
|
276
494
|
baggage,
|
|
@@ -287,6 +505,7 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
287
505
|
* @type {(id: string) => void}
|
|
288
506
|
*/
|
|
289
507
|
assertUniqueOfferId(id) {
|
|
508
|
+
const { facets } = this;
|
|
290
509
|
const {
|
|
291
510
|
liveOffers,
|
|
292
511
|
liveOfferSeats,
|
|
@@ -297,13 +516,14 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
297
516
|
const used =
|
|
298
517
|
liveOffers.has(id) ||
|
|
299
518
|
liveOfferSeats.has(id) ||
|
|
519
|
+
facets.helper.getLiveOfferPayments().has(id) ||
|
|
300
520
|
offerToInvitationMakers.has(id) ||
|
|
301
521
|
offerToPublicSubscriberPaths.has(id) ||
|
|
302
522
|
offerToUsedInvitation.has(id);
|
|
303
523
|
!used || Fail`cannot re-use offer id ${id}`;
|
|
304
524
|
},
|
|
305
525
|
/**
|
|
306
|
-
* @param {
|
|
526
|
+
* @param {Purse} purse
|
|
307
527
|
* @param {Amount<any>} balance
|
|
308
528
|
*/
|
|
309
529
|
updateBalance(purse, balance) {
|
|
@@ -342,36 +562,286 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
342
562
|
});
|
|
343
563
|
},
|
|
344
564
|
|
|
345
|
-
/** @type {(purse: ERef<
|
|
565
|
+
/** @type {(purse: ERef<Purse>) => Promise<void>} */
|
|
346
566
|
async watchPurse(purseRef) {
|
|
347
|
-
const {
|
|
567
|
+
const { helper } = this.facets;
|
|
568
|
+
|
|
569
|
+
// This would seem to fit the observeNotifier() pattern,
|
|
570
|
+
// but purse notifiers are not necessarily durable.
|
|
571
|
+
// If there is an error due to upgrade, retry watchPurse().
|
|
348
572
|
|
|
349
573
|
const purse = await purseRef; // promises don't fit in durable storage
|
|
574
|
+
const handler = makeAmountWatcher(purse, helper);
|
|
350
575
|
|
|
351
|
-
const { helper } = this.facets;
|
|
352
576
|
// publish purse's balance and changes
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
577
|
+
const notifier = await E(purse).getCurrentAmountNotifier();
|
|
578
|
+
const startP = E(notifier).getUpdateSince(undefined);
|
|
579
|
+
watchPromise(startP, handler, notifier);
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
watchNextBalance(handler, notifier, updateCount) {
|
|
583
|
+
const nextP = E(notifier).getUpdateSince(updateCount);
|
|
584
|
+
watchPromise(nextP, handler, notifier);
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Provide a purse given a NameHub of issuers and their brands.
|
|
589
|
+
*
|
|
590
|
+
* We currently support only one NameHub, agoricNames, and hence one
|
|
591
|
+
* purse per brand. But we store an array of them to facilitate a
|
|
592
|
+
* transition to decentralized introductions.
|
|
593
|
+
*
|
|
594
|
+
* @param {Brand} brand
|
|
595
|
+
* @param {ERef<import('@agoric/vats').NameHub>} known - namehub with
|
|
596
|
+
* brand, issuer branches
|
|
597
|
+
* @returns {Promise<Purse | undefined>} undefined if brand is not known
|
|
598
|
+
*/
|
|
599
|
+
async getPurseIfKnownBrand(brand, known) {
|
|
600
|
+
const { helper, self } = this.facets;
|
|
601
|
+
const brandToPurses = getBrandToPurses(walletPurses, self);
|
|
602
|
+
|
|
603
|
+
if (brandToPurses.has(brand)) {
|
|
604
|
+
const purses = brandToPurses.get(brand);
|
|
605
|
+
if (purses.length > 0) {
|
|
606
|
+
// UNTIL https://github.com/Agoric/agoric-sdk/issues/6126
|
|
607
|
+
// multiple purses
|
|
608
|
+
return purses[0].purse;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const found = await namesOf(brand, E(known).lookup('brand'));
|
|
613
|
+
if (found.length === 0) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
const [edgeName] = found;
|
|
617
|
+
const issuer = await E(known).lookup('issuer', edgeName);
|
|
618
|
+
|
|
619
|
+
// Even though we rely on this nameHub, double-check
|
|
620
|
+
// that the issuer and the brand belong to each other.
|
|
621
|
+
if (!(await checkMutual(issuer, brand))) {
|
|
622
|
+
// if they don't, it's not a "known" brand in a coherent way
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Accept the issuer; rely on it in future offers.
|
|
627
|
+
const [displayInfo, purse] = await Promise.all([
|
|
628
|
+
E(issuer).getDisplayInfo(),
|
|
629
|
+
E(issuer).makeEmptyPurse(),
|
|
630
|
+
]);
|
|
631
|
+
|
|
632
|
+
// adopt edgeName as petname
|
|
633
|
+
// NOTE: for decentralized introductions, qualify edgename by nameHub petname
|
|
634
|
+
const petname = edgeName;
|
|
635
|
+
const assetInfo = { petname, brand, issuer, purse, displayInfo };
|
|
636
|
+
appendToStoredArray(brandToPurses, brand, assetInfo);
|
|
637
|
+
// NOTE: when we decentralize introduction of issuers,
|
|
638
|
+
// process queued payments for this brand.
|
|
639
|
+
|
|
640
|
+
void helper.watchPurse(purse);
|
|
641
|
+
return purse;
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* see https://github.com/Agoric/agoric-sdk/issues/8445 and
|
|
646
|
+
* https://github.com/Agoric/agoric-sdk/issues/8286. As originally
|
|
647
|
+
* released, the smartWallet didn't durably monitor the promises for the
|
|
648
|
+
* outcomes of offers, and would have dropped them on upgrade of Zoe or
|
|
649
|
+
* the smartWallet itself. Using watchedPromises, (see offerWatcher.js)
|
|
650
|
+
* we've addressed the problem for new offers. This function will
|
|
651
|
+
* backfill the solution for offers that were outstanding before the
|
|
652
|
+
* transition to incarnation 2 of the smartWallet.
|
|
653
|
+
*/
|
|
654
|
+
async repairUnwatchedSeats() {
|
|
655
|
+
const { state, facets } = this;
|
|
656
|
+
const { address, invitationPurse, liveOffers, liveOfferSeats } =
|
|
657
|
+
state;
|
|
658
|
+
const { zoe, agoricNames, invitationBrand, invitationIssuer } =
|
|
659
|
+
shared;
|
|
660
|
+
|
|
661
|
+
const invitationFromSpec = makeInvitationsHelper(
|
|
662
|
+
zoe,
|
|
663
|
+
agoricNames,
|
|
664
|
+
invitationBrand,
|
|
665
|
+
invitationPurse,
|
|
666
|
+
state.offerToInvitationMakers.get,
|
|
362
667
|
);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
668
|
+
|
|
669
|
+
const watcherPromises = [];
|
|
670
|
+
for (const seatId of liveOfferSeats.keys()) {
|
|
671
|
+
facets.helper.logWalletInfo(`repairing ${seatId}`);
|
|
672
|
+
const offerSpec = liveOffers.get(seatId);
|
|
673
|
+
const seat = liveOfferSeats.get(seatId);
|
|
674
|
+
|
|
675
|
+
const watchOutcome = (async () => {
|
|
676
|
+
await null;
|
|
677
|
+
let invitationAmount = state.offerToUsedInvitation.has(
|
|
678
|
+
// @ts-expect-error older type allowed number
|
|
679
|
+
offerSpec.id,
|
|
680
|
+
)
|
|
681
|
+
? state.offerToUsedInvitation.get(
|
|
682
|
+
// @ts-expect-error older type allowed number
|
|
683
|
+
offerSpec.id,
|
|
684
|
+
)
|
|
685
|
+
: undefined;
|
|
686
|
+
if (invitationAmount) {
|
|
687
|
+
facets.helper.logWalletInfo(
|
|
688
|
+
'recovered invitation amount for offer',
|
|
689
|
+
offerSpec.id,
|
|
690
|
+
);
|
|
691
|
+
} else {
|
|
692
|
+
facets.helper.logWalletInfo(
|
|
693
|
+
'inferring invitation amount for offer',
|
|
694
|
+
offerSpec.id,
|
|
695
|
+
);
|
|
696
|
+
const tempInvitation = invitationFromSpec(
|
|
697
|
+
offerSpec.invitationSpec,
|
|
698
|
+
);
|
|
699
|
+
invitationAmount =
|
|
700
|
+
await E(invitationIssuer).getAmountOf(tempInvitation);
|
|
701
|
+
void E(invitationIssuer).burn(tempInvitation);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const watcher = makeOfferWatcher(
|
|
705
|
+
facets.helper,
|
|
706
|
+
facets.deposit,
|
|
707
|
+
offerSpec,
|
|
708
|
+
address,
|
|
709
|
+
invitationAmount,
|
|
710
|
+
seat,
|
|
711
|
+
);
|
|
712
|
+
return watchOfferOutcomes(watcher, seat);
|
|
713
|
+
})();
|
|
714
|
+
trace(`Repaired seat ${seatId} for wallet ${address}`);
|
|
715
|
+
watcherPromises.push(watchOutcome);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
await Promise.all(watcherPromises);
|
|
719
|
+
},
|
|
720
|
+
async repairUnwatchedPurses() {
|
|
721
|
+
const { state, facets } = this;
|
|
722
|
+
const { helper, self } = facets;
|
|
723
|
+
const { invitationPurse, address } = state;
|
|
724
|
+
|
|
725
|
+
const brandToPurses = getBrandToPurses(walletPurses, self);
|
|
726
|
+
trace(`Found ${brandToPurses.values()} purse(s) for ${address}`);
|
|
727
|
+
for (const purses of brandToPurses.values()) {
|
|
728
|
+
for (const record of purses) {
|
|
729
|
+
void helper.watchPurse(record.purse);
|
|
730
|
+
trace(`Repaired purse ${record.petname} of ${address}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
void helper.watchPurse(invitationPurse);
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
/** @param {OfferStatus} offerStatus */
|
|
738
|
+
updateStatus(offerStatus) {
|
|
739
|
+
const { state, facets } = this;
|
|
740
|
+
facets.helper.logWalletInfo('offerStatus', offerStatus);
|
|
741
|
+
|
|
742
|
+
void state.updateRecorderKit.recorder.write({
|
|
743
|
+
updated: 'offerStatus',
|
|
744
|
+
status: offerStatus,
|
|
370
745
|
});
|
|
746
|
+
|
|
747
|
+
if ('numWantsSatisfied' in offerStatus) {
|
|
748
|
+
if (state.liveOfferSeats.has(offerStatus.id)) {
|
|
749
|
+
state.liveOfferSeats.delete(offerStatus.id);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (facets.helper.getLiveOfferPayments().has(offerStatus.id)) {
|
|
753
|
+
facets.helper.getLiveOfferPayments().delete(offerStatus.id);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (state.liveOffers.has(offerStatus.id)) {
|
|
757
|
+
state.liveOffers.delete(offerStatus.id);
|
|
758
|
+
// This might get skipped in subsequent passes, since we .delete()
|
|
759
|
+
// the first time through
|
|
760
|
+
facets.helper.publishCurrentState();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* @param {string} offerId
|
|
767
|
+
* @param {Amount<'set'>} invitationAmount
|
|
768
|
+
* @param {import('./types.js').InvitationMakers} invitationMakers
|
|
769
|
+
* @param {import('./types.js').PublicSubscribers} publicSubscribers
|
|
770
|
+
*/
|
|
771
|
+
async addContinuingOffer(
|
|
772
|
+
offerId,
|
|
773
|
+
invitationAmount,
|
|
774
|
+
invitationMakers,
|
|
775
|
+
publicSubscribers,
|
|
776
|
+
) {
|
|
777
|
+
const { state, facets } = this;
|
|
778
|
+
|
|
779
|
+
state.offerToUsedInvitation.init(offerId, invitationAmount);
|
|
780
|
+
state.offerToInvitationMakers.init(offerId, invitationMakers);
|
|
781
|
+
const pathMap = await objectMapStoragePath(publicSubscribers);
|
|
782
|
+
if (pathMap) {
|
|
783
|
+
facets.helper.logWalletInfo('recording pathMap', pathMap);
|
|
784
|
+
state.offerToPublicSubscriberPaths.init(offerId, pathMap);
|
|
785
|
+
}
|
|
786
|
+
facets.helper.publishCurrentState();
|
|
787
|
+
},
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* @param {Brand} brand
|
|
791
|
+
* @returns {Promise<Purse>}
|
|
792
|
+
*/
|
|
793
|
+
async purseForBrand(brand) {
|
|
794
|
+
const { state, facets } = this;
|
|
795
|
+
const { registry, invitationBrand } = shared;
|
|
796
|
+
|
|
797
|
+
if (registry.has(brand)) {
|
|
798
|
+
return E(state.bank).getPurse(brand);
|
|
799
|
+
} else if (invitationBrand === brand) {
|
|
800
|
+
return state.invitationPurse;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const purse = await facets.helper.getPurseIfKnownBrand(
|
|
804
|
+
brand,
|
|
805
|
+
shared.agoricNames,
|
|
806
|
+
);
|
|
807
|
+
if (purse) {
|
|
808
|
+
return purse;
|
|
809
|
+
}
|
|
810
|
+
throw Fail`cannot find/make purse for ${brand}`;
|
|
811
|
+
},
|
|
812
|
+
logWalletInfo(...args) {
|
|
813
|
+
const { state } = this;
|
|
814
|
+
console.info('wallet', state.address, ...args);
|
|
815
|
+
},
|
|
816
|
+
logWalletError(...args) {
|
|
817
|
+
const { state } = this;
|
|
818
|
+
console.error('wallet', state.address, ...args);
|
|
819
|
+
},
|
|
820
|
+
// In new SmartWallets, this is part of state, but we can't add fields
|
|
821
|
+
// to instance state for older SmartWallets, so put it in baggage.
|
|
822
|
+
getLiveOfferPayments() {
|
|
823
|
+
const { state } = this;
|
|
824
|
+
|
|
825
|
+
if (state.liveOfferPayments) {
|
|
826
|
+
return state.liveOfferPayments;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// This will only happen for legacy wallets, before WF incarnation 2
|
|
830
|
+
if (!baggage.has(state.address)) {
|
|
831
|
+
trace(`getLiveOfferPayments adding store for ${state.address}`);
|
|
832
|
+
baggage.init(
|
|
833
|
+
state.address,
|
|
834
|
+
makeScalarBigMapStore('live offer payments', {
|
|
835
|
+
durable: true,
|
|
836
|
+
}),
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return baggage.get(state.address);
|
|
371
840
|
},
|
|
372
841
|
},
|
|
373
842
|
/**
|
|
374
|
-
* Similar to {DepositFacet} but async because it has to look up the
|
|
843
|
+
* Similar to {DepositFacet} but async because it has to look up the
|
|
844
|
+
* purse.
|
|
375
845
|
*/
|
|
376
846
|
deposit: {
|
|
377
847
|
/**
|
|
@@ -379,159 +849,288 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
379
849
|
*
|
|
380
850
|
* If the purse doesn't exist, we hold the payment in durable storage.
|
|
381
851
|
*
|
|
382
|
-
* @param {
|
|
383
|
-
* @returns {Promise<Amount>}
|
|
852
|
+
* @param {Payment} payment
|
|
853
|
+
* @returns {Promise<Amount>}
|
|
854
|
+
* @throws if there's not yet a purse, though the payment is held to try
|
|
855
|
+
* again when there is
|
|
384
856
|
*/
|
|
385
857
|
async receive(payment) {
|
|
386
|
-
const {
|
|
858
|
+
const {
|
|
859
|
+
state,
|
|
860
|
+
facets: { helper },
|
|
861
|
+
} = this;
|
|
862
|
+
const { paymentQueues: queues, bank, invitationPurse } = state;
|
|
387
863
|
const { registry, invitationBrand } = shared;
|
|
864
|
+
|
|
388
865
|
const brand = await E(payment).getAllegedBrand();
|
|
389
866
|
|
|
390
867
|
// When there is a purse deposit into it
|
|
391
868
|
if (registry.has(brand)) {
|
|
392
869
|
const purse = E(bank).getPurse(brand);
|
|
393
|
-
// @ts-expect-error
|
|
870
|
+
// @ts-expect-error narrow assetKind to 'nat'
|
|
394
871
|
return E(purse).deposit(payment);
|
|
395
872
|
} else if (invitationBrand === brand) {
|
|
396
|
-
// @ts-expect-error
|
|
873
|
+
// @ts-expect-error narrow assetKind to 'set'
|
|
397
874
|
return E(invitationPurse).deposit(payment);
|
|
398
875
|
}
|
|
399
876
|
|
|
877
|
+
const purse = await helper.getPurseIfKnownBrand(
|
|
878
|
+
brand,
|
|
879
|
+
shared.agoricNames,
|
|
880
|
+
);
|
|
881
|
+
if (purse) {
|
|
882
|
+
return E(purse).deposit(payment);
|
|
883
|
+
}
|
|
884
|
+
|
|
400
885
|
// When there is no purse, save the payment into a queue.
|
|
401
886
|
// It's not yet ever read but a future version of the contract can
|
|
402
887
|
appendToStoredArray(queues, brand, payment);
|
|
403
|
-
|
|
888
|
+
throw Fail`cannot deposit payment with brand ${brand}: no purse`;
|
|
404
889
|
},
|
|
405
890
|
},
|
|
891
|
+
|
|
892
|
+
payments: {
|
|
893
|
+
/**
|
|
894
|
+
* Withdraw the offered amount from the appropriate purse of this
|
|
895
|
+
* wallet.
|
|
896
|
+
*
|
|
897
|
+
* Save its amount in liveOfferPayments in case we need to reclaim the
|
|
898
|
+
* payment.
|
|
899
|
+
*
|
|
900
|
+
* @param {AmountKeywordRecord} give
|
|
901
|
+
* @param {OfferId} offerId
|
|
902
|
+
* @returns {PaymentPKeywordRecord}
|
|
903
|
+
*/
|
|
904
|
+
withdrawGive(give, offerId) {
|
|
905
|
+
const { facets } = this;
|
|
906
|
+
|
|
907
|
+
/** @type {MapStore<Brand, Payment>} */
|
|
908
|
+
const brandPaymentRecord = makeScalarBigMapStore('paymentToBrand', {
|
|
909
|
+
durable: true,
|
|
910
|
+
});
|
|
911
|
+
facets.helper
|
|
912
|
+
.getLiveOfferPayments()
|
|
913
|
+
.init(offerId, brandPaymentRecord);
|
|
914
|
+
|
|
915
|
+
// Add each payment amount to brandPaymentRecord as it is withdrawn. If
|
|
916
|
+
// there's an error later, we can use it to redeposit the correct amount.
|
|
917
|
+
return objectMap(give, amount => {
|
|
918
|
+
/** @type {Promise<Purse>} */
|
|
919
|
+
const purseP = facets.helper.purseForBrand(amount.brand);
|
|
920
|
+
const paymentP = E(purseP).withdraw(amount);
|
|
921
|
+
void E.when(
|
|
922
|
+
paymentP,
|
|
923
|
+
payment => brandPaymentRecord.init(amount.brand, payment),
|
|
924
|
+
e => {
|
|
925
|
+
// recovery will be handled by tryReclaimingWithdrawnPayments()
|
|
926
|
+
facets.helper.logWalletInfo(
|
|
927
|
+
`⚠️ Payment withdrawal failed.`,
|
|
928
|
+
offerId,
|
|
929
|
+
e,
|
|
930
|
+
);
|
|
931
|
+
},
|
|
932
|
+
);
|
|
933
|
+
return paymentP;
|
|
934
|
+
});
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Find the live payments for the offer and deposit them back in the
|
|
939
|
+
* appropriate purses.
|
|
940
|
+
*
|
|
941
|
+
* @param {OfferId} offerId
|
|
942
|
+
* @returns {Promise<Amount[]>}
|
|
943
|
+
*/
|
|
944
|
+
async tryReclaimingWithdrawnPayments(offerId) {
|
|
945
|
+
const { facets } = this;
|
|
946
|
+
|
|
947
|
+
await null;
|
|
948
|
+
|
|
949
|
+
const liveOfferPayments = facets.helper.getLiveOfferPayments();
|
|
950
|
+
if (liveOfferPayments.has(offerId)) {
|
|
951
|
+
const brandPaymentRecord = liveOfferPayments.get(offerId);
|
|
952
|
+
if (!brandPaymentRecord) {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
const out = [];
|
|
956
|
+
// Use allSettled to ensure we attempt all the deposits, regardless of
|
|
957
|
+
// individual rejections.
|
|
958
|
+
await Promise.allSettled(
|
|
959
|
+
Array.from(brandPaymentRecord.entries()).map(([b, p]) => {
|
|
960
|
+
// Wait for the withdrawal to complete. This protects against a
|
|
961
|
+
// race when updating paymentToPurse.
|
|
962
|
+
const purseP = facets.helper.purseForBrand(b);
|
|
963
|
+
|
|
964
|
+
// Now send it back to the purse.
|
|
965
|
+
return E(purseP)
|
|
966
|
+
.deposit(p)
|
|
967
|
+
.then(amt => {
|
|
968
|
+
out.push(amt);
|
|
969
|
+
});
|
|
970
|
+
}),
|
|
971
|
+
);
|
|
972
|
+
return harden(out);
|
|
973
|
+
}
|
|
974
|
+
return [];
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
|
|
406
978
|
offers: {
|
|
407
979
|
/**
|
|
408
|
-
* Take an offer description provided in capData, augment it with
|
|
980
|
+
* Take an offer description provided in capData, augment it with
|
|
981
|
+
* payments and call zoe.offer()
|
|
409
982
|
*
|
|
410
|
-
* @param {
|
|
411
|
-
* @returns {Promise<void>} after the offer has been both seated and
|
|
412
|
-
*
|
|
983
|
+
* @param {OfferSpec} offerSpec
|
|
984
|
+
* @returns {Promise<void>} after the offer has been both seated and
|
|
985
|
+
* exited by Zoe.
|
|
986
|
+
* @throws if any parts of the offer can be determined synchronously to
|
|
987
|
+
* be invalid
|
|
413
988
|
*/
|
|
414
989
|
async executeOffer(offerSpec) {
|
|
415
990
|
const { facets, state } = this;
|
|
416
|
-
const {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
invitationPurse,
|
|
420
|
-
offerToInvitationMakers,
|
|
421
|
-
offerToUsedInvitation,
|
|
422
|
-
offerToPublicSubscriberPaths,
|
|
423
|
-
updateRecorderKit,
|
|
424
|
-
} = this.state;
|
|
425
|
-
const { invitationBrand, zoe, invitationIssuer, registry } = shared;
|
|
991
|
+
const { address, invitationPurse } = state;
|
|
992
|
+
const { zoe, agoricNames } = shared;
|
|
993
|
+
const { invitationBrand, invitationIssuer } = shared;
|
|
426
994
|
|
|
427
995
|
facets.helper.assertUniqueOfferId(String(offerSpec.id));
|
|
428
996
|
|
|
429
|
-
|
|
430
|
-
info: (...args) => console.info('wallet', address, ...args),
|
|
431
|
-
error: (...args) => console.error('wallet', address, ...args),
|
|
432
|
-
};
|
|
997
|
+
await null;
|
|
433
998
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
),
|
|
446
|
-
/**
|
|
447
|
-
* @param {Brand} brand
|
|
448
|
-
* @returns {Promise<RemotePurse>}
|
|
449
|
-
*/
|
|
450
|
-
purseForBrand: async brand => {
|
|
451
|
-
if (registry.has(brand)) {
|
|
452
|
-
// @ts-expect-error RemotePurse cast
|
|
453
|
-
return E(bank).getPurse(brand);
|
|
454
|
-
} else if (invitationBrand === brand) {
|
|
455
|
-
// @ts-expect-error RemotePurse cast
|
|
456
|
-
return invitationPurse;
|
|
457
|
-
}
|
|
458
|
-
throw Fail`cannot find/make purse for ${brand}`;
|
|
459
|
-
},
|
|
460
|
-
logger,
|
|
461
|
-
},
|
|
462
|
-
onStatusChange: offerStatus => {
|
|
463
|
-
logger.info('offerStatus', offerStatus);
|
|
999
|
+
/** @type {UserSeat} */
|
|
1000
|
+
let seatRef;
|
|
1001
|
+
let watcher;
|
|
1002
|
+
try {
|
|
1003
|
+
const invitationFromSpec = makeInvitationsHelper(
|
|
1004
|
+
zoe,
|
|
1005
|
+
agoricNames,
|
|
1006
|
+
invitationBrand,
|
|
1007
|
+
invitationPurse,
|
|
1008
|
+
state.offerToInvitationMakers.get,
|
|
1009
|
+
);
|
|
464
1010
|
|
|
465
|
-
|
|
466
|
-
updated: 'offerStatus',
|
|
467
|
-
status: offerStatus,
|
|
468
|
-
});
|
|
1011
|
+
facets.helper.logWalletInfo('starting executeOffer', offerSpec.id);
|
|
469
1012
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (state.liveOfferSeats.has(offerStatus.id)) {
|
|
473
|
-
state.liveOfferSeats.delete(offerStatus.id);
|
|
474
|
-
}
|
|
1013
|
+
// 1. Prepare values and validate synchronously.
|
|
1014
|
+
const { proposal } = offerSpec;
|
|
475
1015
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1016
|
+
const invitation = invitationFromSpec(offerSpec.invitationSpec);
|
|
1017
|
+
|
|
1018
|
+
const invitationAmount =
|
|
1019
|
+
await E(invitationIssuer).getAmountOf(invitation);
|
|
1020
|
+
|
|
1021
|
+
// 2. Begin executing offer
|
|
1022
|
+
// No explicit signal to user that we reached here but if anything above
|
|
1023
|
+
// failed they'd get an 'error' status update.
|
|
1024
|
+
|
|
1025
|
+
const withdrawnPayments =
|
|
1026
|
+
proposal?.give &&
|
|
1027
|
+
(await deeplyFulfilledObject(
|
|
1028
|
+
facets.payments.withdrawGive(proposal.give, offerSpec.id),
|
|
1029
|
+
));
|
|
1030
|
+
|
|
1031
|
+
seatRef = await E(zoe).offer(
|
|
1032
|
+
invitation,
|
|
1033
|
+
proposal,
|
|
1034
|
+
withdrawnPayments,
|
|
1035
|
+
offerSpec.offerArgs,
|
|
1036
|
+
);
|
|
1037
|
+
facets.helper.logWalletInfo(offerSpec.id, 'seated');
|
|
1038
|
+
|
|
1039
|
+
watcher = makeOfferWatcher(
|
|
1040
|
+
facets.helper,
|
|
1041
|
+
facets.deposit,
|
|
1042
|
+
offerSpec,
|
|
1043
|
+
address,
|
|
485
1044
|
invitationAmount,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
) => {
|
|
489
|
-
offerToUsedInvitation.init(offerId, invitationAmount);
|
|
490
|
-
offerToInvitationMakers.init(offerId, invitationMakers);
|
|
491
|
-
const pathMap = await objectMapStoragePath(publicSubscribers);
|
|
492
|
-
if (pathMap) {
|
|
493
|
-
logger.info('recording pathMap', pathMap);
|
|
494
|
-
offerToPublicSubscriberPaths.init(offerId, pathMap);
|
|
495
|
-
}
|
|
496
|
-
facets.helper.publishCurrentState();
|
|
497
|
-
},
|
|
498
|
-
});
|
|
1045
|
+
seatRef,
|
|
1046
|
+
);
|
|
499
1047
|
|
|
500
|
-
return executor.executeOffer(offerSpec, seatRef => {
|
|
501
1048
|
state.liveOffers.init(offerSpec.id, offerSpec);
|
|
502
|
-
facets.helper.publishCurrentState();
|
|
503
1049
|
state.liveOfferSeats.init(offerSpec.id, seatRef);
|
|
504
|
-
|
|
1050
|
+
|
|
1051
|
+
// publish the live offers
|
|
1052
|
+
facets.helper.publishCurrentState();
|
|
1053
|
+
|
|
1054
|
+
// await so that any errors are caught and handled below
|
|
1055
|
+
await watchOfferOutcomes(watcher, seatRef);
|
|
1056
|
+
} catch (reason) {
|
|
1057
|
+
// This block only runs if the block above fails during one vat incarnation.
|
|
1058
|
+
facets.helper.logWalletError('IMMEDIATE OFFER ERROR:', reason);
|
|
1059
|
+
|
|
1060
|
+
// Update status to observers
|
|
1061
|
+
if (isUpgradeDisconnection(reason)) {
|
|
1062
|
+
// The offer watchers will reconnect. Don't reclaim or exit
|
|
1063
|
+
return;
|
|
1064
|
+
} else if (watcher) {
|
|
1065
|
+
// The watcher's onRejected will updateStatus()
|
|
1066
|
+
} else {
|
|
1067
|
+
facets.helper.updateStatus({
|
|
1068
|
+
error: reason.toString(),
|
|
1069
|
+
...offerSpec,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Backstop recovery, in case something very basic fails.
|
|
1074
|
+
if (offerSpec?.proposal?.give) {
|
|
1075
|
+
facets.payments
|
|
1076
|
+
.tryReclaimingWithdrawnPayments(offerSpec.id)
|
|
1077
|
+
.catch(e =>
|
|
1078
|
+
facets.helper.logWalletError(
|
|
1079
|
+
'recovery failed reclaiming payments',
|
|
1080
|
+
e,
|
|
1081
|
+
),
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// XXX tests rely on throwing immediate errors, not covering the
|
|
1086
|
+
// error handling in the event the failure is after an upgrade
|
|
1087
|
+
throw reason;
|
|
1088
|
+
}
|
|
505
1089
|
},
|
|
506
1090
|
/**
|
|
507
1091
|
* Take an offer's id, look up its seat, try to exit.
|
|
508
1092
|
*
|
|
509
|
-
* @param {
|
|
1093
|
+
* @param {OfferId} offerId
|
|
510
1094
|
* @returns {Promise<void>}
|
|
511
1095
|
* @throws if the seat can't be found or E(seatRef).tryExit() fails.
|
|
512
1096
|
*/
|
|
513
1097
|
async tryExitOffer(offerId) {
|
|
1098
|
+
const { facets } = this;
|
|
1099
|
+
const amts = await facets.payments
|
|
1100
|
+
.tryReclaimingWithdrawnPayments(offerId)
|
|
1101
|
+
.catch(e => {
|
|
1102
|
+
facets.helper.logWalletError(
|
|
1103
|
+
'recovery failed reclaiming payments',
|
|
1104
|
+
e,
|
|
1105
|
+
);
|
|
1106
|
+
return [];
|
|
1107
|
+
});
|
|
1108
|
+
if (amts.length > 0) {
|
|
1109
|
+
facets.helper.logWalletInfo('reclaimed', amts, 'from', offerId);
|
|
1110
|
+
}
|
|
514
1111
|
const seatRef = this.state.liveOfferSeats.get(offerId);
|
|
515
1112
|
await E(seatRef).tryExit();
|
|
516
1113
|
},
|
|
517
1114
|
},
|
|
518
1115
|
self: {
|
|
519
1116
|
/**
|
|
520
|
-
* Umarshals the actionCapData and delegates to the appropriate action
|
|
1117
|
+
* Umarshals the actionCapData and delegates to the appropriate action
|
|
1118
|
+
* handler.
|
|
521
1119
|
*
|
|
522
|
-
* @param {import('@endo/marshal').CapData<string>} actionCapData
|
|
523
|
-
*
|
|
1120
|
+
* @param {import('@endo/marshal').CapData<string | null>} actionCapData
|
|
1121
|
+
* of type BridgeAction
|
|
1122
|
+
* @param {boolean} [canSpend]
|
|
524
1123
|
* @returns {Promise<void>}
|
|
525
1124
|
*/
|
|
526
1125
|
handleBridgeAction(actionCapData, canSpend = false) {
|
|
1126
|
+
const { facets } = this;
|
|
1127
|
+
const { offers } = facets;
|
|
527
1128
|
const { publicMarshaller } = shared;
|
|
528
1129
|
|
|
529
|
-
const { offers } = this.facets;
|
|
530
|
-
|
|
531
1130
|
/** @param {Error} err */
|
|
532
1131
|
const recordError = err => {
|
|
533
|
-
const {
|
|
534
|
-
|
|
1132
|
+
const { updateRecorderKit } = this.state;
|
|
1133
|
+
facets.helper.logWalletError('handleBridgeAction error:', err);
|
|
535
1134
|
void updateRecorderKit.recorder.write({
|
|
536
1135
|
updated: 'walletAction',
|
|
537
1136
|
status: { error: err.message },
|
|
@@ -576,14 +1175,18 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
576
1175
|
},
|
|
577
1176
|
/** @deprecated use getPublicTopics */
|
|
578
1177
|
getCurrentSubscriber() {
|
|
579
|
-
|
|
1178
|
+
const { state } = this;
|
|
1179
|
+
return state.currentRecorderKit.subscriber;
|
|
580
1180
|
},
|
|
581
1181
|
/** @deprecated use getPublicTopics */
|
|
582
1182
|
getUpdatesSubscriber() {
|
|
583
|
-
|
|
1183
|
+
const { state } = this;
|
|
1184
|
+
return state.updateRecorderKit.subscriber;
|
|
584
1185
|
},
|
|
585
1186
|
getPublicTopics() {
|
|
586
|
-
const {
|
|
1187
|
+
const { state } = this;
|
|
1188
|
+
const { currentRecorderKit, updateRecorderKit } = state;
|
|
1189
|
+
|
|
587
1190
|
return harden({
|
|
588
1191
|
current: {
|
|
589
1192
|
description: 'Current state of wallet',
|
|
@@ -597,6 +1200,27 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
597
1200
|
},
|
|
598
1201
|
});
|
|
599
1202
|
},
|
|
1203
|
+
// TODO remove this and repairUnwatchedSeats once the repair has taken place.
|
|
1204
|
+
/**
|
|
1205
|
+
* To be called once ever per wallet.
|
|
1206
|
+
*
|
|
1207
|
+
* @param {object} key
|
|
1208
|
+
*/
|
|
1209
|
+
repairWalletForIncarnation2(key) {
|
|
1210
|
+
const { state, facets } = this;
|
|
1211
|
+
|
|
1212
|
+
if (key !== shared.secretWalletFactoryKey) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
facets.helper.repairUnwatchedSeats().catch(e => {
|
|
1217
|
+
console.error('repairUnwatchedSeats rejection', e);
|
|
1218
|
+
});
|
|
1219
|
+
facets.helper.repairUnwatchedPurses().catch(e => {
|
|
1220
|
+
console.error('repairUnwatchedPurses rejection', e);
|
|
1221
|
+
});
|
|
1222
|
+
trace(`repaired wallet ${state.address}`);
|
|
1223
|
+
},
|
|
600
1224
|
},
|
|
601
1225
|
},
|
|
602
1226
|
{
|
|
@@ -604,14 +1228,18 @@ export const prepareSmartWallet = (baggage, shared) => {
|
|
|
604
1228
|
const { invitationPurse } = state;
|
|
605
1229
|
const { helper } = facets;
|
|
606
1230
|
|
|
607
|
-
// @ts-expect-error RemotePurse cast
|
|
608
1231
|
void helper.watchPurse(invitationPurse);
|
|
609
1232
|
},
|
|
610
1233
|
},
|
|
611
1234
|
);
|
|
612
1235
|
|
|
613
1236
|
/**
|
|
614
|
-
* @param {Omit<
|
|
1237
|
+
* @param {Omit<
|
|
1238
|
+
* UniqueParams,
|
|
1239
|
+
* 'currentStorageNode' | 'walletStorageNode'
|
|
1240
|
+
* > & {
|
|
1241
|
+
* walletStorageNode: ERef<StorageNode>;
|
|
1242
|
+
* }} uniqueWithoutChildNodes
|
|
615
1243
|
*/
|
|
616
1244
|
const makeSmartWallet = async uniqueWithoutChildNodes => {
|
|
617
1245
|
const [walletStorageNode, currentStorageNode] = await Promise.all([
|