diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index 64522ba0a79d..cdb01fc9ceae 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -103,6 +103,9 @@ export const vivifyPaymentLedger = ( getAmountShape() { return amountShape; }, + aux() { + return brandAuxData; + }, }); const emptyAmount = AmountMath.makeEmpty(brand, assetKind); @@ -112,6 +115,13 @@ export const vivifyPaymentLedger = ( elementShape, ); + const brandAuxData = harden({ + name, + assetKind, + displayInfo, + amountShape, + }); + const { IssuerI, MintI, PaymentI, PurseIKit } = makeIssuerInterfaces( brand, assetKind, diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index 59debca89c7a..ba4acda0d113 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -140,11 +140,19 @@ export const DisplayInfoShape = M.partial( // //////////////////////// Interfaces ///////////////////////////////////////// +export const BrandAuxDataShape = harden({ + name: M.string(), + assetKind: AssetKindShape, + displayInfo: DisplayInfoShape, + amountShape: M.pattern(), +}); + export const BrandI = M.interface('Brand', { isMyIssuer: M.callWhen(M.await(IssuerShape)).returns(M.boolean()), getAllegedName: M.call().returns(M.string()), getDisplayInfo: M.call().returns(DisplayInfoShape), getAmountShape: M.call().returns(M.pattern()), + aux: M.call().returns(BrandAuxDataShape), }); /** diff --git a/packages/ERTP/src/types.js b/packages/ERTP/src/types.js index 97fb2ee7b317..819f387064fe 100644 --- a/packages/ERTP/src/types.js +++ b/packages/ERTP/src/types.js @@ -94,6 +94,14 @@ * AssetKind.SET or AssetKind.COPY_SET (non-fungible) */ +/** + * @typedef {object} BrandAuxData + * @property {string} name + * @property {AssetKind} assetKind + * @property {DisplayInfo} displayInfo + * @property {Pattern} amountShape + */ + /** * @template {AssetKind} [K=AssetKind] * @typedef {object} Brand @@ -113,6 +121,7 @@ * @property {() => DisplayInfo} getDisplayInfo * Give information to UI on how to display the amount. * @property {() => Pattern} getAmountShape + * @property {() => BrandAuxData} aux */ // /////////////////////////// Issuer ////////////////////////////////////////// diff --git a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js index be09a36884fe..56c024464500 100644 --- a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js +++ b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js @@ -68,6 +68,13 @@ test('natMathHelpers coerce', t => { isMyIssuer: async () => false, getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), getAmountShape: () => M.any(), + aux: () => + harden({ + name: 'somename', + assetKind: AssetKind.NAT, + displayInfo: { assetKind: AssetKind.NAT }, + amountShape: M.any(), + }), }), value: 4n, }), diff --git a/packages/SwingSet/src/lib/capdata.js b/packages/SwingSet/src/lib/capdata.js index f3f256b1231e..4d292f4b8855 100644 --- a/packages/SwingSet/src/lib/capdata.js +++ b/packages/SwingSet/src/lib/capdata.js @@ -1,4 +1,8 @@ import { assert, details as X } from '@agoric/assert'; +import { E } from '@endo/eventual-send'; +import { Remotable } from '@endo/marshal'; + +const { defineProperty } = Object; /* eslint-disable jsdoc/require-returns-check */ /** @@ -43,3 +47,52 @@ export function extractSingleSlot(data) { } return null; } + +// TODO Move someplace more reusable; perhaps even @endo/marshal +export const makeRemotePresence = (iface, fulfilledHandler) => { + let remotePresence; + const p = new HandledPromise((_res, _rej, resolveWithPresence) => { + remotePresence = resolveWithPresence(fulfilledHandler); + let auxData; + let hasAuxData = false; + + /** + * A remote presence has an `aux()` method that either returns a promise + * or a non-promise. The first time it is called, it does an eventual-send + * of an `aux()` message to the far object it designates, remembers that + * promise as its auxdata, and returns that. It also watches that promise + * to react to its fulfillment. Until that reaction, every time its + * `aux()` is called, it will return that same promise. Once it reacts + * to the promise being fulfilled, if that ever happens, then the + * fulfillment becomes its new auxdata which gets returned from then on. + * + * @template T + * @returns {ERef} + */ + const aux = () => { + if (hasAuxData) { + return auxData; + } + auxData = E(remotePresence).aux(); + hasAuxData = true; + // TODO Also watch for a rejection. If rejected with the special + // value indicating the promise was broken by upgrade, then ask again. + // To keep the return promise valid across that upgrade-polling + // requires more bookkeeping, to keep the returned promise distinct + // from the promise for the result of the send. + E.when(auxData, value => (auxData = value)); + return auxData; + }; + defineProperty(remotePresence, 'aux', { + value: aux, + writable: false, + enumerable: false, + configurable: false, + }); + // Use Remotable rather than Far to make a remote from a presence + Remotable(iface, undefined, remotePresence); + }); + + harden(p); + return remotePresence; +}; diff --git a/packages/SwingSet/src/liveslots/liveslots.js b/packages/SwingSet/src/liveslots/liveslots.js index 22884071cd45..bdab0ff416ab 100644 --- a/packages/SwingSet/src/liveslots/liveslots.js +++ b/packages/SwingSet/src/liveslots/liveslots.js @@ -12,7 +12,7 @@ import { makeVatSlot, parseVatSlot, } from '../lib/parseVatSlots.js'; -import { insistCapData } from '../lib/capdata.js'; +import { insistCapData, makeRemotePresence } from '../lib/capdata.js'; import { extractMethod, legibilizeMethod } from '../lib/kdebug.js'; import { insistMessage } from '../lib/message.js'; import { makeVirtualReferenceManager } from './virtualReferences.js'; @@ -377,22 +377,17 @@ function build( }, }; - let remotePresence; - const p = new HandledPromise((_res, _rej, resolveWithPresence) => { - // Use Remotable rather than Far to make a remote from a presence - remotePresence = Remotable( - iface, - undefined, - resolveWithPresence(fulfilledHandler), - ); - // remote === presence, actually + const remotePresence = makeRemotePresence(iface, fulfilledHandler); + + // remote === presence, actually - // todo: mfig says resolveWithPresence - // gives us a Presence, Remotable gives us a Remote. I think that - // implies we have a lot of renaming to do, 'makeRemote' instead of - // 'makeImportedPresence', etc. I'd like to defer that for a later - // cleanup/renaming pass. - }); // no unfulfilledHandler + // todo: mfig says resolveWithPresence + // gives us a Presence, Remotable gives us a Remote. I think that + // implies we have a lot of renaming to do, 'makeRemote' instead of + // 'makeImportedPresence', etc. I'd like to defer that for a later + // cleanup/renaming pass. + + // no unfulfilledHandler // The call to resolveWithPresence performs the forwarding logic // immediately, so by the time we reach here, E(presence).foo() will use @@ -404,8 +399,6 @@ function build( // `HandledPromise.resolve(presence)`. So we must harden it now, for // safety, to prevent it from being used as a communication channel // between isolated objects that share a reference to the Presence. - harden(p); - // Up at the application level, presence~.foo(args) starts by doing // HandledPromise.resolve(presence), which retrieves it, and then does // p.eventualSend('foo', [args]), which uses the fulfilledHandler. diff --git a/packages/zoe/src/issuerStorage.js b/packages/zoe/src/issuerStorage.js index 2771406ae3e4..e20b347dc777 100644 --- a/packages/zoe/src/issuerStorage.js +++ b/packages/zoe/src/issuerStorage.js @@ -89,15 +89,16 @@ export const provideIssuerStorage = zcfBaggage => { assertInstantiated(); const brandP = E(issuerP).getBrand(); const brandIssuerMatchP = E(brandP).isMyIssuer(issuerP); - const displayInfoP = E(brandP).getDisplayInfo(); - /** @type {[Issuer,Brand,boolean,DisplayInfo]} */ - const [issuer, brand, brandIssuerMatch, displayInfo] = await Promise.all([ + const brandAuxDataP = E(brandP).aux(); + /** @type {[Issuer,Brand,boolean,BrandAuxData]} */ + const [issuer, brand, brandIssuerMatch, brandAuxData] = await Promise.all([ issuerP, brandP, brandIssuerMatchP, - displayInfoP, + brandAuxDataP, ]); // AWAIT ///// + const { displayInfo } = brandAuxData; if (issuerToIssuerRecord.has(issuer)) { return issuerToIssuerRecord.get(issuer); diff --git a/packages/zoe/test/unitTests/test-zoe.js b/packages/zoe/test/unitTests/test-zoe.js index 8c490c0b545e..5b7719d113ae 100644 --- a/packages/zoe/test/unitTests/test-zoe.js +++ b/packages/zoe/test/unitTests/test-zoe.js @@ -9,6 +9,7 @@ import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; import { passStyleOf, Far } from '@endo/marshal'; import { getMethodNames } from '@agoric/internal'; +import { M } from '@agoric/store'; // eslint-disable-next-line import/no-extraneous-dependencies import bundleSource from '@endo/bundle-source'; @@ -173,6 +174,13 @@ test(`E(zoe).startInstance - bad issuer, makeEmptyPurse throws`, async t => { // eslint-disable-next-line no-use-before-define isMyIssuer: i => i === badIssuer, getDisplayInfo: () => ({ decimalPlaces: 6, assetKind: AssetKind.NAT }), + aux: () => + harden({ + name: 'bogusBrand', + assetKind: AssetKind.NAT, + displayInfo: { decimalPlaces: 6, assetKind: AssetKind.NAT }, + amountShape: M.any(), + }), }); const badIssuer = Far('issuer', { makeEmptyPurse: async () => { diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index 7ffc08cae9ba..7ee081da6c3d 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -6,8 +6,9 @@ import { Far } from '@endo/marshal'; import { AssetKind, AmountMath } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { getMethodNames } from '@agoric/internal'; -import { makeOffer } from '../makeOffer.js'; +import { M } from '@agoric/store'; +import { makeOffer } from '../makeOffer.js'; import { setup } from '../setupBasicMints.js'; import buildManualTimer from '../../../tools/manualTimer.js'; @@ -205,6 +206,13 @@ test(`zcf.saveIssuer - bad issuer, makeEmptyPurse throws`, async t => { // eslint-disable-next-line no-use-before-define isMyIssuer: i => i === badIssuer, getDisplayInfo: () => ({ decimalPlaces: 6, assetKind: AssetKind.NAT }), + aux: () => + harden({ + name: 'bogusBrand', + assetKind: AssetKind.NAT, + displayInfo: { decimalPlaces: 6, assetKind: AssetKind.NAT }, + amountShape: M.any(), + }), }); const badIssuer = Far('issuer', { makeEmptyPurse: async () => {