Skip to content

Commit

Permalink
feat(zoe): caretaker-style revocable, ownable (#8745)
Browse files Browse the repository at this point in the history
* feat(zoe): caretaker-style revocable, ownable

* fixup! ts lint tolerance

* fixup! review suggestions

* fixup! move revocable to zoe contractSupport (#8753)

* fixup! review suggestion to zonify

* fixup! correct package versions

* fixup! review suggestions

* fixup! review suggestions

* fixup! typedoc

* fixup! reform typedoc setup

* fixup! better import

* fixup! review suggestions
  • Loading branch information
erights authored Feb 5, 2024
1 parent 121b677 commit f30b379
Show file tree
Hide file tree
Showing 8 changed files with 546 additions and 6 deletions.
3 changes: 3 additions & 0 deletions packages/zoe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"homepage": "https://github.com/Agoric/agoric-sdk#readme",
"dependencies": {
"@agoric/assert": "^0.6.0",
"@agoric/base-zone": "^0.1.0",
"@agoric/ertp": "^0.16.2",
"@agoric/internal": "^0.3.2",
"@agoric/notifier": "^0.6.2",
Expand All @@ -52,7 +53,9 @@
"@agoric/swingset-vat": "^0.32.2",
"@agoric/time": "^0.3.2",
"@agoric/vat-data": "^0.5.2",
"@agoric/zone": "^0.2.2",
"@endo/bundle-source": "^3.0.2",
"@endo/common": "^1.0.2",
"@endo/captp": "^4.0.2",
"@endo/eventual-send": "^1.1.0",
"@endo/far": "^1.0.2",
Expand Down
15 changes: 9 additions & 6 deletions packages/zoe/src/contractFacet/types-ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ type IssuerOptionsRecord = import('@agoric/ertp').IssuerOptionsRecord;
*/
type Completion = any;
type ZCFMakeEmptySeatKit = (exit?: ExitRule | undefined) => ZcfSeatKit;

type MakeInvitation = <Result>(
offerHandler: OfferHandler<Result>,
description: string,
customDetails?: object,
proposalShape?: Pattern,
) => Promise<Invitation<R, A>>;

/**
* Zoe Contract Facet
*
Expand Down Expand Up @@ -54,12 +62,7 @@ type ZCF<CT extends unknown = Record<string, unknown>> = {
* getting in the `customDetails`. `customDetails` will be
* placed in the details of the invitation.
*/
makeInvitation: <Result>(
offerHandler: OfferHandler<Result>,
description: string,
customDetails?: object,
proposalShape?: Pattern,
) => Promise<Invitation<R, A>>;
makeInvitation: MakeInvitation;
shutdown: (completion: Completion) => void;
shutdownWithFailure: ShutdownWithFailure;
getZoeService: () => ERef<ZoeService>;
Expand Down
2 changes: 2 additions & 0 deletions packages/zoe/src/contractSupport/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export {
} from './ratio.js';

export * from './durability.js';
export * from './prepare-ownable.js';
export * from './prepare-revocable.js';
export * from './priceAuthority.js';
export * from './priceQuote.js';
export * from './statistics.js';
Expand Down
94 changes: 94 additions & 0 deletions packages/zoe/src/contractSupport/prepare-ownable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { M } from '@endo/patterns';
import { OfferHandlerI } from '../typeGuards.js';
import { prepareRevocableKit } from './prepare-revocable.js';

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

/**
* @typedef {object} OwnableOptions
* @property {string} [uInterfaceName]
* The `interfaceName` of the underlying interface guard.
* Defaults to the `uKindName`.
*/

/**
* @template {any} [U=any]
* @param {import('@agoric/base-zone').Zone} zone
* @param {MakeInvitation} makeInvitation
* A function with the same behavior as `zcf.makeInvitation`.
* A contract will normally just extract it from its own zcf using the
* argument expression
* ```js
* (...args) => zcf.makeInvitation(...args)
* ```
* See ownable-counter.js for the canonical example.
* @param {string} uKindName
* The `kindName` of the underlying exo class
* @param {(string|symbol)[]} uMethodNames
* The method names of the underlying exo class that should be represented
* by transparently-forwarding methods of the wrapping ownable object.
* @param {OwnableOptions} [options]
* @returns {(underlying: U) => U}
*/
export const prepareOwnable = (
zone,
makeInvitation,
uKindName,
uMethodNames,
options = {},
) => {
const { uInterfaceName = uKindName } = options;
const makeRevocableKit = prepareRevocableKit(zone, uKindName, uMethodNames, {
uInterfaceName,
extraMethodGuards: {
makeTransferInvitation: M.call().returns(M.promise()),
},
extraMethods: {
makeTransferInvitation() {
const { underlying } = this.state;
const { revoker } = this.facets;
const customDetails = underlying.getInvitationCustomDetails();
// eslint-disable-next-line no-use-before-define
const transferHandler = makeTransferHandler(underlying);

const invitation = makeInvitation(
transferHandler,
'transfer',
customDetails,
TransferProposalShape,
);
revoker.revoke();
return invitation;
},
},
});

const makeTransferHandler = zone.exoClass(
'TransferHandler',
OfferHandlerI,
underlying => ({
underlying,
}),
{
handle(seat) {
const { underlying } = this.state;
const { revocable } = makeRevocableKit(underlying);
seat.exit();
return revocable;
},
},
);

const makeOwnable = underlying => {
const { revocable } = makeRevocableKit(underlying);
return revocable;
};
return harden(makeOwnable);
};
harden(prepareOwnable);
137 changes: 137 additions & 0 deletions packages/zoe/src/contractSupport/prepare-revocable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { M } from '@endo/patterns';
import { fromUniqueEntries } from '@endo/common/from-unique-entries.js';

const { Fail, quote: q } = assert;

/**
* @typedef {object} Revoker
* @property {() => boolean} revoke
*/

/**
* @template {any} [U=any]
* @typedef {object} RevocableKit
* @property {Revoker} revoker
* @property {U} revocable
* Forwards to the underlying exo object, until revoked
*/

/**
* @template [U=any]
* @typedef {object} RevocableKitThis
* @property {RevocableKit<U>} facets
* @property {{ underlying: U }} state
*/

/**
* @template {any} [U=any]
* @typedef {object} RevocableKitOptions
* @property {string} [uInterfaceName]
* The `interfaceName` of the underlying interface guard.
* Defaults to the `uKindName`.
* @property {Record<
* string|symbol,
* import('@endo/patterns').MethodGuard
* >} [extraMethodGuards]
* For guarding the `extraMethods`, if you include them below. These appear
* only on the synthesized interface guard for the revocable caretaker, and
* do not necessarily correspond to any method of the underlying.
* @property {Record<
* string|symbol,
* (this: RevocableKitThis<U>, ...args: any[]) => any
* >} [extraMethods]
* Extra methods adding behavior only to the revocable caretaker, and
* do not necessarily correspond to any methods of the underlying.
*/

/**
* Make an exo class kit for wrapping an underlying exo class,
* where the wrapper is a revocable forwarder
*
* @template {any} [U=any]
* @param {import('@agoric/base-zone').Zone} zone
* @param {string} uKindName
* The `kindName` of the underlying exo class
* @param {(string|symbol)[]} uMethodNames
* The method names of the underlying exo class that should be represented
* by transparently-forwarding methods of the revocable caretaker.
* @param {RevocableKitOptions} [options]
* @returns {(underlying: U) => RevocableKit<U>}
*/
export const prepareRevocableKit = (
zone,
uKindName,
uMethodNames,
options = {},
) => {
const {
uInterfaceName = uKindName,
extraMethodGuards = undefined,
extraMethods = undefined,
} = options;
const RevocableIKit = harden({
revoker: M.interface(`${uInterfaceName}_revoker`, {
revoke: M.call().returns(M.boolean()),
}),
revocable: M.interface(
`${uInterfaceName}_revocable`,
{
...extraMethodGuards,
},
{
defaultGuards: 'raw',
},
),
});

const revocableKindName = `${uKindName}_caretaker`;

const makeRevocableKit = zone.exoClassKit(
revocableKindName,
RevocableIKit,
underlying => ({
underlying,
}),
{
revoker: {
revoke() {
const { state } = this;
if (state.underlying === undefined) {
return false;
}
state.underlying = undefined;
return true;
},
},
revocable: {
...fromUniqueEntries(
uMethodNames.map(name => [
name,
{
// Use concise method syntax for exo methods
[name](...args) {
// @ts-expect-error normal exo-this typing confusion
const { underlying } = this.state;
underlying !== undefined ||
Fail`${q(revocableKindName)} revoked`;
return underlying[name](...args);
},
// @ts-expect-error using possible symbol as index type
}[name],
]),
),
...extraMethods,
},
},
{
stateShape: {
underlying: M.opt(M.remotable('underlying')),
},
},
);

// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore parameter confusion
return makeRevocableKit;
};
harden(prepareRevocableKit);
Loading

0 comments on commit f30b379

Please sign in to comment.