From 70ed2947c0e7a62a0a9b396b1df9c4b7576ef788 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Fri, 7 Jul 2023 11:02:28 -0700 Subject: [PATCH] feat(zoe): first transferable --- .../src/contractSupport/transferableKit.js | 94 +++++++++++++++++++ .../zoe/src/contracts/transferable-counter.js | 89 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 packages/zoe/src/contractSupport/transferableKit.js create mode 100644 packages/zoe/src/contracts/transferable-counter.js diff --git a/packages/zoe/src/contractSupport/transferableKit.js b/packages/zoe/src/contractSupport/transferableKit.js new file mode 100644 index 000000000000..ff3d76ef97df --- /dev/null +++ b/packages/zoe/src/contractSupport/transferableKit.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 makeTransferablesKit = ( + zcf, + baggage, + detailsShape, + makeUseObject, + getSelfFromThis = defaultGetSelfFromThis, +) => { + const UseObjectMethodGuards = 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 makeUseObject(customDetails); + }, + }, + { + receiveRevoker(revoke) { + revokeTransferHandler = revoke; + }, + }, + ); + + let revokeUseObject; + + const useObjectMethods = harden({ + makeTransferInvitation() { + const self = apply(getSelfFromThis, this, []); + const invitation = zcf.makeInvitation( + // eslint-disable-next-line no-use-before-define + transferHandler, + 'transfer', + self.getCustomDetails(), + TransferProposalShape, + ); + revokeUseObject(self); + return invitation; + }, + }); + + const useObjectOptions = harden({ + receiveRevoker(revoke) { + revokeUseObject = revoke; + }, + }); + + return harden({ + // note: includes getCustomDetails + UseObjectMethodGuards, + // note: does not include getCustomDetails, + // so getCustomDetails is effectively an abstract method that must be + // concretely implemented. + useObjectMethods, + useObjectOptions, + }); +}; +harden(makeTransferablesKit); diff --git a/packages/zoe/src/contracts/transferable-counter.js b/packages/zoe/src/contracts/transferable-counter.js new file mode 100644 index 000000000000..d6f6fcca980f --- /dev/null +++ b/packages/zoe/src/contracts/transferable-counter.js @@ -0,0 +1,89 @@ +import { M } from '@endo/patterns'; +import { prepareExo, prepareExoClass } from '@agoric/vat-data'; +import { makeTransferablesKit } from '../contractSupport/transferableKit.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 makeForwardUseCounter = customDetails => + // eslint-disable-next-line no-use-before-define + makeUseCounter(customDetails); + + const { UseObjectMethodGuards, useObjectMethods, useObjectOptions } = + makeTransferablesKit( + zcf, + instanceBaggage, + CounterDetailsShape, + makeForwardUseCounter, + ); + + const UseCounterI = M.interface('UseCounter', { + ...UseObjectMethodGuards, + incr: M.call().returns(M.bigint()), + }); + + const makeUseCounter = prepareExoClass( + instanceBaggage, + 'UseCounter', + UseCounterI, + customDetails => { + const { count } = customDetails; + assert(count === instanceBaggage.get('count')); + return harden({}); + }, + { + ...useObjectMethods, + + 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'), + }); + }, + }, + + { + ...useObjectOptions, + }, + ); + + 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: makeUseCounter(startCount), + publicFacet: viewCounter, + }); +}; +harden(start);