Skip to content

Commit

Permalink
feat(contracts): upload to contractHost
Browse files Browse the repository at this point in the history
ag-solo upload-contract can be used separately, but by
default all contracts in BASEDIR/contracts/contract-* are
bundled and uploaded to the newly-created contractHost.
  • Loading branch information
michaelfig committed Oct 12, 2019
1 parent c8337f9 commit f9e7b5e
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 23 deletions.
55 changes: 55 additions & 0 deletions lib/ag-solo/build-source-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { rollup } from 'rollup';
import path from 'path';
import resolve from 'rollup-plugin-node-resolve';
import acornEventualSend from '@agoric/acorn-eventual-send';

const DEFAULT_MODULE_FORMAT = 'getExport';

export default async function bundleSource(
startFilename,
moduleFormat = DEFAULT_MODULE_FORMAT,
) {
const resolvedPath = path.resolve(startFilename);
const bundle = await rollup({
input: resolvedPath,
treeshake: false,
external: ['@agoric/evaluate', '@agoric/nat', '@agoric/harden'],
plugins: [resolve()],
acornInjectPlugins: [acornEventualSend()],
});
const { output } = await bundle.generate({
exports: 'named',
format: moduleFormat === 'getExport' ? 'cjs' : moduleFormat,
});
if (output.length !== 1) {
throw Error('unprepared for more than one chunk/asset');
}
if (output[0].isAsset) {
throw Error(`unprepared for assets: ${output[0].fileName}`);
}
let { code: source } = output[0];

// 'source' is now a string that contains a program, which references
// require() and sets module.exports . This is close, but we need a single
// stringifiable function, so we must wrap it in an outer function that
// returns the exports.
//
// build-kernel.js will prefix this with 'export default' so it becomes an
// ES6 module. The Vat controller will wrap it with parenthesis so it can
// be evaluated and invoked to get at the exports.

const sourceMap = `//# sourceURL=${resolvedPath}\n`;
if (moduleFormat === 'getExport')
source = `\
function getExport() { \
let exports = {}; \
const module = { exports }; \
\
${source}
return module.exports;
}
`;

return { source, sourceMap, moduleFormat };
}
14 changes: 14 additions & 0 deletions lib/ag-solo/contracts/contract-2n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default harden((_terms, inviteMaker) => {
const result = makePromise();
const seat = harden({
provide(n) {
result.res(2 * n);
},
result() {
return result.p;
},
});
return harden({
seat: inviteMaker.make('multiplicand', seat),
});
});
10 changes: 10 additions & 0 deletions lib/ag-solo/contracts/contract-encouragementBot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { encourage } from './encourage';

export default harden((_terms, _inviteMaker) => {
return harden({
encourageMe(name) {
// log(`=> encouragementBot.encourageMe got the name: ${name}`);
return encourage(name);
},
});
});
1 change: 1 addition & 0 deletions lib/ag-solo/contracts/encourage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const encourage = name => `${name}, you are awesome, keep it up!`;
12 changes: 12 additions & 0 deletions lib/ag-solo/init-basedir.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ export default function initBasedir(basedir, webport, webhost, subdir, egresses)
);
});

const source_contractsdir = path.join(here, 'contracts');
const dest_contractsdir = path.join(basedir, 'contracts');
fs.mkdirSync(dest_contractsdir);
fs.readdirSync(source_contractsdir)
.filter(name => name.match(/^[^.]/))
.forEach(name => {
fs.copyFileSync(
path.join(source_contractsdir, name),
path.join(dest_contractsdir, name),
);
});

// Save our version codes.
const pj = 'package.json';
fs.copyFileSync(path.join(`${here}/../..`, pj), path.join(dest_htmldir, pj));
Expand Down
4 changes: 4 additions & 0 deletions lib/ag-solo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { insist } from './insist';
import initBasedir from './init-basedir';
import setGCIIngress from './set-gci-ingress';
import start from './start';
import uploadContract from './upload-contract';

// As we add more egress types, put the default types in a comma-separated
// string below.
Expand Down Expand Up @@ -76,6 +77,9 @@ start
const basedir = insistIsBasedir();
const withSES = true;
await start(basedir, withSES, argv.slice(1));
} else if (argv[0] === 'upload-contract') {
const basedir = insistIsBasedir();
await uploadContract(basedir, argv.slice(1));
} else {
console.log(`unrecognized command ${argv[0]}`);
console.log(`try one of: init, set-gci-ingress, start`);
Expand Down
17 changes: 17 additions & 0 deletions lib/ag-solo/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { buildStorageInMemory } from '@agoric/swingset-vat/src/hostStorage';
import buildCommand from '@agoric/swingset-vat/src/devices/command';

import uploadContract from './upload-contract';
import { deliver, addDeliveryTarget } from './outbound';
import { makeHTTPListener } from './web';

Expand Down Expand Up @@ -188,4 +189,20 @@ export default async function start(basedir, withSES, argv) {
}

console.log(`swingset running`);

// Install the contracts.
const contractsDir = path.join(basedir, 'contracts');
const pairs = (await fs.promises.readdir(contractsDir)).reduce(
(prior, name) => {
const match = name.match(/^contract-([^.]+)/);
if (match) {
prior.push(`${match[1]}=${contractsDir}/${name}`);
}
return prior;
},
[],
)
if (pairs.length > 0) {
await uploadContract(basedir, pairs);
}
}
119 changes: 119 additions & 0 deletions lib/ag-solo/upload-contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable no-await-in-loop */
import parseArgs from 'minimist';
import WebSocket from 'ws';
import { E } from '@agoric/eventual-send';
import makeCapTP from '@agoric/captp';
import fs from 'fs';
import path from 'path';

import buildSourceBundle from './build-source-bundle';

const makePromise = () => {
const pr = {};
pr.p = new Promise((resolve, reject) => {
pr.res = resolve;
pr.rej = reject;
});
return pr;
};

const sendJSON = (ws, obj) => {
if (ws.readyState !== ws.OPEN) {
return;
}
// console.log('sending', obj);
ws.send(JSON.stringify(obj));
};

export default async function upload(basedir, args) {
const { _: namePaths, 'ag-solo': agSolo } = parseArgs(args, {
stopEarly: true,
});
if (namePaths.length === 0) {
console.error('You must specify NAME=PATH pairs to upload');
return 1;
}

let wsurl = agSolo;
if (!agSolo) {
const cjson = await fs.promises.readFile(
path.join(basedir, 'connections.json'),
);
for (const conn of JSON.parse(cjson)) {
if (conn.type === 'http') {
wsurl = `ws://${conn.host}:${conn.port}/captp`;
}
}
}

const ws = new WebSocket(wsurl);
const exit = makePromise();
ws.on('open', async () => {
try {
const [handler, bootstrap] = makeCapTP('upload', obj =>
sendJSON(ws, obj),
);
ws.on('message', data => {
// console.log(data);
try {
const obj = JSON.parse(data);
if (obj.type === 'CTP_ERROR') {
throw obj.error;
}
handler[obj.type](obj);
} catch (e) {
console.log('server error processing message', data, e);
exit.rej(e);
}
});

let bootC;
for (;;) {
bootC = E.C(bootstrap());
if (!(await bootC.G.LOADING.P)) {
break;
}
console.log(`waiting for chain to become ready`);
await new Promise(resolve => setTimeout(resolve, 3000));
}

const ids = await Promise.all(
namePaths.map(async namePath => {
const match = namePath.match(/^([^=]+)=(.+)$/);
if (!match) {
throw Error(`${namePath} isn't NAME=PATH`);
}
const name = match[1];
const path = match[2];
const { source, moduleFormat } = await buildSourceBundle(path);
// console.log(`Uploading ${source}`);

return bootC.G.contractHost.M.install(source, moduleFormat).P.then(
res => bootC.G.registry.M.set(name, res).P,
);
}),
);

console.log('Success!');
setTimeout(
() =>
console.log(`\
To create a contract instance, use:
home.registry~.get(ID)~.spawn(TERMS)
where ID is the registered installation id, one of:
${ids.join('\n ')}`),
1000,
);
ws.close();
exit.res(0);
} catch (e) {
exit.rej(e);
}
});
ws.on('close', (_code, _reason) => {
console.log('connection closed');
exit.res(1);
});
return exit.p;
}
2 changes: 2 additions & 0 deletions lib/ag-solo/vats/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ export default function setup(syscall, state, helpers) {
return harden({
async createDemoBundle(nickname) {
const handoff = await E(vats.handoff).getSharedHandoffService();
const registry = await E(vats.registry).getSharedRegistry();
const pixelBundle = await E(vats.pixel).createPixelBundle(nickname);
return harden({
...pixelBundle,
handoff,
contractHost,
registry,
});
},
});
Expand Down
59 changes: 43 additions & 16 deletions lib/ag-solo/vats/vat-http.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import harden from '@agoric/harden';
import makeCapTP from '@agoric/captp';

import { getReplHandler } from './repl';
// import { getCapTP } from './captp';

// This vat contains the HTTP request handler.

function build(E, D) {
let commandDevice;
let provisioner;
Expand All @@ -28,25 +27,53 @@ function build(E, D) {
},
};
if (ROLES.client) {
const conns = new Map();
const forward = method => obj => {
const handlers = conns.get(obj.connectionID);
if (!handlers || !handlers[method]) {
console.log(
`Could not find CapTP handler ${method}`,
obj.connectionID,
);
return undefined;
}
return handlers[method](obj);
};
Object.assign(
handler,
getReplHandler(E, homeObjects, msg =>
D(commandDevice).sendBroadcast(msg),
),
// getCapTP(homeObjects),
{
CTP_OPEN(obj) {
console.log(`Starting CapTP`, obj.connectionID);
const sendObj = o => {
o.connectionID = obj.connectionID;
D(commandDevice).sendBroadcast(o);
};
const [handlers] = makeCapTP(obj.connectionID, sendObj, () =>
harden({
contractHost: homeObjects.contractHost,
handoffService: homeObjects.handoffService,
registry: homeObjects.registry,
LOADING: homeObjects.LOADING,
}),
);
conns.set(obj.connectionID, handlers);
},
CTP_CLOSE(obj) {
console.log(`Finishing CapTP`, obj.connectionID);
conns.delete(obj.connectionID);
},
CTP_ERROR(obj) {
console.log(`Error in CapTP`, obj.connectionID, obj.error);
},
CTP_BOOTSTRAP: forward('CTP_BOOTSTRAP'),
CTP_CALL: forward('CTP_CALL'),
CTP_RETURN: forward('CTP_RETURN'),
CTP_RESOLVE: forward('CTP_RESOLVE'),
},
);
handler.uploadContract = async obj => {
const { connectionID, source, moduleType } = obj;
if (!homeObjects.contractHost) {
throw Error('home.contractHost is not available');
}
console.log('would upload', moduleType);
D(commandDevice).sendBroadcast({
type: 'contractUploaded',
connectionID,
code: 99,
});
};
}
if (ROLES.controller) {
handler.pleaseProvision = obj => {
Expand Down Expand Up @@ -94,7 +121,7 @@ function build(E, D) {
try {
console.log(`vat-http.inbound (from browser) ${count}`, obj);
const res = await handler[obj.type](obj);
D(commandDevice).sendResponse(count, false, res);
D(commandDevice).sendResponse(count, false, harden(res));
} catch (rej) {
D(commandDevice).sendResponse(count, true, harden(rej));
}
Expand Down
Loading

0 comments on commit f9e7b5e

Please sign in to comment.