From c53ddd19082e38a2c49e6b0dbad4fd7aa30d6641 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 20 Dec 2023 11:08:11 -0800 Subject: [PATCH] feat: refactor ZoeSeat to drop cyclic structure that blocked GC --- .../zoe/src/zoeService/originalZoeSeat.js | 346 ++++++++++++++++++ packages/zoe/src/zoeService/zoeSeat.js | 302 ++++++++++----- .../contracts/test-automaticRefund.js | 2 +- packages/zoe/test/unitTests/zcf/test-zcf.js | 2 +- 4 files changed, 561 insertions(+), 91 deletions(-) create mode 100644 packages/zoe/src/zoeService/originalZoeSeat.js diff --git a/packages/zoe/src/zoeService/originalZoeSeat.js b/packages/zoe/src/zoeService/originalZoeSeat.js new file mode 100644 index 000000000000..28707dab45d0 --- /dev/null +++ b/packages/zoe/src/zoeService/originalZoeSeat.js @@ -0,0 +1,346 @@ +/* eslint @typescript-eslint/no-floating-promises: "warn" */ +import { prepareDurablePublishKit, SubscriberShape } from '@agoric/notifier'; +import { E } from '@endo/eventual-send'; +import { M, prepareExoClassKit } from '@agoric/vat-data'; +import { deeplyFulfilled } from '@endo/marshal'; +import { makePromiseKit } from '@endo/promise-kit'; + +import { satisfiesWant } from '../contractFacet/offerSafety.js'; +import '../types.js'; +import '../internal-types.js'; +import { + AmountKeywordRecordShape, + KeywordShape, + ExitObjectShape, + PaymentPKeywordRecordShape, +} from '../typeGuards.js'; + +const { Fail } = assert; + +const OriginalZoeSeatIKit = harden({ + zoeSeatAdmin: M.interface('ZoeSeatAdmin', { + replaceAllocation: M.call(AmountKeywordRecordShape).returns(), + exit: M.call(M.any()).returns(), + fail: M.call(M.any()).returns(), + resolveExitAndResult: M.call({ + offerResultPromise: M.promise(), + exitObj: ExitObjectShape, + }).returns(), + getExitSubscriber: M.call().returns(SubscriberShape), + // The return promise is empty, but doExit relies on settlement as a signal + // that the payouts have settled. The exit publisher is notified after that. + finalPayouts: M.call(M.eref(PaymentPKeywordRecordShape)).returns( + M.promise(), + ), + }), + userSeat: M.interface('UserSeat', { + getProposal: M.call().returns(M.promise()), + getPayouts: M.call().returns(M.promise()), + getPayout: M.call(KeywordShape).returns(M.promise()), + getOfferResult: M.call().returns(M.promise()), + hasExited: M.call().returns(M.promise()), + tryExit: M.call().returns(M.promise()), + numWantsSatisfied: M.call().returns(M.promise()), + getFinalAllocation: M.call().returns(M.promise()), + getExitSubscriber: M.call().returns(M.any()), + }), +}); + +const assertHasNotExited = (c, msg) => { + !c.state.instanceAdminHelper.hasExited(c.facets.zoeSeatAdmin) || + assert(!c.state.instanceAdminHelper.hasExited(c.facets.zoeSeatAdmin), msg); +}; + +/** + * declareOldZoeSeatAdminKind declares an exo for the original kind of ZoeSeatKit. + * This version creates a reference cycle that garbage collection can't remove + * because it goes through weakMaps in two different Vats. We've defined a new + * Kind that doesn't have this problem, but we won't upgrade the existing + * objects, so the Kind must continue to be defined, but we don't return the + * maker function. + * + * The original ZoeSeatKit is an object that manages the state + * of a seat participating in a Zoe contract and return its two facets. + * + * The UserSeat is suitable to be handed to an agent outside zoe and the + * contract and allows them to query or monitor the current state, access the + * payouts and result, and call exit() if that's allowed for this seat. + * + * The zoeSeatAdmin is passed by Zoe to the ContractFacet (zcf), to allow zcf to + * query or update the allocation or exit the seat cleanly. + * + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {() => PublishKit} makeDurablePublishKit + */ +export const declareOldZoeSeatAdminKind = (baggage, makeDurablePublishKit) => { + const doExit = ( + zoeSeatAdmin, + currentAllocation, + withdrawFacet, + instanceAdminHelper, + ) => { + /** @type {PaymentPKeywordRecord} */ + const payouts = withdrawFacet.withdrawPayments(currentAllocation); + return E.when( + zoeSeatAdmin.finalPayouts(payouts), + () => instanceAdminHelper.exitZoeSeatAdmin(zoeSeatAdmin), + () => instanceAdminHelper.exitZoeSeatAdmin(zoeSeatAdmin), + ); + }; + + // There is a race between resolveExitAndResult() and getOfferResult() that + // can be limited to when the adminFactory is paged in. If state.offerResult + // is defined, getOfferResult will return it. If it's not defined when + // getOfferResult is called, create a promiseKit, return the promise and store + // the kit here. When resolveExitAndResult() is called, it saves + // state.offerResult and resolves the promise if it exists, then removes the + // table entry. + /** + * @typedef {WeakMap} + */ + const ephemeralOfferResultStore = new WeakMap(); + + // notice that this returns a maker function which we drop on the floor. + prepareExoClassKit( + baggage, + 'ZoeSeatKit', + OriginalZoeSeatIKit, + /** + * + * @param {Allocation} initialAllocation + * @param {ProposalRecord} proposal + * @param {InstanceAdminHelper} instanceAdminHelper + * @param {WithdrawFacet} withdrawFacet + * @param {ERef} [exitObj] + * @param {boolean} [offerResultIsUndefined] + */ + ( + initialAllocation, + proposal, + instanceAdminHelper, + withdrawFacet, + exitObj = undefined, + // emptySeatKits start with offerResult validly undefined; others can set + // it to anything (including undefined) in resolveExitAndResult() + offerResultIsUndefined = false, + ) => { + const { publisher, subscriber } = makeDurablePublishKit(); + return { + currentAllocation: initialAllocation, + proposal, + exitObj, + offerResult: undefined, + offerResultStored: offerResultIsUndefined, + instanceAdminHelper, + withdrawFacet, + publisher, + subscriber, + payouts: harden({}), + exiting: false, + }; + }, + { + zoeSeatAdmin: { + replaceAllocation(replacementAllocation) { + const { state } = this; + assertHasNotExited( + this, + 'Cannot replace allocation. Seat has already exited', + ); + harden(replacementAllocation); + // Merging happens in ZCF, so replacementAllocation can + // replace the old allocation entirely. + + state.currentAllocation = replacementAllocation; + }, + exit(completion) { + const { state, facets } = this; + // Since this method doesn't wait, we could re-enter via exitAllSeats. + // If that happens, we shouldn't re-do any of the work. + if (state.exiting) { + return; + } + assertHasNotExited(this, 'Cannot exit seat. Seat has already exited'); + + state.exiting = true; + E.when( + doExit( + facets.zoeSeatAdmin, + state.currentAllocation, + state.withdrawFacet, + state.instanceAdminHelper, + ), + () => state.publisher.finish(completion), + ); + }, + fail(reason) { + const { state, facets } = this; + // Since this method doesn't wait, we could re-enter via failAllSeats. + // If that happens, we shouldn't re-do any of the work. + if (state.exiting) { + return; + } + + assertHasNotExited(this, 'Cannot fail seat. Seat has already exited'); + + state.exiting = true; + E.when( + doExit( + facets.zoeSeatAdmin, + state.currentAllocation, + state.withdrawFacet, + state.instanceAdminHelper, + ), + () => state.publisher.fail(reason), + () => state.publisher.fail(reason), + ); + }, + // called only for seats resulting from offers. + /** @param {HandleOfferResult} result */ + resolveExitAndResult({ offerResultPromise, exitObj }) { + const { state, facets } = this; + + !state.offerResultStored || + Fail`offerResultStored before offerResultPromise`; + + if (!ephemeralOfferResultStore.has(facets.userSeat)) { + // this was called before getOfferResult + const kit = makePromiseKit(); + kit.resolve(offerResultPromise); + ephemeralOfferResultStore.set(facets.userSeat, kit); + } + + const pKit = ephemeralOfferResultStore.get(facets.userSeat); + E.when( + offerResultPromise, + offerResult => { + // Resolve the ephemeral promise for offerResult + pKit.resolve(offerResult); + // Now we want to store the offerResult in `state` to get it off the heap, + // but we need to handle three cases: + // 1. already durable. (This includes being a remote presence.) + // 2. has promises for durable objects. + // 3. not durable even after resolving promises. + // For #1 we can assign directly, but we deeply await to also handle #2. + void E.when( + deeplyFulfilled(offerResult), + fulfilledOfferResult => { + try { + // In cases 1-2 this assignment will succeed. + state.offerResult = fulfilledOfferResult; + // If it doesn't, then these lines won't be reached so the + // flag will stay false and the promise will stay in the heap + state.offerResultStored = true; + ephemeralOfferResultStore.delete(facets.userSeat); + } catch (err) { + console.warn( + `non-durable offer result will be lost upon zoe vat termination: ${offerResult}`, + ); + } + }, + // no rejection handler because an offer result containing promises that reject + // is within spec + ); + }, + e => { + pKit.reject(e); + // NB: leave the rejected promise in the ephemeralOfferResultStore + // because it can't go in durable state + }, + ); + + state.exitObj = exitObj; + }, + getExitSubscriber() { + const { state } = this; + return state.subscriber; + }, + async finalPayouts(payments) { + const { state } = this; + + const settledPayouts = await deeplyFulfilled(payments); + state.payouts = settledPayouts; + }, + }, + userSeat: { + async getProposal() { + const { state } = this; + return state.proposal; + }, + async getPayouts() { + const { state } = this; + + return E.when( + state.subscriber.subscribeAfter(), + () => state.payouts, + () => state.payouts, + ); + }, + async getPayout(keyword) { + const { state } = this; + + // subscriber.subscribeAfter() only triggers after publisher.finish() + // in exit() or publisher.fail() in fail(). Both of those wait for + // doExit(), which ensures that finalPayouts() has set state.payouts. + return E.when( + state.subscriber.subscribeAfter(), + () => state.payouts[keyword], + () => state.payouts[keyword], + ); + }, + + async getOfferResult() { + const { state, facets } = this; + + if (state.offerResultStored) { + return state.offerResult; + } + + if (ephemeralOfferResultStore.has(facets.userSeat)) { + return ephemeralOfferResultStore.get(facets.userSeat).promise; + } + + const kit = makePromiseKit(); + ephemeralOfferResultStore.set(facets.userSeat, kit); + return kit.promise; + }, + async hasExited() { + const { state, facets } = this; + + return ( + state.exiting || + state.instanceAdminHelper.hasExited(facets.zoeSeatAdmin) + ); + }, + async tryExit() { + const { state } = this; + if (!state.exitObj) + throw Fail`exitObj must be initialized before use`; + assertHasNotExited(this, 'Cannot exit; seat has already exited'); + + return E(state.exitObj).exit(); + }, + async numWantsSatisfied() { + const { state } = this; + return E.when( + state.subscriber.subscribeAfter(), + () => satisfiesWant(state.proposal, state.currentAllocation), + () => satisfiesWant(state.proposal, state.currentAllocation), + ); + }, + getExitSubscriber() { + const { state } = this; + return state.subscriber; + }, + getFinalAllocation() { + const { state } = this; + return E.when( + state.subscriber.subscribeAfter(), + () => state.currentAllocation, + () => state.currentAllocation, + ); + }, + }, + }, + ); +}; diff --git a/packages/zoe/src/zoeService/zoeSeat.js b/packages/zoe/src/zoeService/zoeSeat.js index f58200e164e0..2cc117cbff0e 100644 --- a/packages/zoe/src/zoeService/zoeSeat.js +++ b/packages/zoe/src/zoeService/zoeSeat.js @@ -10,14 +10,33 @@ import '../types.js'; import '../internal-types.js'; import { AmountKeywordRecordShape, - KeywordShape, ExitObjectShape, + KeywordShape, PaymentPKeywordRecordShape, } from '../typeGuards.js'; +import { declareOldZoeSeatAdminKind } from './originalZoeSeat.js'; const { Fail } = assert; -const ZoeSeatIKit = harden({ +// ZoeSeatAdmin has the implementation of these methods, but ZoeUserSeat is the +// facet shared with users. The latter transparently forwards to the former. +const coreUserSeatMethods = harden({ + getProposal: M.call().returns(M.promise()), + getPayouts: M.call().returns(M.promise()), + getPayout: M.call(KeywordShape).returns(M.promise()), + getOfferResult: M.call().returns(M.promise()), + hasExited: M.call().returns(M.promise()), + numWantsSatisfied: M.call().returns(M.promise()), + getFinalAllocation: M.call().returns(M.promise()), + getExitSubscriber: M.call().returns(M.any()), +}); + +const ZoeSeatAdmin = harden({ + userSeatAccess: M.interface('UserSeatAccess', { + ...coreUserSeatMethods, + initExitObjectSetter: M.call(M.any()).returns(), + assertHasNotExited: M.call(M.string()).returns(), + }), zoeSeatAdmin: M.interface('ZoeSeatAdmin', { replaceAllocation: M.call(AmountKeywordRecordShape).returns(), exit: M.call(M.any()).returns(), @@ -33,24 +52,18 @@ const ZoeSeatIKit = harden({ M.promise(), ), }), +}); + +const ZoeUserSeat = harden({ userSeat: M.interface('UserSeat', { - getProposal: M.call().returns(M.promise()), - getPayouts: M.call().returns(M.promise()), - getPayout: M.call(KeywordShape).returns(M.promise()), - getOfferResult: M.call().returns(M.promise()), - hasExited: M.call().returns(M.promise()), + ...coreUserSeatMethods, tryExit: M.call().returns(M.promise()), - numWantsSatisfied: M.call().returns(M.promise()), - getFinalAllocation: M.call().returns(M.promise()), - getExitSubscriber: M.call().returns(M.any()), + }), + exitObjSetter: M.interface('exitObjSetter', { + setExitObject: M.call(M.or(M.remotable(), M.undefined())).returns(), }), }); -const assertHasNotExited = (c, msg) => { - !c.state.instanceAdminHelper.hasExited(c.facets.zoeSeatAdmin) || - assert(!c.state.instanceAdminHelper.hasExited(c.facets.zoeSeatAdmin), msg); -}; - /** * makeZoeSeatAdminFactory returns a maker for an object that manages the state * of a seat participating in a Zoe contract and return its two facets. @@ -70,6 +83,8 @@ export const makeZoeSeatAdminFactory = baggage => { 'zoe Seat publisher', ); + declareOldZoeSeatAdminKind(baggage, makeDurablePublishKit); + const doExit = ( zoeSeatAdmin, currentAllocation, @@ -97,17 +112,15 @@ export const makeZoeSeatAdminFactory = baggage => { */ const ephemeralOfferResultStore = new WeakMap(); - return prepareExoClassKit( + const makeZoeSeatAdmin = prepareExoClassKit( baggage, - 'ZoeSeatKit', - ZoeSeatIKit, + 'ZoeSeatAdmin', + ZoeSeatAdmin, /** - * * @param {Allocation} initialAllocation * @param {ProposalRecord} proposal * @param {InstanceAdminHelper} instanceAdminHelper * @param {WithdrawFacet} withdrawFacet - * @param {ERef} [exitObj] * @param {boolean} [offerResultIsUndefined] */ ( @@ -115,7 +128,6 @@ export const makeZoeSeatAdminFactory = baggage => { proposal, instanceAdminHelper, withdrawFacet, - exitObj = undefined, // emptySeatKits start with offerResult validly undefined; others can set // it to anything (including undefined) in resolveExitAndResult() offerResultIsUndefined = false, @@ -124,7 +136,6 @@ export const makeZoeSeatAdminFactory = baggage => { return { currentAllocation: initialAllocation, proposal, - exitObj, offerResult: undefined, offerResultStored: offerResultIsUndefined, instanceAdminHelper, @@ -133,14 +144,97 @@ export const makeZoeSeatAdminFactory = baggage => { subscriber, payouts: harden({}), exiting: false, + /** @type {{ setExitObject: (exitObj: ExitObj | undefined) => void} | undefined} */ + exitObjectSetter: undefined, }; }, { + // methods for userSeat to call + userSeatAccess: { + async getProposal() { + const { state } = this; + return state.proposal; + }, + async getPayouts() { + const { state } = this; + + return E.when( + state.subscriber.subscribeAfter(), + () => state.payouts, + () => state.payouts, + ); + }, + async getPayout(keyword) { + const { state } = this; + + // subscriber.subscribeAfter() only triggers after publisher.finish() + // in exit() or publisher.fail() in fail(). Both of those wait for + // doExit(), which ensures that finalPayouts() has set state.payouts. + return E.when( + state.subscriber.subscribeAfter(), + () => state.payouts[keyword], + () => state.payouts[keyword], + ); + }, + + async getOfferResult() { + const { state, facets } = this; + + if (state.offerResultStored) { + return state.offerResult; + } + + if (ephemeralOfferResultStore.has(facets.zoeSeatAdmin)) { + return ephemeralOfferResultStore.get(facets.zoeSeatAdmin).promise; + } + + const kit = makePromiseKit(); + ephemeralOfferResultStore.set(facets.zoeSeatAdmin, kit); + return kit.promise; + }, + async hasExited() { + const { state, facets } = this; + + return ( + state.exiting || + state.instanceAdminHelper.hasExited(facets.zoeSeatAdmin) + ); + }, + async numWantsSatisfied() { + const { state } = this; + return E.when( + state.subscriber.subscribeAfter(), + () => satisfiesWant(state.proposal, state.currentAllocation), + () => satisfiesWant(state.proposal, state.currentAllocation), + ); + }, + getExitSubscriber() { + const { state } = this; + return state.subscriber; + }, + getFinalAllocation() { + const { state } = this; + return E.when( + state.subscriber.subscribeAfter(), + () => state.currentAllocation, + () => state.currentAllocation, + ); + }, + initExitObjectSetter(setter) { + this.state.exitObjectSetter = setter; + }, + assertHasNotExited(msg) { + const { state, facets } = this; + const { instanceAdminHelper } = state; + const hasExited1 = instanceAdminHelper.hasExited(facets.zoeSeatAdmin); + + !hasExited1 || assert(!hasExited1, msg); + }, + }, zoeSeatAdmin: { replaceAllocation(replacementAllocation) { - const { state } = this; - assertHasNotExited( - this, + const { state, facets } = this; + facets.userSeatAccess.assertHasNotExited( 'Cannot replace allocation. Seat has already exited', ); harden(replacementAllocation); @@ -156,7 +250,9 @@ export const makeZoeSeatAdminFactory = baggage => { if (state.exiting) { return; } - assertHasNotExited(this, 'Cannot exit seat. Seat has already exited'); + facets.userSeatAccess.assertHasNotExited( + 'Cannot exit seat. Seat has already exited', + ); state.exiting = true; E.when( @@ -168,6 +264,11 @@ export const makeZoeSeatAdminFactory = baggage => { ), () => state.publisher.finish(completion), ); + + if (state.exitObjectSetter) { + state.exitObjectSetter.setExitObject(undefined); + state.exitObjectSetter = undefined; + } }, fail(reason) { const { state, facets } = this; @@ -177,10 +278,12 @@ export const makeZoeSeatAdminFactory = baggage => { return; } - assertHasNotExited(this, 'Cannot fail seat. Seat has already exited'); + facets.userSeatAccess.assertHasNotExited( + 'Cannot fail seat. Seat has already exited', + ); state.exiting = true; - E.when( + void E.when( doExit( facets.zoeSeatAdmin, state.currentAllocation, @@ -190,6 +293,11 @@ export const makeZoeSeatAdminFactory = baggage => { () => state.publisher.fail(reason), () => state.publisher.fail(reason), ); + + if (state.exitObjectSetter) { + state.exitObjectSetter.setExitObject(undefined); + state.exitObjectSetter = undefined; + } }, // called only for seats resulting from offers. /** @param {HandleOfferResult} result */ @@ -199,15 +307,15 @@ export const makeZoeSeatAdminFactory = baggage => { !state.offerResultStored || Fail`offerResultStored before offerResultPromise`; - if (!ephemeralOfferResultStore.has(facets.userSeat)) { + if (!ephemeralOfferResultStore.has(facets.zoeSeatAdmin)) { // this was called before getOfferResult const kit = makePromiseKit(); kit.resolve(offerResultPromise); - ephemeralOfferResultStore.set(facets.userSeat, kit); + ephemeralOfferResultStore.set(facets.zoeSeatAdmin, kit); } - const pKit = ephemeralOfferResultStore.get(facets.userSeat); - E.when( + const pKit = ephemeralOfferResultStore.get(facets.zoeSeatAdmin); + void E.when( offerResultPromise, offerResult => { // Resolve the ephemeral promise for offerResult @@ -227,7 +335,7 @@ export const makeZoeSeatAdminFactory = baggage => { // If it doesn't, then these lines won't be reached so the // flag will stay false and the promise will stay in the heap state.offerResultStored = true; - ephemeralOfferResultStore.delete(facets.userSeat); + ephemeralOfferResultStore.delete(facets.zoeSeatAdmin); } catch (err) { console.warn( `non-durable offer result will be lost upon zoe vat termination: ${offerResult}`, @@ -245,7 +353,8 @@ export const makeZoeSeatAdminFactory = baggage => { }, ); - state.exitObj = exitObj; + // @ts-expect-error exitObjectSetter is set at birth. + state.exitObjectSetter.setExitObject(exitObj); }, getExitSubscriber() { const { state } = this; @@ -258,85 +367,100 @@ export const makeZoeSeatAdminFactory = baggage => { state.payouts = settledPayouts; }, }, + }, + ); + + const makeUserSeat = prepareExoClassKit( + baggage, + 'ZoeUserSeat', + ZoeUserSeat, + (userSeatAccess, exitObj) => { + return { + userSeatAccess, + exitObj, + }; + }, + { userSeat: { async getProposal() { - const { state } = this; - return state.proposal; + return this.state.userSeatAccess.getProposal(); }, async getPayouts() { - const { state } = this; - - return E.when( - state.subscriber.subscribeAfter(), - () => state.payouts, - () => state.payouts, - ); + return this.state.userSeatAccess.getPayouts(); }, async getPayout(keyword) { - const { state } = this; - - // subscriber.subscribeAfter() only triggers after publisher.finish() - // in exit() or publisher.fail() in fail(). Both of those wait for - // doExit(), which ensures that finalPayouts() has set state.payouts. - return E.when( - state.subscriber.subscribeAfter(), - () => state.payouts[keyword], - () => state.payouts[keyword], - ); + return this.state.userSeatAccess.getPayout(keyword); }, async getOfferResult() { - const { state, facets } = this; - - if (state.offerResultStored) { - return state.offerResult; - } - - if (ephemeralOfferResultStore.has(facets.userSeat)) { - return ephemeralOfferResultStore.get(facets.userSeat).promise; - } - - const kit = makePromiseKit(); - ephemeralOfferResultStore.set(facets.userSeat, kit); - return kit.promise; + return this.state.userSeatAccess.getOfferResult(); }, async hasExited() { - const { state, facets } = this; - - return ( - state.exiting || - state.instanceAdminHelper.hasExited(facets.zoeSeatAdmin) - ); + return this.state.userSeatAccess.hasExited(); }, async tryExit() { const { state } = this; + + state.userSeatAccess.assertHasNotExited( + 'Cannot exit; seat has already exited', + ); if (!state.exitObj) - throw Fail`exitObj must be initialized before use`; - assertHasNotExited(this, 'Cannot exit; seat has already exited'); + throw Fail`exitObj not initialized or already nullified`; - return E(state.exitObj).exit(); + const exitResult = E(state.exitObj).exit(); + + // unlink an un-collectible cycle. + state.exitObj = undefined; + + return exitResult; }, async numWantsSatisfied() { - const { state } = this; - return E.when( - state.subscriber.subscribeAfter(), - () => satisfiesWant(state.proposal, state.currentAllocation), - () => satisfiesWant(state.proposal, state.currentAllocation), - ); + return this.state.userSeatAccess.numWantsSatisfied(); }, getExitSubscriber() { - const { state } = this; - return state.subscriber; + return this.state.userSeatAccess.getExitSubscriber(); }, getFinalAllocation() { - const { state } = this; - return E.when( - state.subscriber.subscribeAfter(), - () => state.currentAllocation, - () => state.currentAllocation, - ); + return this.state.userSeatAccess.getFinalAllocation(); + }, + }, + exitObjSetter: { + setExitObject(exitObject) { + this.state.exitObj = exitObject; }, }, }, ); + + /** + * @param {Allocation} initialAllocation + * @param {ProposalRecord} proposal + * @param {InstanceAdminHelper} instanceAdminHelper + * @param {WithdrawFacet} withdrawFacet + * @param {ERef} [exitObj] + * @param {boolean} [offerResultIsUndefined] + */ + const makeZoeSeatAdminKit = ( + initialAllocation, + proposal, + instanceAdminHelper, + withdrawFacet, + exitObj = undefined, + offerResultIsUndefined = false, + ) => { + const { zoeSeatAdmin, userSeatAccess } = makeZoeSeatAdmin( + initialAllocation, + proposal, + instanceAdminHelper, + withdrawFacet, + offerResultIsUndefined, + ); + const { userSeat, exitObjSetter } = makeUserSeat(userSeatAccess, exitObj); + userSeatAccess.initExitObjectSetter(exitObjSetter); + + // The original makeZoeSeatAdminKit returned two facets of the same kind. + // This is returning two independent facets. + return { userSeat, zoeSeatAdmin }; + }; + return makeZoeSeatAdminKit; }; diff --git a/packages/zoe/test/unitTests/contracts/test-automaticRefund.js b/packages/zoe/test/unitTests/contracts/test-automaticRefund.js index 317e668a7604..0b2a03f0683f 100644 --- a/packages/zoe/test/unitTests/contracts/test-automaticRefund.js +++ b/packages/zoe/test/unitTests/contracts/test-automaticRefund.js @@ -352,7 +352,7 @@ test('zoe - alice tries to complete after completion has already occurred', asyn t.is(await E(aliceSeat).getOfferResult(), 'The offer was accepted'); await t.throwsAsync(() => E(aliceSeat).tryExit(), { - message: /seat has (already|been) exited/, + message: /exitObj not initialized or already nullified/, }); const moolaPayout = await aliceSeat.getPayout('ContributionA'); diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index 89b326779119..31b3f7a37d5f 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -989,7 +989,7 @@ test(`userSeat.getPayout() should throw from zcf.makeEmptySeatKit`, async t => { // @ts-expect-error deliberate invalid arguments for testing await t.throwsAsync(() => E(userSeat).getPayout(), { message: - 'In "getPayout" method of (ZoeSeatKit userSeat): Expected at least 1 arguments: []', + 'In "getPayout" method of (ZoeUserSeat userSeat): Expected at least 1 arguments: []', }); });