@agoric/deploy-script-support
Version:
Helpers and other support for writing deploy scripts
218 lines (201 loc) • 7.52 kB
JavaScript
// @ts-check
const t = 'makeCoreProposalBehavior';
/**
* @import {Installation} from '@agoric/zoe/src/zoeService/utils.js';
*/
/**
* TODO import these from @agoric/vats when the types are better managed
*
* @typedef {*} ChainBootstrapSpace
* @typedef {*} BootstrapPowers
*/
/**
* @import {ManifestBundleRef} from './externalTypes.js'
* @typedef {[methodName: string, ...args: unknown[]]} FlatMethargs
* @typedef {Record<string, Record<string, unknown>>} Manifest
*/
/**
* These permits are expected to be the minimum powers required by the
* `coreProposalBehavior` function returned from `makeCoreProposalBehavior`.
* They are merged with all of the manifest getter's permits to produce the
* total permits needed by the resulting core proposal (such as might be---and
* generally are---written into a *-permit.json file).
* @see {@link ./writeCoreEvalParts.js}
*/
export const permits = {
consume: { agoricNamesAdmin: t, vatAdminSvc: t, zoe: t },
evaluateBundleCap: t,
installation: { produce: t },
modules: { utils: { runModuleBehaviors: t } },
};
/**
* Create a behavior for a core-eval proposal.
*
* We rely on directly stringifying this function to leverage our JS toolchain
* for catching bugs. Thus, this maker must not reference any other modules or
* definitions.
*
* @param {object} inputs
* @param {ManifestBundleRef} inputs.manifestBundleRef
* @param {FlatMethargs} inputs.getManifestCall
* @param {Manifest} [inputs.customManifest]
* @param {typeof import('@endo/far').E} inputs.E
* @param {(...args: unknown[]) => void} [inputs.log]
* @param {(ref: import('./externalTypes.js').ManifestBundleRef) => Promise<import('@agoric/zoe/src/zoeService/utils.js').Installation<unknown>>} [inputs.customRestoreRef]
* @returns {(vatPowers: unknown) => Promise<unknown>}
*/
export const makeCoreProposalBehavior = ({
manifestBundleRef,
getManifestCall: [manifestGetterName, ...manifestGetterArgs],
customManifest,
E,
log = console.info,
customRestoreRef,
}) => {
const { entries, fromEntries } = Object;
/**
* Given an object whose properties may be promise-valued, return a promise
* for an analogous object in which each such value has been replaced with its
* fulfillment.
* This is a non-recursive form of endo `deeplyFulfilled`.
*
* @template T
* @param {{[K in keyof T]: (T[K] | Promise<T[K]>)}} obj
* @returns {Promise<T>}
*/
const shallowlyFulfilled = async obj => {
if (!obj) {
return obj;
}
const awaitedEntries = await Promise.all(
entries(obj).map(async ([key, valueP]) => {
const value = await valueP;
return [key, value];
}),
);
return fromEntries(awaitedEntries);
};
const makeRestoreRef = (vatAdminSvc, zoe) => {
/** @type {(ref: import('./externalTypes.js').ManifestBundleRef) => Promise<Installation<unknown>>} */
const defaultRestoreRef = async bundleRef => {
// extract-proposal.js creates these records, and bundleName is
// the optional name under which the bundle was installed into
// config.bundles
const bundleIdP =
'bundleName' in bundleRef
? E(vatAdminSvc).getBundleIDByName(bundleRef.bundleName)
: bundleRef.bundleID;
const bundleID = await bundleIdP;
const label = bundleID.slice(0, 8);
return E(zoe).installBundleID(bundleID, label);
};
return defaultRestoreRef;
};
/** @param {ChainBootstrapSpace & BootstrapPowers & { evaluateBundleCap: any }} powers */
const coreProposalBehavior = async powers => {
// NOTE: `powers` is expected to match or be a superset of the above `permits` export,
// which should therefore be kept in sync with this deconstruction code.
// HOWEVER, do note that this function is invoked with at least the *union* of powers
// required by individual moduleBehaviors declared by the manifest getter, which is
// necessary so it can use `runModuleBehaviors` to provide the appropriate subset to
// each one (see ./writeCoreEvalParts.js).
// Handle `powers` with the requisite care.
const {
consume: { vatAdminSvc, zoe, agoricNamesAdmin },
evaluateBundleCap,
installation: { produce: produceInstallations },
modules: {
utils: { runModuleBehaviors },
},
} = powers;
// Get the on-chain installation containing the manifest and behaviors.
log('evaluateBundleCap', {
manifestBundleRef,
manifestGetterName,
vatAdminSvc,
});
let bcapP;
if ('bundleName' in manifestBundleRef) {
bcapP = E(vatAdminSvc).getNamedBundleCap(manifestBundleRef.bundleName);
} else if ('bundleID' in manifestBundleRef) {
bcapP = E(vatAdminSvc).getBundleCap(manifestBundleRef.bundleID);
} else {
const keys = Reflect.ownKeys(manifestBundleRef).map(key =>
typeof key === 'string' ? JSON.stringify(key) : String(key),
);
const keysStr = `[${keys.join(', ')}]`;
throw Error(
`bundleRef must have own bundleName or bundleID, missing in ${keysStr}`,
);
}
const bundleCap = await bcapP;
const proposalNS = await evaluateBundleCap(bundleCap);
// Get the manifest and its metadata.
log('execute', {
manifestGetterName,
bundleExports: Object.keys(proposalNS),
});
const restoreRef = customRestoreRef || makeRestoreRef(vatAdminSvc, zoe);
const {
manifest,
options: rawOptions,
installations: rawInstallations,
} = await proposalNS[manifestGetterName](
harden({ restoreRef }),
...manifestGetterArgs,
);
// Await promises in the returned options and installations records.
const [options, installations] = await Promise.all(
[rawOptions, rawInstallations].map(shallowlyFulfilled),
);
// Publish the installations for our dependencies.
const installationEntries = entries(installations || {});
if (installationEntries.length > 0) {
const installAdmin = E(agoricNamesAdmin).lookupAdmin('installation');
await Promise.all(
installationEntries.map(([key, value]) => {
produceInstallations[key].reset();
produceInstallations[key].resolve(value);
return E(installAdmin).update(key, value);
}),
);
}
// Evaluate the manifest.
return runModuleBehaviors({
// Remember that `powers` may be arbitrarily broad.
allPowers: powers,
behaviors: proposalNS,
manifest: customManifest || manifest,
makeConfig: (name, _permit) => {
log('coreProposal:', name);
return { options };
},
});
};
return coreProposalBehavior;
};
/**
* @param {object} inputs
* @param {Array<{ ref: ManifestBundleRef, call: FlatMethargs, customManifest?: Manifest }>} inputs.metadataRecords
* @param {typeof import('@endo/far').E} inputs.E
*/
export const makeEnactCoreProposalsFromBundleRef = ({ metadataRecords, E }) => {
/**
* @param {ChainBootstrapSpace & BootstrapPowers & { evaluateBundleCap: any }} powers
* @returns {Promise<void>}
*/
const enactCoreProposals = async powers => {
await Promise.all(
metadataRecords.map(async ({ ref, call, customManifest }) => {
const coreProposalBehavior = makeCoreProposalBehavior({
manifestBundleRef: ref,
getManifestCall: call,
customManifest,
E,
});
return coreProposalBehavior(powers);
}),
);
};
return enactCoreProposals;
};