-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
170 changes: 170 additions & 0 deletions
170
packages/zoe/src/contractSupport/prepare-ownable-object.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
38
packages/zoe/test/unitTests/contracts/test-ownable-counter.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |