diff --git a/packages/zoe/src/contractSupport/ownableKit.js b/packages/zoe/src/contractSupport/ownableKit.js new file mode 100644 index 00000000000..6bc73efc07c --- /dev/null +++ b/packages/zoe/src/contractSupport/ownableKit.js @@ -0,0 +1,94 @@ +import { M } from '@endo/patterns'; +import { prepareExo } from '@agoric/vat-data'; +import { OfferHandlerI } from '../typeGuards.js'; + +/** @typedef {import('@agoric/vat-data').Baggage} Baggage */ + +const { apply } = Reflect; + +const TransferProposalShape = harden({ + give: {}, + want: {}, + exit: { + onDemand: {}, + }, +}); + +const defaultGetSelfFromThis = { + getSelfFromThis() { + const { self } = this; + return self; + }, +}.getSelfFromThis; + +export const makeOwnableKit = ( + zcf, + baggage, + detailsShape, + makeOwnableObject, + getSelfFromThis = defaultGetSelfFromThis, +) => { + const OwnableObjectMethodGuards = harden({ + incr: M.call().returns(M.bigint()), + getCustomDetails: M.call().returns(detailsShape), + makeTransferInvitation: M.call().returns(M.promise()), + }); + + let revokeTransferHandler; + + const transferHandler = prepareExo( + baggage, + 'TransferHandler', + OfferHandlerI, + { + handle(seat) { + // @ts-expect-error Usual self-typing problem + const { self } = this; + // TODO implement *Seat.getDetails() + const { customDetails } = seat.getDetails(); + seat.exit(); + revokeTransferHandler(self); + return makeOwnableObject(customDetails); + }, + }, + { + receiveRevoker(revoke) { + revokeTransferHandler = revoke; + }, + }, + ); + + let revokeOwnableObject; + + const ownableObjectMethods = harden({ + makeTransferInvitation() { + const self = apply(getSelfFromThis, this, []); + const invitation = zcf.makeInvitation( + // eslint-disable-next-line no-use-before-define + transferHandler, + 'transfer', + self.getCustomDetails(), + TransferProposalShape, + ); + revokeOwnableObject(self); + return invitation; + }, + }); + + const ownableObjectOptions = harden({ + receiveRevoker(revoke) { + revokeOwnableObject = revoke; + }, + }); + + return harden({ + // note: includes getCustomDetails + OwnableObjectMethodGuards, + // note: does not include getCustomDetails, + // so getCustomDetails is effectively an abstract method that must be + // concretely implemented. + ownableObjectMethods, + ownableObjectOptions, + }); +}; +harden(makeOwnableKit); diff --git a/packages/zoe/src/contracts/ownable-counter.js b/packages/zoe/src/contracts/ownable-counter.js new file mode 100644 index 00000000000..7feccf6744e --- /dev/null +++ b/packages/zoe/src/contracts/ownable-counter.js @@ -0,0 +1,92 @@ +import { M } from '@endo/patterns'; +import { prepareExo, prepareExoClass } from '@agoric/vat-data'; +import { makeOwnableKit } from '../contractSupport/ownableKit.js'; + +/** @typedef {import('@agoric/vat-data').Baggage} Baggage */ + +const CounterDetailsShape = harden({ + count: M.bigint(), +}); + +/** + * @param {ZCF} zcf + * @param {{ count: bigint}} privateArgs + * @param {Baggage} instanceBaggage + */ +export const start = async (zcf, privateArgs, instanceBaggage) => { + const { count: startCount = 0n } = privateArgs; + assert.typeof(startCount, 'bigint'); + + // for use by upgraded versions. + const firstTime = !instanceBaggage.has('count'); + if (firstTime) { + instanceBaggage.init('count', startCount); + } + + const makeForwardOwnableCounter = customDetails => + // eslint-disable-next-line no-use-before-define + makeOwnableCounter(customDetails); + + const { + OwnableObjectMethodGuards, + ownableObjectMethods, + ownableObjectOptions, + } = makeOwnableKit( + zcf, + instanceBaggage, + CounterDetailsShape, + makeForwardOwnableCounter, + ); + + const OwnableCounterI = M.interface('OwnableCounter', { + ...OwnableObjectMethodGuards, + incr: M.call().returns(M.bigint()), + }); + + const makeOwnableCounter = prepareExoClass( + instanceBaggage, + 'OwnableCounter', + OwnableCounterI, + customDetails => { + const { count } = customDetails; + assert(count === instanceBaggage.get('count')); + return harden({}); + }, + { + ...ownableObjectMethods, + + incr() { + const count = instanceBaggage.get('count') + 1n; + instanceBaggage.set('count', count); + return count; + }, + + // note: abstract method must be concretely implemented + getCustomDetails() { + return harden({ + count: instanceBaggage.get('count'), + }); + }, + }, + + { + ...ownableObjectOptions, + }, + ); + + const ViewCounterI = M.interface('ViewCounter', { + view: M.call().returns(M.bigint()), + }); + + const viewCounter = prepareExo(instanceBaggage, 'ViewCounter', ViewCounterI, { + view() { + return instanceBaggage.get('count'); + }, + }); + + return harden({ + creatorFacet: makeOwnableCounter(startCount), + publicFacet: viewCounter, + }); +}; +harden(start);