@agoric/zoe
Version:
Zoe: the Smart Contract Framework for Offer Enforcement
391 lines (357 loc) • 11.9 kB
JavaScript
import { E } from '@endo/eventual-send';
import { passStyleOf } from '@endo/marshal';
import {
M,
provideDurableWeakMapStore,
prepareExoClass,
prepareExo,
watchPromise,
} from '@agoric/vat-data';
import { initEmpty } from '@agoric/store';
import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';
import { Fail, q } from '@endo/errors';
import { defineDurableHandle } from '../makeHandle.js';
import { makeInstanceAdminMaker } from './instanceAdminStorage.js';
import {
AdminFacetI,
InstanceAdminI,
InstanceAdminShape,
} from '../typeGuards.js';
/**
* @import {Baggage} from '@agoric/vat-data';
* @import {WeakMapStore} from '@agoric/store';
* @import {BundleCap} from '@agoric/swingset-vat';
*/
/**
* @param {Pick<ZoeStorageManager, 'makeZoeInstanceStorageManager' | 'unwrapInstallation'>} startInstanceAccess
* @param {() => ERef<BundleCap>} getZcfBundleCapP
* @param {(id: string) => BundleCap} getBundleCapByIdNow
* @param {Baggage} zoeBaggage
* @returns {import('./utils.js').StartInstance}
*/
export const makeStartInstance = (
startInstanceAccess,
getZcfBundleCapP,
getBundleCapByIdNow,
zoeBaggage,
) => {
const makeInstanceHandle = defineDurableHandle(zoeBaggage, 'Instance');
/** @type {WeakMapStore<SeatHandle, ZoeSeatAdmin>} */
const seatHandleToZoeSeatAdmin = provideDurableWeakMapStore(
zoeBaggage,
'seatHandleToZoeSeatAdmin',
);
const instanceAdminMaker = makeInstanceAdminMaker(
zoeBaggage,
seatHandleToZoeSeatAdmin,
);
const getFreshZcfBundleCap = async () => {
const settledBundleCap = await getZcfBundleCapP();
settledBundleCap !== undefined || Fail`the ZCF bundle cap was broken`;
return settledBundleCap;
};
const InstanceAdminStateShape = harden({
instanceStorage: M.remotable('ZoeInstanceStorageManager'),
instanceAdmin: M.remotable('InstanceAdmin'),
seatHandleToSeatAdmin: M.remotable(), // seatHandleToSeatAdmin, but putting that string here is backwards-incompatible
adminNode: M.remotable('adminNode'),
});
/** @type {import('@agoric/swingset-liveslots').PromiseWatcher<Completion, [InstanceAdmin, Handle<'adminNode'>]>} */
const watcher = prepareExo(
zoeBaggage,
'InstanceCompletionWatcher',
M.interface('InstanceCompletionWatcher', {
onFulfilled: M.call(
M.any(),
InstanceAdminShape,
M.remotable('adminNode'),
).returns(),
onRejected: M.call(
M.any(),
InstanceAdminShape,
M.remotable('adminNode'),
).returns(),
}),
{
onFulfilled: (completion, instanceAdmin) =>
instanceAdmin.exitAllSeats(completion),
onRejected: (/** @type {Error} */ reason, instanceAdmin, adminNode) => {
if (isUpgradeDisconnection(reason)) {
console.log(`resetting promise watcher after upgrade`, reason);
watchForAdminNodeDone(adminNode, instanceAdmin);
} else {
instanceAdmin.failAllSeats(reason);
}
},
},
);
const watchForAdminNodeDone = (adminNode, instAdmin) => {
watchPromise(E(adminNode).done(), watcher, instAdmin, adminNode);
};
const makeZoeInstanceAdmin = prepareExoClass(
zoeBaggage,
'zoeInstanceAdmin',
InstanceAdminI,
/**
*
* @param {ZoeInstanceStorageManager} instanceStorage
* @param {InstanceAdmin} instanceAdmin
* @param {WeakMapStore<SeatHandle, ZoeSeatAdmin>} seatHandleToSeatAdmin
* @param {import('@agoric/swingset-vat').VatAdminFacet} adminNode
*/
(instanceStorage, instanceAdmin, seatHandleToSeatAdmin, adminNode) => ({
instanceStorage,
instanceAdmin,
seatHandleToSeatAdmin,
adminNode,
}),
{
makeInvitation(handle, desc, customDetails, proposalShape) {
const { state } = this;
return state.instanceStorage.makeInvitation(
handle,
desc,
customDetails,
proposalShape,
);
},
// checks of keyword done on zcf side
saveIssuer(issuer, keyword) {
const { state } = this;
return state.instanceStorage.saveIssuer(issuer, keyword);
},
// A Seat requested by the contract without any payments to escrow
makeNoEscrowSeat(initialAllocations, proposal, exitObj, seatHandle) {
const { state } = this;
return state.instanceAdmin.makeNoEscrowSeat(
initialAllocations,
proposal,
exitObj,
seatHandle,
);
},
exitAllSeats(completion) {
const { state } = this;
state.instanceAdmin.exitAllSeats(completion);
},
failAllSeats(reason) {
const { state } = this;
return state.instanceAdmin.failAllSeats(reason);
},
exitSeat(seatHandle, completion) {
const { state } = this;
state.seatHandleToSeatAdmin.get(seatHandle).exit(completion);
},
failSeat(seatHandle, reason) {
const { state } = this;
state.seatHandleToSeatAdmin.get(seatHandle).fail(reason);
},
makeZoeMint(keyword, assetKind, displayInfo, options) {
const { state } = this;
return state.instanceStorage.makeZoeMint(
keyword,
assetKind,
displayInfo,
options,
);
},
registerFeeMint(keyword, feeMintAccess) {
const { state } = this;
return state.instanceStorage.registerFeeMint(keyword, feeMintAccess);
},
replaceAllocations(seatHandleAllocations) {
const { state } = this;
try {
for (const { seatHandle, allocation } of seatHandleAllocations) {
const zoeSeatAdmin = state.seatHandleToSeatAdmin.get(seatHandle);
zoeSeatAdmin.replaceAllocation(allocation);
}
} catch (err) {
// nothing for Zoe to do if the termination fails
void E(state.adminNode).terminateWithFailure(err);
throw err;
}
},
stopAcceptingOffers() {
const { state } = this;
return state.instanceAdmin.stopAcceptingOffers();
},
setOfferFilter(strings) {
const { state } = this;
state.instanceAdmin.setOfferFilter(strings);
},
getOfferFilter() {
const { state } = this;
return state.instanceAdmin.getOfferFilter();
},
getExitSubscriber(seatHandle) {
const { state } = this;
return state.seatHandleToSeatAdmin.get(seatHandle).getExitSubscriber();
},
isBlocked(string) {
const { state } = this;
return state.instanceAdmin.isBlocked(string);
},
repairContractCompletionWatcher() {
const { state, self } = this;
void watchForAdminNodeDone(state.adminNode, self);
},
},
{
stateShape: InstanceAdminStateShape,
},
);
const prepareEmptyFacet = facetName =>
prepareExoClass(
zoeBaggage,
facetName,
M.interface(facetName, {}),
initEmpty,
{},
);
const makeEmptyCreatorFacet = prepareEmptyFacet('emptyCreatorFacet');
const makeEmptyPublicFacet = prepareEmptyFacet('emptyPublicFacet');
const makeAdminFacet = prepareExoClass(
zoeBaggage,
'adminFacet',
AdminFacetI,
/**
*
* @param {import('@agoric/swingset-vat').VatAdminFacet} adminNode
* @param {*} contractBundleCap
*/
(adminNode, contractBundleCap) => ({
adminNode,
contractBundleCap,
}),
{
getVatShutdownPromise() {
const { state } = this;
return E(state.adminNode).done();
},
restartContract(newPrivateArgs = undefined) {
const { state } = this;
const vatParameters = {
contractBundleCap: state.contractBundleCap,
privateArgs: newPrivateArgs,
};
return E.when(getFreshZcfBundleCap(), bCap =>
E(state.adminNode).upgrade(bCap, { vatParameters }),
);
},
async upgradeContract(contractBundleId, newPrivateArgs = undefined) {
const { state } = this;
const newContractBundleCap =
await getBundleCapByIdNow(contractBundleId);
const vatParameters = {
contractBundleCap: newContractBundleCap,
privateArgs: newPrivateArgs,
};
state.contractBundleCap = newContractBundleCap;
return E.when(getFreshZcfBundleCap(), bCap =>
E(state.adminNode).upgrade(bCap, { vatParameters }),
);
},
terminateContract(reason) {
const { state } = this;
return E(state.adminNode).terminateWithFailure(reason);
},
},
);
/**
* @type {import('./utils.js').StartInstance}
*/
const startInstance = async (
installationP,
uncleanIssuerKeywordRecord = harden({}),
// @ts-expect-error FIXME may not match the expected terms of SF
customTerms = harden({}),
privateArgs = undefined,
instanceLabel = '',
) => {
const { installation, bundle, bundleCap } =
await E(startInstanceAccess).unwrapInstallation(installationP);
// AWAIT ///
const contractBundleCap = bundle || bundleCap;
assert(contractBundleCap);
if (privateArgs !== undefined) {
const passStyle = passStyleOf(privateArgs);
passStyle === 'copyRecord' ||
Fail`privateArgs must be a pass-by-copy record, but instead was a ${q(
passStyle,
)}: ${privateArgs}`;
}
const instanceHandle = makeInstanceHandle();
const zoeInstanceStorageManager = await E(
startInstanceAccess,
).makeZoeInstanceStorageManager(
installation,
customTerms,
uncleanIssuerKeywordRecord,
instanceHandle,
contractBundleCap,
instanceLabel,
);
// AWAIT ///
const adminNode = zoeInstanceStorageManager.getAdminNode();
/** @type {ZCFRoot} */
const zcfRoot = zoeInstanceStorageManager.getRoot();
/** @type {InstanceAdmin} */
const instanceAdmin = instanceAdminMaker(
instanceHandle,
zoeInstanceStorageManager,
adminNode,
);
zoeInstanceStorageManager.initInstanceAdmin(instanceHandle, instanceAdmin);
void watchForAdminNodeDone(adminNode, instanceAdmin);
/** @type {ZoeInstanceAdmin} */
// @ts-expect-error XXX saveIssuer
const zoeInstanceAdminForZcf = makeZoeInstanceAdmin(
zoeInstanceStorageManager,
instanceAdmin,
seatHandleToZoeSeatAdmin,
adminNode,
);
// At this point, the contract will start executing. All must be ready
const {
creatorFacet = makeEmptyCreatorFacet(),
publicFacet = makeEmptyPublicFacet(),
creatorInvitation: creatorInvitationP,
handleOfferObj,
} = await E(zcfRoot).startZcf(
zoeInstanceAdminForZcf,
zoeInstanceStorageManager.getInstanceRecord(),
zoeInstanceStorageManager.getIssuerRecords(),
privateArgs,
);
instanceAdmin.initDelayedState(handleOfferObj, publicFacet);
// creatorInvitation can be undefined, but if it is defined,
// let's make sure it is an invitation.
// @ts-expect-error cast
return E.when(
Promise.all([
creatorInvitationP,
creatorInvitationP !== undefined &&
zoeInstanceStorageManager
.getInvitationIssuer()
.isLive(creatorInvitationP),
]),
([creatorInvitation, isLiveResult]) => {
creatorInvitation === undefined ||
isLiveResult ||
Fail`The contract did not correctly return a creatorInvitation`;
const adminFacet = makeAdminFacet(adminNode, contractBundleCap);
// Actually returned to the user.
return harden({
creatorFacet,
// TODO (#5775) deprecate this return value from contracts.
creatorInvitation,
instance: instanceHandle,
publicFacet,
adminFacet,
});
},
);
};
return harden(startInstance);
};