Skip to content

Commit

Permalink
feat(zoe): prepare-ownable-object
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 11, 2024
1 parent 92c4196 commit 4b12033
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 0 deletions.
170 changes: 170 additions & 0 deletions packages/zoe/src/contractSupport/prepare-ownable-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
M,
getCopyMapEntries,
getInterfaceGuardPayload,
mustMatch,
} from '@endo/patterns';
import { prepareExoClass } from '@agoric/vat-data';
import { OfferHandlerI } from '../typeGuards.js';

/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

const { fromEntries } = Object;

const TransferProposalShape = M.splitRecord({
give: {},
want: {},
exit: {
onDemand: {},
},
});

export const makePrepareOwnableClass = zcf => {
/**
* @template {object} CustomDetails
* @template {object} State
* @template {Record<PropertyKey, CallableFunction>} T methods
* @param {Baggage} baggage
* @param {string} kindName
* @param {import('@endo/patterns').InterfaceGuard} interfaceGuard
* Does not itself provide guards for
* - `getCustomDetails`
* - `makeTranferInvitation`.
*
* Rather, those guards are automatically added
* @param {(customDetails: CustomDetails) => State} init
* @param {T & ThisType<{
* self: T,
* state: State,
* }>} methods
* Does not itself provide the
* - `makeTransferInvitation` method.
*
* Rather, that method is atomatically added.
* The `methods` parameter must contain a method for
* - `getCustomDetails`
* whose return result will be used to call the `init` function.
* @param {import('@agoric/vat-data').DefineKindOptions<{
* self: T,
* state: State
* }> & {
* detailsShape?: any,
* }} [options]
* If `detailsShape` is provided, it will be used to guard the returns of
* `getCustomDetails`.
* @returns {(customDetails: CustomDetails) => (T & import('@endo/eventual-send').RemotableBrand<{}, T>)}
*/
const prepareOwnableClass = (
baggage,
kindName,
interfaceGuard,
init,
methods,
options = {},
) => {
const { detailsShape = M.any(), ...restOptions } = options;
// TODO what about interfaceGuardPayload options?
const {
interfaceName,
methodGuards,
symbolMethodGuards = undefined,
} = getInterfaceGuardPayload(interfaceGuard);

let ownableInterfaceMethodGuards;
if (symbolMethodGuards === undefined) {
ownableInterfaceMethodGuards = harden({
...methodGuards,
getCustomDetails: M.call().returns(detailsShape),
makeTransferInvitation: M.call().returns(M.promise()),
});
} else {
ownableInterfaceMethodGuards = harden({
...methodGuards,
...fromEntries(getCopyMapEntries(symbolMethodGuards)),
getCustomDetails: M.call().returns(detailsShape),
makeTransferInvitation: M.call().returns(M.promise()),
});
}

const ownableInterfaceGuard = M.interface(
`Ownable_${interfaceName}`,
ownableInterfaceMethodGuards,
);

let revokeTransferHandler;

const makeTransferHandler = prepareExoClass(
baggage,
'TransferHandler',
OfferHandlerI,
customDetails => {
customDetails;
},
{
handle(seat) {
const {
self,
// @ts-expect-error TODO should type `state`
state: { customDetails },
} = this;
seat.exit();
revokeTransferHandler(self);
// eslint-disable-next-line no-use-before-define
return makeOwnableObject(customDetails);
},
},
{
receiveRevoker(revoke) {
revokeTransferHandler = revoke;
},
},
);

let revokeOwnableObject;

const makeTransferInvitation = () => {
// @ts-expect-error TODO Should use `ThisType`
const { self } = this;
const customDetails = self.getCustomDetails();
const transferHandler = makeTransferHandler(customDetails);

const invitation = zcf.makeInvitation(
// eslint-disable-next-line no-use-before-define
transferHandler,
'transfer',
customDetails,
TransferProposalShape,
);
revokeOwnableObject(self);
return invitation;
};

const initWrapper = customDetails => {
mustMatch(customDetails, detailsShape, 'makeOwnableObject');
return init(customDetails);
};

const makeOwnableObject = prepareExoClass(
baggage,
// Might be upgrading from a previous non-ownable class of the same
// kindName.
kindName,
ownableInterfaceGuard,
initWrapper,
{
...methods,
makeTransferInvitation,
},
{
...restOptions,
receiveRevoker(revoke) {
revokeOwnableObject = revoke;
},
},
);

return makeOwnableObject;
};
return harden(prepareOwnableClass);
};
harden(makePrepareOwnableClass);
81 changes: 81 additions & 0 deletions packages/zoe/src/contracts/ownable-counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { M } from '@endo/patterns';
import { prepareExo } from '@agoric/vat-data';
import { makePrepareOwnableClass } from '../contractSupport/prepare-ownable-object.js';

/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

const CounterI = M.interface('Counter', {
incr: M.call().returns(M.bigint()),
});

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 prepareOwnableClass = makePrepareOwnableClass(zcf);

const makeOwnableCounter = prepareOwnableClass(
instanceBaggage,
'OwnableCounter',
CounterI,
customDetails => {
// @ts-expect-error TODO type the counter's `customDetails`
const { count } = customDetails;
assert(count === instanceBaggage.get('count'));
return harden({});
},
{
incr() {
const count = instanceBaggage.get('count') + 1n;
instanceBaggage.set('count', count);
return count;
},

// note: abstract method must be concretely implemented by
// ownable objects
getCustomDetails() {
return harden({
count: instanceBaggage.get('count'),
});
},
},
{
detailsShape: CounterDetailsShape,
},
);

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(
harden({
count: startCount,
}),
),
publicFacet: viewCounter,
});
};
harden(start);
38 changes: 38 additions & 0 deletions packages/zoe/test/unitTests/contracts/test-ownable-counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js';

import path from 'path';

import bundleSource from '@endo/bundle-source';
import { E } from '@endo/eventual-send';

import { makeZoeForTest } from '../../../tools/setup-zoe.js';
import { makeFakeVatAdmin } from '../../../tools/fakeVatAdmin.js';

const filename = new URL(import.meta.url).pathname;
const dirname = path.dirname(filename);

const root = `${dirname}/../../../src/contracts/ownable-counter.js`;

test('zoe - ownable-counter contract', async t => {
const { admin: fakeVatAdmin, vatAdminState } = makeFakeVatAdmin();
const zoe = makeZoeForTest(fakeVatAdmin);
// Pack the contract.
const bundle = await bundleSource(root);
vatAdminState.installBundle('b1-ownable-counter', bundle);
const installation = await E(zoe).installBundleID('b1-ownable-counter');

const { creatorFacet: firstCounter, publicFacet: viewCounter } = await E(
zoe,
).startInstance(
installation,
undefined,
undefined,
harden({
count: 3n,
}),
'c1-ownable-counter',
);

t.is(await E(firstCounter).incr(), 4n);
t.is(await E(viewCounter).view(), 4n);
});

0 comments on commit 4b12033

Please sign in to comment.