diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js b/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js index 0f441f86b7c..3930e533487 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js @@ -233,11 +233,11 @@ export function buildRootObject(vatPowers, vatParameters) { registerAPIHandler(handler) { return E(vats.http).registerURLHandler(handler, '/api'); }, - async registerWallet(wallet, handler, bridgeHandler) { + async registerWallet(wallet, privateWallet, privateWalletBridge) { await Promise.all([ - E(vats.http).registerURLHandler(handler, '/private/wallet'), + E(vats.http).registerURLHandler(privateWallet, '/private/wallet'), E(vats.http).registerURLHandler( - bridgeHandler, + privateWalletBridge, '/private/wallet-bridge', ), E(vats.http).setWallet(wallet), diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/captp.js b/packages/cosmic-swingset/lib/ag-solo/vats/captp.js index 18c5682e977..fc55585667d 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/captp.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/captp.js @@ -1,20 +1,45 @@ // Avoid importing the full captp bundle, which would carry // in its own makeHardener, etc. import { makeCapTP } from '@agoric/captp/lib/captp'; +import { E } from '@agoric/eventual-send'; +import { makePromiseKit } from '@agoric/promise-kit'; -export const getCapTPHandler = (send, getBootstrapObject) => { +export const getCapTPHandler = ( + send, + getAdminAndConnectionFacets, + fallback = undefined, +) => { const chans = new Map(); + const doCall = (obj, method, ...args) => { + if (obj) { + return E(obj) + [method](...args) + .catch(_ => {}); + } + return undefined; + }; const handler = harden({ - onOpen(_obj, meta) { + onOpen(obj, meta) { const { channelHandle, origin = 'unknown' } = meta || {}; console.debug(`Starting CapTP`, meta); const sendObj = o => { send(o, [channelHandle]); }; - const { dispatch, abort } = makeCapTP( + const adminFacetPK = makePromiseKit(); + const connectionFacetPK = makePromiseKit(); + let booted = false; + const { dispatch, abort, getBootstrap } = makeCapTP( origin, sendObj, - getBootstrapObject, + async () => { + if (!booted) { + booted = true; + const admconnP = getAdminAndConnectionFacets(meta, getBootstrap()); + adminFacetPK.resolve(E.G(admconnP).adminFacet); + connectionFacetPK.resolve(E.G(admconnP).connectionFacet); + } + return connectionFacetPK.promise; + }, { onReject(err) { // Be quieter for sanity's sake. @@ -22,33 +47,50 @@ export const getCapTPHandler = (send, getBootstrapObject) => { }, }, ); - chans.set(channelHandle, [dispatch, abort]); + chans.set(channelHandle, { + dispatch, + abort, + adminFacet: adminFacetPK.promise, + }); + doCall(fallback, 'onOpen', obj, meta); }, - onClose(_obj, meta) { + onClose(obj, meta) { console.debug(`Finishing CapTP`, meta); - const dispatchAbort = chans.get(meta.channelHandle); - if (dispatchAbort) { - (1, dispatchAbort[1])(); + const chan = chans.get(meta.channelHandle); + if (chan) { + const { abort, adminFacet } = chan; + doCall(adminFacet, 'onClose'); + abort(); } chans.delete(meta.channelHandle); + doCall(fallback, 'onClose', obj, meta); }, onError(obj, meta) { console.debug(`Error in CapTP`, meta, obj.error); + const chan = chans.get(meta.channelHandle); + if (chan) { + const { abort, adminFacet } = chan; + doCall(adminFacet, 'onError', obj.error); + abort(obj.error); + } + doCall(fallback, 'onError', obj, meta); }, - onMessage(obj, meta) { + async onMessage(obj, meta) { console.debug('processing inbound', obj); - const dispatchAbort = chans.get(meta.channelHandle); - if (!dispatchAbort || !(1, dispatchAbort[0])(obj)) { + const chan = chans.get(meta.channelHandle); + if (chan) { + const { dispatch } = chan; + if (dispatch(obj)) { + return true; + } + } + const done = await doCall(fallback, 'onMessage', obj, meta); + if (!done) { console.error(`Could not find CapTP handler ${obj.type}`, meta); - return false; } - return true; + return done; }, }); - return harden({ - getCommandHandler() { - return handler; - }, - }); + return harden(handler); }; diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js b/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js index 91751002e68..89248fcf369 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js @@ -45,12 +45,23 @@ export function buildRootObject(vatPowers) { } }; - const handler = {}; const registeredURLHandlers = new Map(); - // TODO: Don't leak memory. - async function registerURLHandler(newHandler, url) { - const commandHandler = await E(newHandler).getCommandHandler(); + async function registerURLHandler(handler, url) { + const fallback = await E(handler) + .getCommandHandler() + .catch(_ => undefined); + const commandHandler = getCapTPHandler( + send, + (meta, otherSide) => + E(handler) + .open(meta, otherSide) + .catch(e => { + console.error('Loading CapTP', e); + return {}; + }), + fallback, + ); let reg = registeredURLHandlers.get(url); if (!reg) { reg = []; @@ -67,12 +78,12 @@ export function buildRootObject(vatPowers) { registerURLHandler(replHandler, '/private/repl'); // Assign the captp handler. - // TODO: Break this out into a separate vat. - const captpHandler = getCapTPHandler( - send, - // Harden only our exported objects, and fetch them afresh each time. - () => harden(exportedToCapTP), - ); + const captpHandler = harden({ + open() { + // Harden only our exported objects, and fetch them afresh each time. + return harden({ connectionFacet: exportedToCapTP }); + }, + }); registerURLHandler(captpHandler, '/private/captp'); }, @@ -164,19 +175,6 @@ export function buildRootObject(vatPowers) { }; delete meta.channelID; - if (url === '/private/repl') { - // Use our local handler object (compatibility). - // TODO: standardise - if (handler[type]) { - D(commandDevice).sendResponse( - count, - false, - await handler[type](obj, meta), - ); - return; - } - } - const urlHandlers = registeredURLHandlers.get(url); if (urlHandlers) { // todo fixme avoid the loop diff --git a/packages/cosmic-swingset/lib/ag-solo/web.js b/packages/cosmic-swingset/lib/ag-solo/web.js index 2dd2946c967..2c9a5547b3a 100644 --- a/packages/cosmic-swingset/lib/ag-solo/web.js +++ b/packages/cosmic-swingset/lib/ag-solo/web.js @@ -196,6 +196,28 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) { server.listen(port, host, () => log.info('Listening on', `${host}:${port}`)); + const wsActions = { + noop() { + // do nothing. + }, + heartbeat() { + this.isAlive = true; + }, + }; + + const pingInterval = setInterval(function ping() { + wss.clients.forEach(ws => { + if (!ws.isAlive) { + ws.terminate(); + return; + } + ws.isAlive = false; + ws.ping(wsActions.noop); + }); + }, 30000); + + wss.on('close', () => clearInterval(pingInterval)); + let lastChannelID = 0; function newChannel(ws, req) { @@ -206,6 +228,10 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) { log(id, `new WebSocket ${req.url}`); + // Manage connection pings. + ws.isAlive = true; + ws.on('pong', wsActions.heartbeat); + // Register the point-to-point channel. channels.set(channelID, ws); diff --git a/packages/dapp-svelte-wallet/api/src/wallet.js b/packages/dapp-svelte-wallet/api/src/wallet.js index b6839d00ade..0f6f611b7e6 100644 --- a/packages/dapp-svelte-wallet/api/src/wallet.js +++ b/packages/dapp-svelte-wallet/api/src/wallet.js @@ -208,6 +208,77 @@ export function buildRootObject(_vatPowers) { function getBridgeURLHandler() { return harden({ + // Use CapTP to interact with this object. + async open(meta, otherSide) { + const dappOrigin = meta.origin; + const suggestedDappPetname = meta.dappOrigin || meta.origin; + + const notYetEnabled = () => + E(otherSide) + .needDappApproval(dappOrigin, suggestedDappPetname) + .catch(_ => {}); + const approve = () => + wallet.waitForDappApproval( + suggestedDappPetname, + dappOrigin, + notYetEnabled, + ); + + return harden({ + async getPurseNotifier() { + await approve(); + return harden({ + async getUpdateSince(count = undefined) { + await approve(); + const pursesJSON = await pursesJSONNotifier.getUpdateSince( + count, + ); + return JSON.parse(pursesJSON); + }, + }); + }, + async addOffer(offer) { + await approve(); + return wallet.addOffer(offer, { ...meta, dappOrigin }); + }, + async getOfferNotifier(status = null) { + await approve(); + return harden({ + async getUpdateSince(count = undefined) { + await approve(); + const update = await inboxJSONNotifier.getUpdateSince(count); + const offers = JSON.parse(update.value); + return harden( + offers.filter( + offer => + (status === null || offer.status === status) && + offer.requestContext && + offer.requestContext.origin === dappOrigin, + ), + ); + }, + }); + }, + async getDepositFacetId(brandBoardId) { + await approve(); + return wallet.getDepositFacetId(brandBoardId); + }, + async suggestIssuer(petname, boardId) { + await approve(); + return wallet.suggestIssuer(petname, boardId, dappOrigin); + }, + async suggestInstallation(petname, boardId) { + await approve(); + return wallet.suggestInstallation(petname, boardId, dappOrigin); + }, + async suggestInstance(petname, boardId) { + await approve(); + return wallet.suggestInstance(petname, boardId, dappOrigin); + }, + }); + }, + + // The legacy HTTP/WebSocket handler. getCommandHandler() { return harden({ onOpen(_obj, meta) {