Skip to content

Commit

Permalink
feat(vat-data,zoe): caretaker-style revocable, ownable
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 12, 2024
1 parent d48e752 commit 6d41337
Show file tree
Hide file tree
Showing 9 changed files with 523 additions and 26 deletions.
1 change: 1 addition & 0 deletions packages/vat-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"author": "Agoric",
"license": "Apache-2.0",
"dependencies": {
"@endo/patterns": "^1.0.1",
"@agoric/assert": "^0.6.0",
"@agoric/internal": "^0.3.2",
"@agoric/store": "^0.9.2",
Expand Down
1 change: 1 addition & 0 deletions packages/vat-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export {
// deprecated
prepareSingleton,
} from './exo-utils.js';
export * from './prepare-revocable.js';

/** @typedef {import('@agoric/swingset-liveslots').DurableKindHandle} DurableKindHandle */
/** @template T @typedef {import('@agoric/swingset-liveslots').DefineKindOptions<T>} DefineKindOptions */
Expand Down
119 changes: 119 additions & 0 deletions packages/vat-data/src/prepare-revocable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { M } from '@endo/patterns';
import { fromUniqueEntries } from '@agoric/internal';
import { prepareExoClassKit } from './exo-utils.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
*/

/**
* Make an exo class kit for wrapping an underlying exo class,
* where the wrapper is a revocable forwarder
*
* @template {any} [U=any]
* @param {import('./exo-utils').Baggage} baggage
* @param {string} uKindName
* The `kindName` of the underlying exo class
* @param {string} uInterfaceName
* The `interfaceName` of the underlying interface guard
* @param {(string|symbol)[]} uMethodNames
* The method names of the underlying exo class
* @param {Record<
* string|symbol,
* import('@endo/patterns').MethodGuard
* >} [extraMethodGuards]
* @param {Record<
* string|symbol,
* (...args) => any
* >} [extraMethods]
* @returns {(underlying: U) => RevocableKit<U>}
*/
export const prepareRevocableKit = (
baggage,
uKindName,
uInterfaceName,
uMethodNames,
extraMethodGuards = undefined,
extraMethods = undefined,
) => {
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 = prepareExoClassKit(
baggage,
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) {
const {
state: {
// @ts-expect-error normal exo-this typing confusion
underlying,
},
} = this;
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')),
},
},
);

// @ts-expect-error type parameter confusion
return makeRevocableKit;
};
harden(prepareRevocableKit);
127 changes: 127 additions & 0 deletions packages/vat-data/test/test-prepare-revocable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Modeled on test-revoke-heap-classes.js

import { test } from './prepare-test-env-ava.js';

// eslint-disable-next-line import/order
import { M } from '@agoric/store';
import { prepareExoClass, prepareExoClassKit } from '../src/exo-utils.js';
import { makeScalarBigMapStore } from '../src/vat-data-bindings.js';
import { prepareRevocableKit } from '../src/prepare-revocable.js';

const UpCounterI = M.interface('UpCounter', {
incr: M.call()
// TODO M.number() should not be needed to get a better error message
.optional(M.and(M.number(), M.gte(0)))
.returns(M.number()),
});

const DownCounterI = M.interface('DownCounter', {
decr: M.call()
// TODO M.number() should not be needed to get a better error message
.optional(M.and(M.number(), M.gte(0)))
.returns(M.number()),
});

test('test revoke defineVirtualExoClass', t => {
const baggage = makeScalarBigMapStore('fakeRootBaggage');

const makeUnderlyingUpCounter = prepareExoClass(
baggage,
'UpCounter',
UpCounterI,
/** @param {number} x */
(x = 0) => ({ x }),
{
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
);

const makeRevocableUpCounterKit = prepareRevocableKit(
baggage,
'UpCounter',
'UpCounter',
['incr'],
);

const makeUpCounterKit = x =>
makeRevocableUpCounterKit(makeUnderlyingUpCounter(x));

const { revoker, revocable: upCounter } = makeUpCounterKit(3);
t.is(upCounter.incr(5), 8);
t.is(revoker.revoke(), true);
t.throws(() => upCounter.incr(1), {
message: '"UpCounter_caretaker" revoked',
});
});

test('test revoke defineVirtualExoClassKit', t => {
const baggage = makeScalarBigMapStore('fakeRootBaggage');

const makeUnderlyingCounterKit = prepareExoClassKit(
baggage,
'Counter',
{ up: UpCounterI, down: DownCounterI },
/** @param {number} x */
(x = 0) => ({ x }),
{
up: {
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
down: {
decr(y = 1) {
const { state } = this;
state.x -= y;
return state.x;
},
},
},
);

const makeRevocableUpCounterKit = prepareRevocableKit(
baggage,
'UpCounter',
'UpCounter',
['incr'],
{
selfRevoke: M.call().returns(M.boolean()),
},
{
selfRevoke() {
const {
facets: {
// @ts-expect-error typing this
revoker,
},
} = this;
return revoker.revoke();
},
},
);

const makeCounterKit = x => {
const { up: upCounter, down: downCounter } = makeUnderlyingCounterKit(x);
const { revocable: revocableUpCounter } =
makeRevocableUpCounterKit(upCounter);
return harden({
up: revocableUpCounter,
down: downCounter,
});
};

const { up: upCounter, down: downCounter } = makeCounterKit(3);
t.is(upCounter.incr(5), 8);
t.is(downCounter.decr(), 7);
t.is(upCounter.selfRevoke(), true);
t.throws(() => upCounter.incr(3), {
message: '"UpCounter_caretaker" revoked',
});
t.is(downCounter.decr(), 6);
});
27 changes: 2 additions & 25 deletions packages/zoe/src/contractFacet/types-ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ type ZCFGetAmountAllocated = (
keyword: Keyword,
brand?: Brand<AssetKind> | undefined,
) => Amount<any>;

type ZCFSeat = {
exit: (completion?: Completion) => void;
fail: ZCFSeatFail;
Expand All @@ -179,32 +180,8 @@ type ZCFSeat = {
getProposal: () => ProposalRecord;
getAmountAllocated: ZCFGetAmountAllocated;
getCurrentAllocation: () => Allocation;
/**
* @deprecated Use atomicRearrange instead
*/
getStagedAllocation: () => Allocation;
/**
* @deprecated Use atomicRearrange instead
*/
hasStagedAllocation: () => boolean;
isOfferSafe: (newAllocation: Allocation) => boolean;
/**
* @deprecated Use atomicRearrange instead
*/
incrementBy: (
amountKeywordRecord: AmountKeywordRecord,
) => AmountKeywordRecord;
/**
* @deprecated Use atomicRearrange instead
*/
decrementBy: (
amountKeywordRecord: AmountKeywordRecord,
) => AmountKeywordRecord;
/**
* @deprecated Use atomicRearrange instead
*/
clear: () => void;
};

type ZcfSeatKit = {
zcfSeat: ZCFSeat;
userSeat: ERef<UserSeat>;
Expand Down
81 changes: 81 additions & 0 deletions packages/zoe/src/contractSupport/prepare-ownable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { M } from '@endo/patterns';
import { prepareExoClass, prepareRevocableKit } from '@agoric/vat-data';
import { OfferHandlerI } from '../typeGuards.js';

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

export const prepareOwnable = (
zcf,
baggage,
uKindName,
uInterfaceName,
uMethodNames,
) => {
const makeRevocableKit = prepareRevocableKit(
baggage,
uKindName,
uInterfaceName,
uMethodNames,
{
makeTransferInvitation: M.call().returns(M.promise()),
},
{
makeTransferInvitation() {
const {
state: {
// @ts-expect-error `this` typing
underlying,
},
facets: {
// @ts-expect-error `this` typing
revoker,
},
} = this;
const customDetails = underlying.getCustomDetails();
// eslint-disable-next-line no-use-before-define
const transferHandler = makeTransferHandler(underlying);

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

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

const makeOwnable = underlying => {
const { revocable } = makeRevocableKit(underlying);
return revocable;
};
return harden(makeOwnable);
};
harden(prepareOwnable);
Loading

0 comments on commit 6d41337

Please sign in to comment.