Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contractStarter): providing terms, privateArgs to started contracts #17

Open
wants to merge 7 commits into
base: dc-boot-tools
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"make:help": "make list",
"start": "yarn docker:make clean start-contract print-key",
"build": "exit 0",
"build:deployer": "rollup -c rollup.config.mjs src/start-contractStarter.js",
"test": "ava --verbose",
"lint": "eslint '**/*.{js,jsx}'",
"lint-fix": "eslint --fix '**/*.{js,jsx}'",
Expand Down
40 changes: 40 additions & 0 deletions contract/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @file rollup configuration to bundle core-eval script
*
* Supports developing core-eval script, permit as a module:
* - import { E } from '@endo/far'
* We can strip this declaration during bundling
* since the core-eval scope includes exports of @endo/far
* - `bundleID = ...` is replaced using updated/cached bundle hash
* - `main` export is appended as script completion value
* - `permit` export is emitted as JSON
*/
// @ts-check
import {
coreEvalGlobals,
moduleToScript,
configureBundleID,
emitPermit,
} from './tools/rollup-plugin-core-eval.js';
import { permit } from './src/start-contractStarter.js';

/** @type {import('rollup').RollupOptions} */
const config = {
output: {
globals: coreEvalGlobals,
file: 'bundles/deploy-starter.js',
format: 'es',
footer: 'main',
},
external: ['@endo/far'],
plugins: [
configureBundleID({
name: 'contractStarter',
rootModule: './src/contractStarter.js',
cache: 'bundles',
}),
moduleToScript(),
emitPermit({ permit, file: 'deploy-starter-permit.json' }),
],
};
export default config;
216 changes: 186 additions & 30 deletions contract/src/contractStarter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,94 @@
* An experiment in delegating the right to start new contracts
* from the full set of stakers to something smaller.
*
* WARNING: anyone can start anything for free.
* WARNING: anyone can start anything.
*
* Options:
* - charge a fee to start a contract
* - charge a fee to install a bundle
* TODO(#24): use governance framework; for example...
* - use a governed API (in the sense of @agoric/governance)
* for install, start
* - use a governed API for install
* - use a governed API for access to bootstrap powers
dckc marked this conversation as resolved.
Show resolved Hide resolved
*
* Issues:
* ISSUE(#25):
* - adminFacet is NOT SAVED. UPGRADE IS IMPOSSIBLE
* - smartWallet provides no effective way to provide privateArgs
*/
// @ts-check

import { E, Far } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import {
InstallationShape,
IssuerRecordShape,
IssuerKeywordRecordShape,
} from '@agoric/zoe/src/typeGuards.js';
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { AmountShape } from '@agoric/ertp/src/typeGuards.js';
import { AmountMath } from '@agoric/ertp/src/amountMath.js';
import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
import { BOARD_AUX_PATH_SEGMENT, publish } from './boardAux.js';

/** @template SF @typedef {import('@agoric/zoe/src/zoeService/utils').StartParams<SF>} StartParams<SF> */

const { fromEntries, keys } = Object;
const { Fail } = assert;

// /** @type {ContractMeta} */
// const meta = {};
/** @type {import('./types').ContractMeta} */
export const meta = harden({
customTermsShape: {
prices: {
startInstance: AmountShape,
installBundleID: AmountShape,
storageNode: AmountShape,
timerService: AmountShape,
board: AmountShape,
priceAuthority: AmountShape,
},
namesByAddress: M.remotable('namesByAddress'),
agoricNames: M.remotable('agoricNames'),
},
privateArgsShape: {
storageNode: M.remotable('storageNode'),
timerService: M.remotable('timerService'),
board: M.remotable('board'),
priceAuthority: M.remotable('priceAuthority'),
},
});
// subordinating these shapes under meta was added after zoe launched on mainnet
export const customTermsShape = meta.customTermsShape;
export const privateArgsShape = meta.privateArgsShape;
dckc marked this conversation as resolved.
Show resolved Hide resolved

/**
* @typedef {{
* namesByAddress: NameHub,
* agoricNames: NameHub,
* }} PublicServices
*/

/**
* @typedef {Record<keyof LimitedAccess
* | 'startInstance' | 'installBundleID', Amount<'nat'>>
* } Prices
*/

/**
* @typedef {{
* storageNode: StorageNode,
* timerService: unknown,
* board: import('@agoric/vats').Board,
* priceAuthority: unknown,
* }} LimitedAccess
*/

export const InstallOptsShape = M.splitRecord(
{ bundleID: M.string() },
{ label: M.string() },
);

/**
* @typedef {{
* bundleID: string,
* label?: string // XXX call it bundleLabel?
* }} InstallOpts
*/

/**
* @see {ZoeService.startInstance}
Expand All @@ -44,12 +102,17 @@ export const StartOptionsShape = M.and(
M.splitRecord({ installation: InstallationShape }),
),
M.partial({
issuerKeywordRecord: IssuerRecordShape,
issuerKeywordRecord: IssuerKeywordRecordShape,
customTerms: M.any(),
privateArgs: M.any(),
instanceLabel: M.string(),
permit: M.partial({
storageNode: BOARD_AUX_PATH_SEGMENT,
timerService: true,
}),
}),
);

// TODO: generate types from shapes (IOU issue #)
/**
* @template SF
Expand All @@ -59,21 +122,88 @@ export const StartOptionsShape = M.and(
* issuerKeywordRecord: Record<string, Issuer>,
* customTerms: StartParams<SF>['terms'],
* privateArgs: StartParams<SF>['privateArgs'],
* instanceLabel: string,
* instanceLabel: string, // XXX add bundleLabel?
* permit: Record<keyof LimitedAccess, string | true>,
* }>} StartOptions
*/

const noHandler = () => Fail`no handler`;
const NoProposalShape = M.not(M.any());

const { add, makeEmpty } = AmountMath;
/** @param {Amount<'nat'>[]} xs */
const sum = xs =>
xs.reduce((subtot, x) => add(subtot, x), makeEmpty(xs[0].brand));

/**
* @param {ZCF} zcf
* @param {unknown} _privateArgs
* @param {ZCF<PublicServices & { prices: Prices }>} zcf
* @param {LimitedAccess} limitedPowers
* @param {unknown} _baggage
*/
export const start = (zcf, _privateArgs, _baggage) => {
export const start = (zcf, limitedPowers, _baggage) => {
const { prices } = zcf.getTerms();
const { storageNode, board } = limitedPowers;

const zoe = zcf.getZoeService();
const invitationIssuerP = E(zoe).getInvitationIssuer();
// TODO(#26): let creator collect fees
const { zcfSeat: fees } = zcf.makeEmptySeatKit();
dckc marked this conversation as resolved.
Show resolved Hide resolved

const pubMarshaller = E(board).getPublishingMarshaller();

const InstallProposalShape = M.splitRecord({
give: { Fee: M.gte(prices.installBundleID) },
// TODO: want: { Started: StartedAmountPattern }
});

/**
* @param {ZCFSeat} seat
* @param {string} description
* @param {{ installation: unknown, instance?: unknown }} handles
*/
const depositHandles = async (seat, description, handles) => {
const handlesInDetails = zcf.makeInvitation(
noHandler,
description,
handles,
NoProposalShape,
);
const amt = await E(invitationIssuerP).getAmountOf(handlesInDetails);
await depositToSeat(
zcf,
seat,
{ Handles: amt },
{ Handles: handlesInDetails },
);
};

/**
* @param {ZCFSeat} seat
* @param {InstallOpts} opts
*/
const installHandler = async (seat, opts) => {
mustMatch(opts, InstallOptsShape);

atomicRearrange(
zcf,
harden([[seat, fees, { Fee: prices.installBundleID }]]),
);

const { bundleID, label } = opts;
const installation = await E(zoe).installBundleID(bundleID, label);

await depositHandles(seat, 'installed', { installation });
seat.exit();
return harden(`${opts.label} installed`);
};

const makeInstallInvitation = () =>
zcf.makeInvitation(
installHandler,
'install',
undefined,
InstallProposalShape,
);

// NOTE: opts could be moved to offerArgs to
// save one layer of closure, but
Expand All @@ -93,45 +223,71 @@ export const start = (zcf, _privateArgs, _baggage) => {
const makeStartInvitation = async opts => {
mustMatch(opts, StartOptionsShape);

const Fee = sum([
prices.startInstance,
...('installation' in opts ? [] : [prices.installBundleID]),
...keys(opts.permit || {}).map(k => prices[k]),
]);

const StartProposalShape = M.splitRecord({
give: { Fee: M.gte(Fee) },
// TODO: want: { Started: StartedAmountPattern }
});

/** @param {ZCFSeat} seat */
const handleStart = async seat => {
atomicRearrange(zcf, harden([[seat, fees, { Fee }]]));
const installation = await ('installation' in opts
? opts.installation
: E(zoe).installBundleID(opts.bundleID));
: E(zoe).installBundleID(opts.bundleID, opts.instanceLabel));

const { issuerKeywordRecord, customTerms, privateArgs, instanceLabel } =
opts;
const { storageNode: nodePermit, ...permit } = opts.permit || {};
const powers = fromEntries(
keys(permit || {}).map(k => [k, limitedPowers[k]]),
);
/** @type {StartedInstanceKit<SF>} */
const it = await E(zoe).startInstance(
installation,
issuerKeywordRecord,
customTerms,
privateArgs,
{ ...privateArgs, ...powers },
instanceLabel,
);
// WARNING: adminFacet is dropped
// TODO: WARNING: adminFacet is dropped
const { instance, creatorFacet } = it;

const handlesInDetails = zcf.makeInvitation(
noHandler,
'started',
{ instance, installation },
NoProposalShape,
);
const amt = await E(invitationIssuerP).getAmountOf(handlesInDetails);
await depositToSeat(
zcf,
seat,
{ Started: amt },
{ Started: handlesInDetails },
const itsTerms = await E(zoe).getTerms(instance);
const itsId = await E(board).getId(instance);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing is putting the installation on the board:

{
  installation: Object [Alleged: BundleIDInstallation#null] {},
  instance: Object [Alleged: InstanceHandle#board064124] {}
}

const itsNode = await E(storageNode).makeChildNode(itsId);
await publish(
itsNode,
{ terms: itsTerms, label: instanceLabel },
pubMarshaller,
);

if (nodePermit) {
const itsStorage = await E(itsNode).makeChildNode('info');
// @ts-expect-error nodePermit implies this method
await E(creatorFacet).initStorageNode(itsStorage);
dckc marked this conversation as resolved.
Show resolved Hide resolved
}

await depositHandles(seat, 'started', { instance, installation });
seat.exit();
return harden({ invitationMakers: creatorFacet });
};
return zcf.makeInvitation(handleStart, 'start');

return zcf.makeInvitation(
handleStart,
'start',
undefined,
StartProposalShape,
);
};

const publicFacet = Far('PublicFacet', {
makeInstallInvitation,
makeStartInvitation,
});

Expand Down
29 changes: 29 additions & 0 deletions contract/src/fixHub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @ts-check
import { E, Far } from '@endo/far';

const { Fail } = assert;

/**
* ref https://github.com/Agoric/agoric-sdk/issues/8408#issuecomment-1741445458
*
* @param {ERef<import('@agoric/vats').NameAdmin>} namesByAddressAdmin
*/
export const fixHub = async namesByAddressAdmin => {
/** @type {import('@agoric/vats').NameHub} */
const hub = Far('Hub work-around', {
lookup: async (addr, ...rest) => {
await E(namesByAddressAdmin).reserve(addr);
const addressAdmin = await E(namesByAddressAdmin).lookupAdmin(addr);
assert(addressAdmin, 'no admin???');
const addressHub = E(addressAdmin).readonly();
if (rest.length === 0) return addressHub;
await E(addressAdmin).reserve(rest[0]);
return E(addressHub).lookup(...rest);
},
has: _key => Fail`key space not well defined`,
entries: () => Fail`enumeration not supported`,
values: () => Fail`enumeration not supported`,
keys: () => Fail`enumeration not supported`,
});
return hub;
};
Loading