-
Notifications
You must be signed in to change notification settings - Fork 214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(smart-wallet): exit offer #7028
Conversation
2635aaa
to
17312dd
Compare
17312dd
to
94cb18b
Compare
94cb18b
to
94443a5
Compare
updated: 'balance', | ||
}); | ||
|
||
const statusUpdateHasKeys = (updateIndex, result, numWants, payouts) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@turadg I reverted these asserts because they only worked when the test was run in isolation, and we ended up changing current
not updates
@@ -124,6 +124,7 @@ const mapToRecord = map => Object.fromEntries(map.entries()); | |||
* purseBalances: MapStore<RemotePurse, Amount>, | |||
* updatePublishKit: PublishKit<UpdateRecord>, | |||
* currentPublishKit: PublishKit<CurrentWalletRecord>, | |||
* possiblyExitableOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@turadg I renamed this because:
- It makes it easier to understand why we're storing them.
- I don't think it's the exact same as currently seated offers. If we have an error, the offer could still be seated, but at that point the smart wallet should stop storing it and try to unseat it automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After my latest changes, point 2 is irrelevant though. We need to still store it in case the exit fails due to a deadline or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget what the earlier term was but this one feels klunky. let's chat higher bandwidth
packages/smart-wallet/src/offers.js
Outdated
@@ -111,7 +116,6 @@ export const makeOfferExecutor = ({ | |||
offerArgs, | |||
); | |||
logger.info(id, 'seated'); | |||
updateStatus({}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I included "exit offer" in this PR because it influenced the design here. We return the seat if this succeeds rather than updating the status with {}
.
I can take this commit out of this PR though if needed.
@@ -124,6 +135,8 @@ const mapToRecord = map => Object.fromEntries(map.entries()); | |||
* purseBalances: MapStore<RemotePurse, Amount>, | |||
* updatePublishKit: PublishKit<UpdateRecord>, | |||
* currentPublishKit: PublishKit<CurrentWalletRecord>, | |||
* possiblyExitableOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>, | |||
* activeOfferSeats: MapStore<import('./offers.js').OfferId, UserSeat<unknown>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose we could store the seats in possiblyExitableOffers
and filter them out when we publish current
. I'm not sure how much of a performance difference it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Compute vs storage, I don't know how those costs compare on the chain. My hunch is that we shouldn't be using durable storage as a cache of computed results but let's get more input.
784d15a
to
784ea0a
Compare
@@ -476,9 +476,8 @@ export const prepareSmartWallet = (baggage, shared) => { | |||
}); | |||
|
|||
const isOfferPossiblyExitable = !( | |||
'error' in offerStatus || 'numWantsSatisfied' in offerStatus |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is an error, we don't necessarily want to forget about it just yet. The offer executor should automatically try to exit the offer in that case, and then numWantsSatisfied
should come back as 0. However, the exit could fail if the rule is "after deadline" or something, so in that case keep it around so the user can manually try to exit it later.
Tested with Agoric/wallet-app#62 and vaults on a local chain and was able to view/exit currently seated offers in the wallet UI |
packages/smart-wallet/src/offers.js
Outdated
*/ | ||
const handleError = err => { | ||
const handleError = (err, seatRef) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this optional seatRef is a little loose. Instead leave this handleError
alone and make a new function like exitSeatAndHandleError
in the tryBody
scope. It won't need to take seatRef
because it's in the closure. Then the error handler lines can continue to be a prebound function instead of a new one for each when
.
Also better to exit the offer before reclaiming payments. I don't know if it affects anything in practice but it's conceptually more clear.
packages/smart-wallet/src/offers.js
Outdated
E(seatRef) | ||
.tryExit() | ||
.catch(e => { | ||
logger.error('EXIT OFFER ERROR:', e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this isn't an error if the seat was already exited. generally the contracts should do that. This is a backstop.
let's only tryExit
if it hasn't already exited (chain with the hasExited()
promise). Then an exception would be an error. A pretty big one though so I think we'd want to throw instead of merely catch and log.
* }} TryExitOfferAction | ||
*/ | ||
|
||
// One method yet but structured to support more. For example, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are now two methods. the part about additional potential ones is worth keeping.
* @typedef {{ | ||
* method: 'tryExitOffer' | ||
* offerId: import('./offers.js').OfferId, | ||
* }} TryExitOfferAction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -124,6 +124,7 @@ const mapToRecord = map => Object.fromEntries(map.entries()); | |||
* purseBalances: MapStore<RemotePurse, Amount>, | |||
* updatePublishKit: PublishKit<UpdateRecord>, | |||
* currentPublishKit: PublishKit<CurrentWalletRecord>, | |||
* possiblyExitableOffers: MapStore<import('./offers.js').OfferId, import('./offers.js').OfferStatus>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget what the earlier term was but this one feels klunky. let's chat higher bandwidth
updatePublishKit.publisher.publish({ | ||
updated: 'offerStatus', | ||
status: offerStatus, | ||
}); | ||
|
||
const isOfferPossiblyExitable = !( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's got to be a better name for this state. "unresolved"? @Chris-Hibbert or @dckc may have ideas
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked in our UserSeat docs, and I'm still not clear on all the possible states and transitions, let alone good names for them.
"pending" is the first word that came to mind, but I don't know whether it's accurate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd offer 'live' or 'open'. The alternative is exited
.
state.activeOfferSeats.has(offerStatus.id) && | ||
state.activeOfferSeats.delete(offerStatus.id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this isn't frontend :-P
state.activeOfferSeats.has(offerStatus.id) && | |
state.activeOfferSeats.delete(offerStatus.id); | |
if (state.activeOfferSeats.has(offerStatus.id)) { | |
state.activeOfferSeats.delete(offerStatus.id); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use https://eslint.org/docs/latest/rules/no-unused-expressions then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Its defaults ban more than we'd want but the settings look like it could be narrowed appropriately.
Do you want to PR that or should I?
Also, thanks for looking for a way to automate the review comment. Always better to let the tools do it.
@@ -160,4 +160,5 @@ test.todo( | |||
// pause the PSM trading such that there is time to exit before offer resolves | |||
// executeOffer to buy the junk (which can't resolve) | |||
// exit the offer "oh I don't want to buy junk!" | |||
// Help? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's pair on Monday
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prune this? (I got distracted)
* @throws if the seat can't be found or E(seatRef).tryExit() fails. | ||
*/ | ||
async tryExitOffer(offerId) { | ||
const seatRef = this.state.activeOfferSeats.get(offerId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused why we have possiblyExitableOffers
and don't use it here
@@ -468,7 +507,23 @@ export const prepareSmartWallet = (baggage, shared) => { | |||
facets.helper.publishCurrentState(); | |||
}, | |||
}); | |||
await executor.executeOffer(offerSpec); | |||
const seatRef = await executor.executeOffer(offerSpec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the return is null only if an error happens. WDYT of a try/catch instead?
} = this.state; | ||
currentPublishKit.publisher.publish({ | ||
purses: [...purseBalances.values()].map(a => ({ | ||
brand: a.brand, | ||
balance: a, | ||
})), | ||
possiblyExitableOffers: mapToRecord(possiblyExitableOffers), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not use mapToRecord
any more; we're trying to get rid of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh... what to use instead...
possiblyExitableOffers: mapToRecord(possiblyExitableOffers), | |
possiblyExitableOffers: possiblyExitableOffers.entries(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops... I suppose .entries()
returns an interator. We should materialize the list:
possiblyExitableOffers: mapToRecord(possiblyExitableOffers), | |
possiblyExitableOffers: [...possiblyExitableOffers.entries()], |
Does the smartWallet already include an exit clause by default (or choice) when making an offer? Some contracts will refuse exit clauses they don't like, so I think this would have to be chosen by the user or configured by the contract. Without an appropriate exit clause ( |
3a2c331
to
c53710c
Compare
Datadog ReportBranch report: ✅ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some comments/questions as I drove by. I didn't see anything objectionable.
Please don't count me as a primary reviewer.
@@ -48,7 +48,7 @@ const DEFAULT_DECIMALS = 9; | |||
* added in the appropriate place and settled when the price reaches that level. | |||
*/ | |||
|
|||
const trace = makeTracer('AucBook', false); | |||
const trace = makeTracer('AucBook'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you intend for this change to be merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does look odd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's been useful for debugging. I expect we'll audit all tracers before cutting the release
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect we'll audit all tracers before cutting the release
Well, here's hoping.
#5222 doesn't seem to be scheduled, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the issue. I put it in the release. I expect it'll get triaged.
E(seatRef) | ||
.hasExited() | ||
.then(hasExited => { | ||
if (!hasExited) { | ||
E(seatRef).tryExit(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MarkM seems to prefer E.when()
to .then()
.
E(seatRef) | |
.hasExited() | |
.then(hasExited => { | |
if (!hasExited) { | |
E(seatRef).tryExit(); | |
} | |
}); | |
E.when( | |
E(seatRef).hasExited(), | |
hasExited => { | |
if (!hasExited) { | |
E(seatRef).tryExit(); | |
} | |
}, | |
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using E.when()
always is easier than thinking, but in this case I'm not going to insist: The risk is that the supposed-promise from hasExited()
would be have a bogus then
method, but it comes from zoe, and the contract pretty much has to assume zoe is correct; that is: it relies on zoe. In fact, I think this promise comes from E
, which the contract relies on even more.
*/ | ||
async tryExitOffer(offerId) { | ||
const seatRef = this.state.liveOfferSeats.get(offerId); | ||
await E(seatRef).tryExit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it need to return await E(seatRef).tryExit();
in order to actually throw/break the returned promise on failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no. There are no good uses for return await x
; it's equivalent to return x
.
Under the normal async
transformation, a function that ends with an await x
expression statement is like one that ends with return x.then(_ => undefined)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, my point wasn't that it should return await
it was that it should return
the results of .tryExit()
.
Does it need to return E(seatRef).tryExit();
in order to actually throw/break the returned promise on failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it need to
return E(seatRef).tryExit();
in order to actually throw/break the returned promise on failure?
no, the await
will propagate the promise rejection from E if the seat's tryExit
throws.
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## master #7028 +/- ##
==========================================
- Coverage 79.38% 70.64% -8.75%
==========================================
Files 396 443 +47
Lines 74792 84631 +9839
Branches 3 3
==========================================
+ Hits 59373 59785 +412
- Misses 15418 24780 +9362
- Partials 1 66 +65
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good stuff.
I make a few somewhat strong suggestions in here... but not of them quite critical.
@@ -160,4 +160,5 @@ test.todo( | |||
// pause the PSM trading such that there is time to exit before offer resolves | |||
// executeOffer to buy the junk (which can't resolve) | |||
// exit the offer "oh I don't want to buy junk!" | |||
// Help? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prune this? (I got distracted)
@@ -48,7 +48,7 @@ const DEFAULT_DECIMALS = 9; | |||
* added in the appropriate place and settled when the price reaches that level. | |||
*/ | |||
|
|||
const trace = makeTracer('AucBook', false); | |||
const trace = makeTracer('AucBook'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does look odd.
@@ -290,10 +291,12 @@ export const prepareAuctionBook = (baggage, zcf) => { | |||
: AmountMath.makeEmptyFromAmount(want); | |||
|
|||
const stillWant = AmountMath.subtract(want, collateralSold); | |||
trace('acceptScaledBidOffer', { stillWant }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason we're adding tracing to auctionBook
in order to add tryExit
to the smart wallet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
used for debugging of the bid life cycle. the test of tryExit
is exit bid
.
@@ -48,7 +48,7 @@ const DEFAULT_DECIMALS = 9; | |||
* added in the appropriate place and settled when the price reaches that level. | |||
*/ | |||
|
|||
const trace = makeTracer('AucBook', false); | |||
const trace = makeTracer('AucBook'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect we'll audit all tracers before cutting the release
Well, here's hoping.
#5222 doesn't seem to be scheduled, though.
@@ -230,7 +229,7 @@ const makeBidOffer = (brands, opts) => { | |||
instancePath: ['auctioneer'], | |||
callPipe: [['makeBidInvitation', [collateralBrand]]], | |||
}, | |||
proposal, | |||
proposal: { give, exit: { onDemand: null } }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏
*/ | ||
async tryExitOffer(offerId) { | ||
const seatRef = this.state.liveOfferSeats.get(offerId); | ||
await E(seatRef).tryExit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no. There are no good uses for return await x
; it's equivalent to return x
.
Under the normal async
transformation, a function that ends with an await x
expression statement is like one that ends with return x.then(_ => undefined)
.
packages/vats/src/core/lib-boot.js
Outdated
messageVatObjectSendOnly: ({ presence, methodName, args = [] }) => { | ||
const object = decodePassable(presence); | ||
const decodedArgs = args.map(decodePassable); | ||
E(object)[methodName](...decodedArgs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E(object)[methodName](...decodedArgs); | |
void E(object)[methodName](...decodedArgs); |
@@ -73,6 +74,7 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { | |||
|
|||
/** | |||
* @type {( (presence: unknown) => Record<string, (...args: any) => Promise<any>> ) & { | |||
* sendOnly: (presence: unknown) => Record<string, (...args: any) => any>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* sendOnly: (presence: unknown) => Record<string, (...args: any) => any>, | |
* sendOnly: (presence: unknown) => Record<string, (...args: any) => void>, |
/** | ||
* @returns {import('@agoric/smart-wallet/src/smartWallet.js').UpdateRecord} | ||
*/ | ||
getLatestUpdateRecord() { | ||
const key = `published.wallet.${walletAddress}`; | ||
const lastWalletStatus = JSON.parse(storage.data.get(key).at(-1)); | ||
const lastWalletStatus = JSON.parse(storage.data.get(key)?.at(-1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this ?
help? JSON.parse(undefined)
throws. Oh... but it seems to typecheck. weird. maybe a js-mode thing.
14d7295
to
2ba5c85
Compare
2ba5c85
to
97c7a71
Compare
If no exit clause is provided, |
closes: #6906
Description
Makes it possible for clients to
tryExit
a Zoe seat. For them to know what seats are live it also publishesliveSeats
on the smart wallet's.current
record.Security Considerations
Publishing status off live offers, but nothing precious.
Scaling Considerations
Iterates over all
liveSeats
to publish them. Strictly speaking this goes against the limit work by message size rule. But as discussed in #6184, there are somewhat conflicting requirements here and this is the best solution on balance.Documentation Considerations
--
Testing Considerations
New test
exit bid