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/mockBrand.js b/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js index 78efb5eeb574..570f6d8f5787 100644 --- a/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js +++ b/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js @@ -1,12 +1,20 @@ +// @ts-check import { Far } from '@endo/marshal'; +import { M } from '@agoric/store'; import { AssetKind } from '../../../src/index.js'; +const mockAuxData = harden({ + name: 'mock', + assetKind: AssetKind.NAT, + displayInfo: { assetKind: AssetKind.NAT }, + amountShape: M.any(), +}); + /** @type {Brand} */ export const mockBrand = Far('brand', { - // eslint-disable-next-line no-unused-vars - isMyIssuer: async allegedIssuer => false, - getAllegedName: () => 'mock', - getDisplayInfo: () => ({ - assetKind: AssetKind.NAT, - }), + getAllegedName: () => mockAuxData.name, + isMyIssuer: async _allegedIssuer => false, + getDisplayInfo: () => mockAuxData.displayInfo, + getAmountShape: () => mockAuxData.amountShape, + aux: () => mockAuxData, }); diff --git a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js index be09a36884fe..e90c5c104acc 100644 --- a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js +++ b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js @@ -11,6 +11,21 @@ import { mockBrand } from './mockBrand.js'; // AmountMath so that we can test that any duplication is handled // correctly. +const otherAuxData = harden({ + name: 'somename', + assetKind: AssetKind.NAT, + displayInfo: { assetKind: AssetKind.NAT }, + amountShape: M.any(), +}); + +const otherBrand = Far('otherBrand', { + getAllegedName: () => otherAuxData.name, + isMyIssuer: async _allegedIssuer => false, + getDisplayInfo: () => otherAuxData.displayInfo, + getAmountShape: () => otherAuxData.amountShape, + aux: () => otherAuxData, +}); + test('natMathHelpers make', t => { t.deepEqual(m.make(mockBrand, 4n), { brand: mockBrand, value: 4n }); // @ts-expect-error deliberate invalid arguments for testing @@ -63,12 +78,7 @@ test('natMathHelpers coerce', t => { m.coerce( mockBrand, harden({ - brand: Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), + brand: otherBrand, value: 4n, }), ), @@ -187,39 +197,14 @@ test('natMathHelpers isGTE', t => { }); test('natMathHelpers isGTE mixed brands', t => { - t.throws( - () => - m.isGTE( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 5n, - ), - m.make(mockBrand, 3n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.isGTE(m.make(otherBrand, 5n), m.make(mockBrand, 3n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers isGTE - brands don't match objective brand`, t => { t.throws( - () => - m.isGTE( - m.make(mockBrand, 5n), - m.make(mockBrand, 3n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.isGTE(m.make(mockBrand, 5n), m.make(mockBrand, 3n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, @@ -238,39 +223,14 @@ test('natMathHelpers isEqual', t => { }); test('natMathHelpers isEqual mixed brands', t => { - t.throws( - () => - m.isEqual( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 4n, - ), - m.make(mockBrand, 4n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.isEqual(m.make(otherBrand, 4n), m.make(mockBrand, 4n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers isEqual - brands don't match objective brand`, t => { t.throws( - () => - m.isEqual( - m.make(mockBrand, 4n), - m.make(mockBrand, 4n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.isEqual(m.make(mockBrand, 4n), m.make(mockBrand, 4n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, @@ -286,39 +246,14 @@ test('natMathHelpers add', t => { }); test('natMathHelpers add mixed brands', t => { - t.throws( - () => - m.add( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 5n, - ), - m.make(mockBrand, 9n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.add(m.make(otherBrand, 5n), m.make(mockBrand, 9n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers add - brands don't match objective brand`, t => { t.throws( - () => - m.add( - m.make(mockBrand, 5n), - m.make(mockBrand, 9n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.add(m.make(mockBrand, 5n), m.make(mockBrand, 9n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, @@ -334,39 +269,14 @@ test('natMathHelpers subtract', t => { }); test('natMathHelpers subtract mixed brands', t => { - t.throws( - () => - m.subtract( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 6n, - ), - m.make(mockBrand, 1n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.subtract(m.make(otherBrand, 6n), m.make(mockBrand, 1n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers subtract brands don't match brand`, t => { t.throws( - () => - m.subtract( - m.make(mockBrand, 6n), - m.make(mockBrand, 1n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.subtract(m.make(mockBrand, 6n), m.make(mockBrand, 1n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, 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 () => {