From 163be943d90c7e0eaf82650fbe274fbc6c60802d Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Sun, 17 Apr 2022 22:41:02 -0700 Subject: [PATCH 1/2] fix: interface guards --- packages/ERTP/src/interfaces.js | 45 --- packages/ERTP/src/payment.js | 23 +- packages/ERTP/src/paymentLedger.js | 303 ++++++++------- packages/ERTP/src/purse.js | 69 ++-- packages/ERTP/src/typeGuards.js | 102 +++++ packages/ERTP/src/types.js | 13 +- .../test/unitTests/test-inputValidation.js | 17 +- .../ERTP/test/unitTests/test-issuerObj.js | 10 +- packages/ERTP/test/unitTests/test-mintObj.js | 2 +- .../src/liveslots/virtualObjectManager.js | 78 ++-- .../test/unitTests/test-paramGovernance.js | 5 +- packages/store/src/index.js | 1 + .../store/src/patterns/interface-tools.js | 236 ++++++++++++ .../store/src/patterns/patternMatchers.js | 351 +++++++++++------- packages/store/src/types.js | 22 +- packages/store/test/test-patterns.js | 72 ++++ packages/vat-data/src/far-class-utils.js | 217 +++++++++++ packages/vat-data/src/index.js | 10 + packages/vat-data/src/kind-utils.js | 33 +- packages/vat-data/src/types.d.ts | 2 + .../test/unitTests/contracts/loan/helpers.js | 22 +- 21 files changed, 1148 insertions(+), 485 deletions(-) delete mode 100644 packages/ERTP/src/interfaces.js create mode 100644 packages/store/src/patterns/interface-tools.js create mode 100644 packages/vat-data/src/far-class-utils.js diff --git a/packages/ERTP/src/interfaces.js b/packages/ERTP/src/interfaces.js deleted file mode 100644 index 35ebaf68ca7..00000000000 --- a/packages/ERTP/src/interfaces.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-use-before-define */ -/* global foo */ -const M = foo(); - -export const MatchDisplayInfo = M.rest( - { - assetKind: M.or('nat', 'set'), - }.optionals({ - decimalPlaces: M.number(), - }), -); - -export const BrandI = M.interface({ - isMyIssuer: M.callWhen(M.await(IssuerI)).returns(M.boolean()), - getAllegedName: M.call().returns(M.string()), - getDisplayInfo: M.call().returns(MatchDisplayInfo), -}); - -export const MatchAmount = { - brand: BrandI, - value: M.or(M.bigint(), M.array()), -}; - -export const IssuerI = M.interface({ - getBrand: M.call().returns(BrandI), - getAllegedName: M.call().returns(M.string()), - getAssetKind: M.call().returns(M.or('nat', 'set')), - getDisplayInfo: M.call().returns(MatchDisplayInfo), - makeEmptyPurse: M.call().returns(PurseI), - - isLive: M.callWhen(M.await(PaymentI)).returns(M.boolean()), - getAmountOf: M.callWhen(M.await(PaymentI)).returns(MatchAmount), -}); - -export const PaymentI = M.interface({ - getAllegedBrand: M.call().returns(BrandI), -}); - -export const PurseI = M.interface({ - getAllegedBrand: M.call().returns(BrandI), - deposit: M.apply(M.rest([PaymentI]).optionals([MatchAmount])).returns( - MatchAmount, - ), - withdraw: M.call(MatchAmount).returns(PaymentI), -}); diff --git a/packages/ERTP/src/payment.js b/packages/ERTP/src/payment.js index d92b7d108df..db3b9c6fbc7 100644 --- a/packages/ERTP/src/payment.js +++ b/packages/ERTP/src/payment.js @@ -1,6 +1,6 @@ // @ts-check -import { defineDurableKind, provideKindHandle } from '@agoric/vat-data'; +import { vivifyFarClass } from '@agoric/vat-data'; /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ @@ -8,15 +8,22 @@ import { defineDurableKind, provideKindHandle } from '@agoric/vat-data'; * @template {AssetKind} K * @param {Baggage} issuerBaggage * @param {string} name - * @param {() => Brand} getBrand must not be called before the issuerKit is - * created + * @param {Brand} brand + * @param {InterfaceGuard} PaymentI * @returns {() => Payment} */ -export const vivifyPaymentKind = (issuerBaggage, name, getBrand) => { - const paymentKindHandle = provideKindHandle(issuerBaggage, `${name} payment`); - const makePayment = defineDurableKind(paymentKindHandle, () => ({}), { - getAllegedBrand: getBrand, - }); +export const vivifyPaymentKind = (issuerBaggage, name, brand, PaymentI) => { + const makePayment = vivifyFarClass( + issuerBaggage, + `${name} payment`, + PaymentI, + () => ({}), + { + getAllegedBrand() { + return brand; + }, + }, + ); return makePayment; }; harden(vivifyPaymentKind); diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index 866e431c6cd..b161a584702 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -1,16 +1,19 @@ /* eslint-disable no-use-before-define */ // @ts-check -import { E } from '@endo/eventual-send'; import { isPromise } from '@endo/promise-kit'; import { assertCopyArray } from '@endo/marshal'; import { fit, M } from '@agoric/store'; -import { vivifySingleton, provideDurableWeakMapStore } from '@agoric/vat-data'; +import { + provideDurableWeakMapStore, + vivifyFarInstance, +} from '@agoric/vat-data'; import { AmountMath } from './amountMath.js'; import { vivifyPaymentKind } from './payment.js'; import { vivifyPurseKind } from './purse.js'; import '@agoric/store/exported.js'; +import { BrandI, makeIssuerInterfaces } from './typeGuards.js'; /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ @@ -70,11 +73,11 @@ const amountShapeFromElementShape = (brand, assetKind, elementShape) => { * @template {AssetKind} [K=AssetKind] * @param {Baggage} issuerBaggage * @param {string} name - * @param {AssetKind} assetKind + * @param {K} assetKind * @param {DisplayInfo} displayInfo * @param {Pattern} elementShape * @param {ShutdownWithFailure=} optShutdownWithFailure - * @returns {{ issuer: Issuer, mint: Mint, brand: Brand }} + * @returns {PaymentLedger} */ export const vivifyPaymentLedger = ( issuerBaggage, @@ -84,9 +87,38 @@ export const vivifyPaymentLedger = ( elementShape, optShutdownWithFailure = undefined, ) => { - const getBrand = () => brand; + /** @type {Brand} */ + const brand = vivifyFarInstance(issuerBaggage, `${name} brand`, BrandI, { + isMyIssuer(allegedIssuer) { + // BrandI delays calling this method until `allegedIssuer` is a Remotable + return allegedIssuer === issuer; + }, + getAllegedName() { + return name; + }, + // Give information to UI on how to display the amount. + getDisplayInfo() { + return displayInfo; + }, + getAmountShape() { + return amountShape; + }, + }); - const makePayment = vivifyPaymentKind(issuerBaggage, name, getBrand); + const emptyAmount = AmountMath.makeEmpty(brand, assetKind); + const amountShape = amountShapeFromElementShape( + brand, + assetKind, + elementShape, + ); + + const { IssuerI, MintI, PaymentI, PurseIKit } = makeIssuerInterfaces( + brand, + assetKind, + amountShape, + ); + + const makePayment = vivifyPaymentKind(issuerBaggage, name, brand, PaymentI); /** @type {ShutdownWithFailure} */ const shutdownLedgerWithFailure = reason => { @@ -268,116 +300,6 @@ export const vivifyPaymentLedger = ( return harden(newPayments); }; - /** @type {IssuerIsLive} */ - const isLive = paymentP => { - return E.when(paymentP, payment => { - return paymentLedger.has(payment); - }); - }; - - /** @type {IssuerGetAmountOf} */ - const getAmountOf = paymentP => { - return E.when(paymentP, payment => { - assertLivePayment(payment); - return paymentLedger.get(payment); - }); - }; - - /** @type {IssuerBurn} */ - const burn = (paymentP, optAmountShape = undefined) => { - return E.when(paymentP, payment => { - assertLivePayment(payment); - const paymentBalance = paymentLedger.get(payment); - assertAmountConsistent(paymentBalance, optAmountShape); - try { - // COMMIT POINT. - deletePayment(payment); - } catch (err) { - shutdownLedgerWithFailure(err); - throw err; - } - return paymentBalance; - }); - }; - - /** @type {IssuerClaim} */ - const claim = (paymentP, optAmountShape = undefined) => { - return E.when(paymentP, srcPayment => { - assertLivePayment(srcPayment); - const srcPaymentBalance = paymentLedger.get(srcPayment); - assertAmountConsistent(srcPaymentBalance, optAmountShape); - // Note COMMIT POINT within moveAssets. - const [payment] = moveAssets( - harden([srcPayment]), - harden([srcPaymentBalance]), - ); - return payment; - }); - }; - - /** @type {IssuerCombine} */ - const combine = (fromPaymentsPArray, optTotalAmount = undefined) => { - assertCopyArray(fromPaymentsPArray, 'fromPaymentsArray'); - // Payments in `fromPaymentsPArray` must be distinct. Alias - // checking is delegated to the `moveAssets` function. - return Promise.all(fromPaymentsPArray).then(fromPaymentsArray => { - fromPaymentsArray.every(assertLivePayment); - const totalPaymentsBalance = fromPaymentsArray - .map(paymentLedger.get) - .reduce(add, emptyAmount); - assertAmountConsistent(totalPaymentsBalance, optTotalAmount); - // Note COMMIT POINT within moveAssets. - const [payment] = moveAssets( - harden(fromPaymentsArray), - harden([totalPaymentsBalance]), - ); - return payment; - }); - }; - - /** @type {IssuerSplit} */ - // payment to two payments, A and B - const split = (paymentP, paymentAmountA) => { - return E.when(paymentP, srcPayment => { - paymentAmountA = coerce(paymentAmountA); - assertLivePayment(srcPayment); - const srcPaymentBalance = paymentLedger.get(srcPayment); - const paymentAmountB = subtract(srcPaymentBalance, paymentAmountA); - // Note COMMIT POINT within moveAssets. - const newPayments = moveAssets( - harden([srcPayment]), - harden([paymentAmountA, paymentAmountB]), - ); - return newPayments; - }); - }; - - /** @type {IssuerSplitMany} */ - const splitMany = (paymentP, amounts) => { - return E.when(paymentP, srcPayment => { - assertLivePayment(srcPayment); - assertCopyArray(amounts, 'amounts'); - amounts = amounts.map(coerce); - // Note COMMIT POINT within moveAssets. - const newPayments = moveAssets(harden([srcPayment]), harden(amounts)); - return newPayments; - }); - }; - - /** - * Creates a new Payment containing newly minted amount. - * - * @param {Amount} newAmount - * @returns {Payment} - */ - const mintPayment = newAmount => { - newAmount = coerce(newAmount); - fit(newAmount, amountShape, 'minted amount'); - const payment = makePayment(); - initPayment(payment, newAmount, undefined); - return payment; - }; - /** * Used by the purse code to implement purse.deposit * @@ -458,56 +380,127 @@ export const vivifyPaymentLedger = ( issuerBaggage, name, assetKind, - getBrand, + brand, + PurseIKit, harden({ depositInternal, withdrawInternal, }), ); - const issuer = vivifySingleton(issuerBaggage, `${name} issuer`, { - getBrand: () => brand, - getAllegedName: () => name, - getAssetKind: () => assetKind, - getDisplayInfo: () => displayInfo, - makeEmptyPurse, - - isLive, - getAmountOf, - burn, - claim, - combine, - split, - splitMany, - }); + /** @type {Issuer} */ + const issuer = vivifyFarInstance(issuerBaggage, `${name} issuer`, IssuerI, { + getBrand() { + return brand; + }, + getAllegedName() { + return name; + }, + getAssetKind() { + return assetKind; + }, + getDisplayInfo() { + return displayInfo; + }, + makeEmptyPurse() { + return makeEmptyPurse(); + }, + isLive(payment) { + // IssuerI delays calling this method until `payment` is a Remotable + return paymentLedger.has(payment); + }, + getAmountOf(payment) { + // IssuerI delays calling this method until `payment` is a Remotable + assertLivePayment(payment); + return paymentLedger.get(payment); + }, - const mint = vivifySingleton(issuerBaggage, `${name} mint`, { - getIssuer: () => issuer, - mintPayment, + burn(payment, optAmountShape = undefined) { + // IssuerI delays calling this method until `payment` is a Remotable + assertLivePayment(payment); + const paymentBalance = paymentLedger.get(payment); + assertAmountConsistent(paymentBalance, optAmountShape); + try { + // COMMIT POINT. + deletePayment(payment); + } catch (err) { + shutdownLedgerWithFailure(err); + throw err; + } + return paymentBalance; + }, + claim(srcPayment, optAmountShape = undefined) { + // IssuerI delays calling this method until `srcPayment` is a Remotable + assertLivePayment(srcPayment); + const srcPaymentBalance = paymentLedger.get(srcPayment); + assertAmountConsistent(srcPaymentBalance, optAmountShape); + // Note COMMIT POINT within moveAssets. + const [payment] = moveAssets( + harden([srcPayment]), + harden([srcPaymentBalance]), + ); + return payment; + }, + combine(fromPaymentsPArray, optTotalAmount = undefined) { + // IssuerI does *not* delay calling `combine`, but rather leaves it + // to `combine` to delay further processing until all the elements of + // `fromPaymentsPArray` have fulfilled. + + // Payments in `fromPaymentsPArray` must be distinct. Alias + // checking is delegated to the `moveAssets` function. + return Promise.all(fromPaymentsPArray).then(fromPaymentsArray => { + fromPaymentsArray.every(assertLivePayment); + const totalPaymentsBalance = fromPaymentsArray + .map(paymentLedger.get) + .reduce(add, emptyAmount); + assertAmountConsistent(totalPaymentsBalance, optTotalAmount); + // Note COMMIT POINT within moveAssets. + const [payment] = moveAssets( + harden(fromPaymentsArray), + harden([totalPaymentsBalance]), + ); + return payment; + }); + }, + split(srcPayment, paymentAmountA) { + // IssuerI delays calling this method until `srcPayment` is a Remotable + paymentAmountA = coerce(paymentAmountA); + assertLivePayment(srcPayment); + const srcPaymentBalance = paymentLedger.get(srcPayment); + const paymentAmountB = subtract(srcPaymentBalance, paymentAmountA); + // Note COMMIT POINT within moveAssets. + const newPayments = moveAssets( + harden([srcPayment]), + harden([paymentAmountA, paymentAmountB]), + ); + return newPayments; + }, + splitMany(srcPayment, amounts) { + // IssuerI delays calling this method until `srcPayment` is a Remotable + assertLivePayment(srcPayment); + assertCopyArray(amounts, 'amounts'); + amounts = amounts.map(coerce); + // Note COMMIT POINT within moveAssets. + const newPayments = moveAssets(harden([srcPayment]), harden(amounts)); + return newPayments; + }, }); - const brand = /** @type {Brand} */ ( - vivifySingleton(issuerBaggage, `${name} brand`, { - isMyIssuer: allegedIssuerP => - E.when(allegedIssuerP, allegedIssuer => allegedIssuer === issuer), - - getAllegedName: () => name, - // Give information to UI on how to display the amount. - getDisplayInfo: () => displayInfo, - getAmountShape: () => amountShape, - }) - ); + /** @type {Mint} */ + const mint = vivifyFarInstance(issuerBaggage, `${name} mint`, MintI, { + getIssuer() { + return issuer; + }, + mintPayment(newAmount) { + newAmount = coerce(newAmount); + fit(newAmount, amountShape, 'minted amount'); + const payment = makePayment(); + initPayment(payment, newAmount, undefined); + return payment; + }, + }); const issuerKit = harden({ issuer, mint, brand }); - - const emptyAmount = AmountMath.makeEmpty(brand, assetKind); - const amountShape = amountShapeFromElementShape( - brand, - assetKind, - elementShape, - ); return issuerKit; }; harden(vivifyPaymentLedger); - -/** @typedef {ReturnType} PaymentLedger */ diff --git a/packages/ERTP/src/purse.js b/packages/ERTP/src/purse.js index 288435a61da..bdacfe0f727 100644 --- a/packages/ERTP/src/purse.js +++ b/packages/ERTP/src/purse.js @@ -1,19 +1,15 @@ -import { - defineDurableKindMulti, - makeScalarBigSetStore, - provideKindHandle, -} from '@agoric/vat-data'; +import { vivifyFarClassKit, makeScalarBigSetStore } from '@agoric/vat-data'; import { AmountMath } from './amountMath.js'; import { makeTransientNotifierKit } from './transientNotifier.js'; const { details: X } = assert; -// `getBrand` must not be called before the issuerKit is created export const vivifyPurseKind = ( issuerBaggage, name, assetKind, - getBrand, + brand, + PurseIKit, purseMethods, ) => { // Note: Virtual for high cardinality, but *not* durable, and so @@ -32,11 +28,12 @@ export const vivifyPurseKind = ( // that created depositFacet as needed. But this approach ensures a constant // identity for the facet and exercises the multi-faceted object style. const { depositInternal, withdrawInternal } = purseMethods; - const purseKitKindHandle = provideKindHandle(issuerBaggage, `${name} Purse`); - const makePurseKit = defineDurableKindMulti( - purseKitKindHandle, + const makePurseKit = vivifyFarClassKit( + issuerBaggage, + `${name} Purse`, + PurseIKit, () => { - const currentBalance = AmountMath.makeEmpty(getBrand(), assetKind); + const currentBalance = AmountMath.makeEmpty(brand, assetKind); /** @type {SetStore} */ const recoverySet = makeScalarBigSetStore('recovery set', { @@ -50,40 +47,50 @@ export const vivifyPurseKind = ( }, { purse: { - deposit: ( - { state, facets: { purse } }, - srcPayment, - optAmountShape = undefined, - ) => { + deposit(srcPayment, optAmountShape = undefined) { + // PurseI does *not* delay `deposit` until `srcPayment` is fulfulled. + // See the comments on PurseI.deposit in typeGuards.js + const { state } = this; // Note COMMIT POINT within deposit. return depositInternal( state.currentBalance, newPurseBalance => - updatePurseBalance(state, newPurseBalance, purse), + updatePurseBalance(state, newPurseBalance, this.facets.purse), srcPayment, optAmountShape, state.recoverySet, ); }, - withdraw: ({ state, facets: { purse } }, amount) => + withdraw(amount) { + const { state } = this; // Note COMMIT POINT within withdraw. - withdrawInternal( + return withdrawInternal( state.currentBalance, newPurseBalance => - updatePurseBalance(state, newPurseBalance, purse), + updatePurseBalance(state, newPurseBalance, this.facets.purse), amount, state.recoverySet, - ), - getCurrentAmount: ({ state }) => state.currentBalance, - getCurrentAmountNotifier: ({ facets: { purse } }) => - provideNotifier(purse), - getAllegedBrand: _context => getBrand(), + ); + }, + getCurrentAmount() { + return this.state.currentBalance; + }, + getCurrentAmountNotifier() { + return provideNotifier(this.facets.purse); + }, + getAllegedBrand() { + return brand; + }, // eslint-disable-next-line no-use-before-define - getDepositFacet: ({ facets }) => facets.depositFacet, + getDepositFacet() { + return this.facets.depositFacet; + }, - getRecoverySet: ({ state }) => state.recoverySet.snapshot(), - recoverAll: ({ state, facets }) => { - const brand = getBrand(); + getRecoverySet() { + return this.state.recoverySet.snapshot(); + }, + recoverAll() { + const { state, facets } = this; let amount = AmountMath.makeEmpty(brand, assetKind); for (const payment of state.recoverySet.keys()) { // This does cause deletions from the set while iterating, @@ -99,7 +106,9 @@ export const vivifyPurseKind = ( }, }, depositFacet: { - receive: ({ facets }, ...args) => facets.purse.deposit(...args), + receive(...args) { + return this.facets.purse.deposit(...args); + }, }, }, ); diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index 4a850457939..6ee3f39498c 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -129,3 +129,105 @@ export const DisplayInfoShape = M.partial( // properties beyond those in the `base` record. }), ); + +// //////////////////////// Interfaces ///////////////////////////////////////// + +export const BrandShape = M.remotable('Brand'); +export const IssuerShape = M.remotable('Issuer'); +export const PaymentShape = M.remotable('Payment'); +export const PurseShape = M.remotable('Purse'); +export const DepositFacetShape = M.remotable('DepositFacet'); +const NotifierShape = M.remotable('Notifier'); +export const MintShape = M.remotable('Mint'); + +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()), +}); + +/** + * @param {Pattern} [brand] + * @param {Pattern} [assetKind] + * @param {Pattern} [amountShape] + */ +export const makeIssuerInterfaces = ( + brand = BrandShape, + assetKind = AssetValueShape, + amountShape = AmountShape, +) => { + const IssuerI = M.interface('Issuer', { + getBrand: M.call().returns(brand), + getAllegedName: M.call().returns(M.string()), + getAssetKind: M.call().returns(assetKind), + getDisplayInfo: M.call().returns(DisplayInfoShape), + makeEmptyPurse: M.call().returns(PurseShape), + + isLive: M.callWhen(M.await(PaymentShape)).returns(M.boolean()), + getAmountOf: M.callWhen(M.await(PaymentShape)).returns(amountShape), + burn: M.callWhen(M.await(PaymentShape)) + .optional(M.pattern()) + .returns(amountShape), + claim: M.callWhen(M.await(PaymentShape)) + .optional(M.pattern()) + .returns(PaymentShape), + combine: M.call(M.arrayOf(M.eref(PaymentShape))) + .optional(amountShape) + .returns(M.eref(PaymentShape)), + split: M.callWhen(M.await(PaymentShape), amountShape).returns( + M.arrayOf(PaymentShape), + ), + splitMany: M.callWhen( + M.await(PaymentShape), + M.arrayOf(amountShape), + ).returns(M.arrayOf(PaymentShape)), + }); + + const MintI = M.interface('Mint', { + getIssuer: M.call().returns(IssuerShape), + mintPayment: M.call(amountShape).returns(PaymentShape), + }); + + const PaymentI = M.interface('Payment', { + getAllegedBrand: M.call().returns(brand), + }); + + const PurseI = M.interface('Purse', { + getAllegedBrand: M.call().returns(brand), + getCurrentAmount: M.call().returns(amountShape), + getCurrentAmountNotifier: M.call().returns(NotifierShape), + // PurseI does *not* delay `deposit` until `srcPayment` is fulfulled. + // Rather, the semantics of `deposit` require it to provide its + // callers with a strong guarantee that `deposit` messages are + // processed without further delay in the order they arrive. + // PurseI therefore requires that the `srcPayment` argument already + // be a remotable, not a promise. + // PurseI only calls this raw method after validating that + // `srcPayment` is a remotable, leaving it + // to this raw method to validate that this remotable is actually + // a live payment of the correct brand with sufficient funds. + deposit: M.call(PaymentShape).optional(M.pattern()).returns(amountShape), + getDepositFacet: M.call().returns(DepositFacetShape), + withdraw: M.call(amountShape).returns(PaymentShape), + getRecoverySet: M.call().returns(M.setOf(PaymentShape)), + recoverAll: M.call().returns(amountShape), + }); + + const DepositFacetI = M.interface('DepositFacet', { + receive: PurseI.methodGuards.deposit, + }); + + const PurseIKit = harden({ + purse: PurseI, + depositFacet: DepositFacetI, + }); + + return harden({ + IssuerI, + MintI, + PaymentI, + PurseIKit, + }); +}; +harden(makeIssuerInterfaces); diff --git a/packages/ERTP/src/types.js b/packages/ERTP/src/types.js index 6bf657fbdbb..a3a5c7de934 100644 --- a/packages/ERTP/src/types.js +++ b/packages/ERTP/src/types.js @@ -254,6 +254,14 @@ * @property {IssuerSplitMany} splitMany */ +/** + * @template {AssetKind} [K=AssetKind] + * @typedef {object} PaymentLedger + * @property {Mint} mint + * @property {Issuer} issuer + * @property {Brand} brand + */ + /** * @template {AssetKind} [K=AssetKind] * @typedef {object} IssuerKit @@ -292,10 +300,6 @@ * @returns {void} */ -/** - * @typedef {import('./issuerKit').IssuerKit} IssuerKit - */ - /** * @template {AssetKind} [K=AssetKind] * @typedef {object} Mint @@ -304,6 +308,7 @@ * * @property {() => Issuer} getIssuer Gets the Issuer for this mint. * @property {(newAmount: Amount) => Payment} mintPayment + * Creates a new Payment containing newly minted amount. */ /** diff --git a/packages/ERTP/test/unitTests/test-inputValidation.js b/packages/ERTP/test/unitTests/test-inputValidation.js index 8c62e5a2f9e..b319962d826 100644 --- a/packages/ERTP/test/unitTests/test-inputValidation.js +++ b/packages/ERTP/test/unitTests/test-inputValidation.js @@ -143,11 +143,14 @@ test('makeIssuerKit bad optShutdownWithFailure', async t => { test('brand.isMyIssuer bad issuer', async t => { const { brand } = makeIssuerKit('myTokens'); // @ts-expect-error Intentional wrong type for testing - // but for some reason it is no longer complaining about the string - // being the wrong type. Hovering over `isMyIssuer` in vscode does - // show the correct type for the parameter. - // TODO(MSM): ask a typing person - const result = await brand.isMyIssuer('not an issuer'); + t.throwsAsync(() => brand.isMyIssuer('not an issuer'), { + message: + 'In "isMyIssuer" method of (myTokens brand) arg 0: string "not an issuer" - Must be a remotable (Issuer)', + }); + const fakeIssuer = /** @type {Issuer} */ ( + /** @type {unknown} */ (Far('myTokens issuer', {})) + ); + const result = await brand.isMyIssuer(fakeIssuer); t.false(result); }); @@ -185,7 +188,7 @@ test('issuer.combine bad payments array', async t => { // @ts-expect-error Intentional wrong type for testing await t.throwsAsync(() => E(issuer).combine(notAnArray), { message: - 'cannot serialize Remotables with non-methods like "length" in {"length":2,"split":"[Function split]"}', + 'In "combine" method of (fungible issuer) arg 0: cannot serialize Remotables with non-methods like "length" in {"length":2,"split":"[Function split]"}', }); const notAnArray2 = Far('notAnArray2', { @@ -195,7 +198,7 @@ test('issuer.combine bad payments array', async t => { // @ts-expect-error Intentional wrong type for testing await t.throwsAsync(() => E(issuer).combine(notAnArray2), { message: - '"fromPaymentsArray" "[Alleged: notAnArray2]" must be a pass-by-copy array, not "remotable"', + 'In "combine" method of (fungible issuer) arg 0: remotable "[Alleged: notAnArray2]" - Must be a copyArray', }); }); diff --git a/packages/ERTP/test/unitTests/test-issuerObj.js b/packages/ERTP/test/unitTests/test-issuerObj.js index 3178927ac89..72035b85006 100644 --- a/packages/ERTP/test/unitTests/test-issuerObj.js +++ b/packages/ERTP/test/unitTests/test-issuerObj.js @@ -198,7 +198,10 @@ test('purse.deposit promise', async t => { await t.throwsAsync( // @ts-expect-error deliberate invalid arguments for testing () => E(purse).deposit(exclusivePaymentP, fungible25), - { message: /deposit does not accept promises/ }, + { + message: + 'In "deposit" method of (fungible Purse purse) arg 0: promise "[Promise]" - Must be a remotable (Payment)', + }, 'failed to reject a promise for a payment', ); }); @@ -332,7 +335,7 @@ test('issuer.split bad amount', async t => { _ => E(issuer).split(payment, AmountMath.make(otherBrand, 10n)), { message: - /The brand in the allegedAmount .* in 'coerce' didn't match the specified brand/, + 'In "split" method of (fungible issuer) arg 1: brand: "[Alleged: other fungible brand]" - Must be: "[Alleged: fungible brand]"', }, 'throws for bad amount', ); @@ -409,11 +412,10 @@ test('issuer.combine array of promises', async t => { } harden(paymentsP); - const checkCombinedResult = paymentP => { + const checkCombinedResult = paymentP => issuer.getAmountOf(paymentP).then(pAmount => { t.is(pAmount.value, 100n); }); - }; await E(issuer).combine(paymentsP).then(checkCombinedResult); }); diff --git a/packages/ERTP/test/unitTests/test-mintObj.js b/packages/ERTP/test/unitTests/test-mintObj.js index 1484ae2c837..12009da19dc 100644 --- a/packages/ERTP/test/unitTests/test-mintObj.js +++ b/packages/ERTP/test/unitTests/test-mintObj.js @@ -46,7 +46,7 @@ test('mint.mintPayment set w strings AssetKind', async t => { const badAmount = AmountMath.make(brand, harden([['badElement']])); t.throws(() => mint.mintPayment(badAmount), { message: - 'minted amount: value: [0]: copyArray ["badElement"] - Must be a string', + 'In "mintPayment" method of (items mint) arg 0: value: [0]: copyArray ["badElement"] - Must be a string', }); }); diff --git a/packages/SwingSet/src/liveslots/virtualObjectManager.js b/packages/SwingSet/src/liveslots/virtualObjectManager.js index 202817d0779..c4895a79f7a 100644 --- a/packages/SwingSet/src/liveslots/virtualObjectManager.js +++ b/packages/SwingSet/src/liveslots/virtualObjectManager.js @@ -2,9 +2,12 @@ /* eslint-disable no-use-before-define, jsdoc/require-returns-type */ import { assert, details as X, q } from '@agoric/assert'; +import { defendPrototype } from '@agoric/store'; import { Far } from '@endo/marshal'; import { parseVatSlot } from '../lib/parseVatSlots.js'; +/** @template T @typedef {import('@agoric/vat-data').DefineKindOptions} DefineKindOptions */ + // import { kdebug } from './kdebug.js'; // Marker associated to flag objects that should be held onto strongly if @@ -148,7 +151,7 @@ export function makeCache(size, fetch, store) { * of virtual references. * @param {() => number} allocateExportID Function to allocate the next object * export ID for the enclosing vat. - * @param {(val: object) => string} getSlotForVal A function that returns the + * @param {(val: object) => string} _getSlotForVal A function that returns the * object ID (vref) for a given object, if any. their corresponding export * IDs * @param {*} registerValue Function to register a new slot+value in liveSlot's @@ -194,7 +197,7 @@ export function makeVirtualObjectManager( syscall, vrm, allocateExportID, - getSlotForVal, + _getSlotForVal, registerValue, serialize, unserialize, @@ -468,49 +471,6 @@ export function makeVirtualObjectManager( } } - function copyMethods(behavior) { - const obj = {}; - for (const prop of Reflect.ownKeys(behavior)) { - const func = behavior[prop]; - assert.typeof(func, 'function'); - obj[prop] = func; - } - return obj; - } - - function bindMethods(tag, contextMap, behaviorMethods) { - const prototype = {}; - for (const prop of Reflect.ownKeys(behaviorMethods)) { - const func = behaviorMethods[prop]; - assert.typeof(func, 'function'); - - // Violating all Jessie rules to create representative that inherit - // methods from a shared prototype. The bound method therefore needs - // to mention `this`. We define it using concise method syntax - // so that it will be `this` sensitive but not constructable. - // - // We normally consider `this` unsafe because of the hazard of a - // method of one abstraction being applied to an instance of - // another abstraction. To prevent that attack, the bound method - // checks that its `this` is in the map in which its representatives - // are registered. - const { method } = { - method(...args) { - const context = contextMap.get(this); - assert( - context, - X`${q( - `${tag}).${String(prop)}`, - )} may only be applied to a valid instance: ${this}`, - ); - return Reflect.apply(func, null, [context, ...args]); - }, - }; - prototype[prop] = method; - } - return Far(tag, prototype); - } - /** * @typedef {{ * kindID: string, @@ -538,7 +498,8 @@ export function makeVirtualObjectManager( * object) or a bag of bags of functions (in the case of a multi-faceted * object) that will become the methods of the object or its facets. * - * @param {*} options Additional options to configure the virtual object kind + * @param {DefineKindOptions<*>} options + * Additional options to configure the virtual object kind * being defined. Currently the only supported option is `finish`, an * optional finisher function that can perform post-creation initialization * operations, such as inserting the new object in a cyclical object graph. @@ -623,9 +584,12 @@ export function makeVirtualObjectManager( isDurable, durableKindDescriptor, ) { - const { finish } = options; + const { + finish, + thisfulMethods = false, + interfaceGuard = undefined, + } = options; let facetNames; - let behaviorTemplate; let contextMapTemplate; let prototypeTemplate; @@ -634,12 +598,13 @@ export function makeVirtualObjectManager( case 'one': { assert(!multifaceted); facetNames = undefined; - behaviorTemplate = copyMethods(behavior); contextMapTemplate = new WeakMap(); - prototypeTemplate = bindMethods( + prototypeTemplate = defendPrototype( tag, contextMapTemplate, - behaviorTemplate, + behavior, + thisfulMethods, + interfaceGuard, ); break; } @@ -650,18 +615,18 @@ export function makeVirtualObjectManager( facetNames.length > 1, 'a multi-facet object must have multiple facets', ); - behaviorTemplate = {}; contextMapTemplate = {}; prototypeTemplate = {}; for (const name of facetNames) { - const behaviorMethods = copyMethods(behavior[name]); - behaviorTemplate[name] = behaviorMethods; const contextMap = new WeakMap(); contextMapTemplate[name] = contextMap; - const prototype = bindMethods( + const prototype = defendPrototype( `${tag} ${name}`, contextMap, - behaviorMethods, + behavior[name], + thisfulMethods, + // TODO some tool does not yet understand the `?.[` syntax + interfaceGuard && interfaceGuard[name], ); prototypeTemplate[name] = prototype; } @@ -707,7 +672,6 @@ export function makeVirtualObjectManager( vrm.registerKind(kindID, reanimate, deleteStoredVO, isDurable); vrm.rememberFacetNames(kindID, facetNames); - harden(behaviorTemplate); harden(contextMapTemplate); harden(prototypeTemplate); diff --git a/packages/governance/test/unitTests/test-paramGovernance.js b/packages/governance/test/unitTests/test-paramGovernance.js index 3d319e7d8e4..3762fe46970 100644 --- a/packages/governance/test/unitTests/test-paramGovernance.js +++ b/packages/governance/test/unitTests/test-paramGovernance.js @@ -215,7 +215,10 @@ test('multiple params bad change', async t => { 2n, paramChangesSpec, ), - { message: /".13n." was not a live payment for brand/ }, + { + message: + 'In "getAmountOf" method of (Zoe Invitation issuer) arg 0: bigint "[13n]" - Must be a remotable (Payment)', + }, ); }); diff --git a/packages/store/src/index.js b/packages/store/src/index.js index e9868e2edbb..294ca4e168a 100755 --- a/packages/store/src/index.js +++ b/packages/store/src/index.js @@ -55,6 +55,7 @@ export { matches, fit, } from './patterns/patternMatchers.js'; +export { defendPrototype } from './patterns/interface-tools.js'; export { compareRank, isRankSorted, sortByRank } from './patterns/rankOrder.js'; export { makeDecodePassable, diff --git a/packages/store/src/patterns/interface-tools.js b/packages/store/src/patterns/interface-tools.js new file mode 100644 index 00000000000..68cbbbcfbd7 --- /dev/null +++ b/packages/store/src/patterns/interface-tools.js @@ -0,0 +1,236 @@ +import { Far } from '@endo/marshal'; +import { E } from '@endo/eventual-send'; +import { listDifference } from '@agoric/internal'; + +import { fit } from './patternMatchers.js'; + +const { details: X, quote: q } = assert; +const { apply, ownKeys } = Reflect; +const { defineProperties } = Object; + +const defendSyncArgs = (args, methodGuard, label) => { + const { argGuards, optionalArgGuards, restArgGuard } = methodGuard; + if (args.length < argGuards.length) { + assert.fail( + X`${label} args: ${args} - expected ${argGuards.length} arguments`, + ); + } + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + const argLabel = `${label} arg ${i}`; + if (i < argGuards.length) { + fit(arg, argGuards[i], argLabel); + } else if ( + optionalArgGuards && + i < argGuards.length + optionalArgGuards.length + ) { + if (arg !== undefined) { + // In the optional section, an `undefined` arg succeeds + // unconditionally + fit(arg, optionalArgGuards[i - argGuards.length], argLabel); + } + } else if (restArgGuard) { + const restArg = harden(args.slice(i)); + fit(restArg, restArgGuard, `${label} rest[${i}]`); + return; + } else { + assert.fail( + X`${argLabel}: ${args} - expected fewer than ${i + 1} arguments`, + ); + } + } + if (restArgGuard) { + fit(harden([]), restArgGuard, `${label} rest[]`); + } +}; + +const defendSyncMethod = (method, methodGuard, label) => { + const { returnGuard } = methodGuard; + const { syncMethod } = { + // Note purposeful use of `this` and concise method syntax + syncMethod(...args) { + defendSyncArgs(harden(args), methodGuard, label); + const result = apply(method, this, args); + fit(harden(result), returnGuard, `${label}: result`); + return result; + }, + }; + return syncMethod; +}; + +const isAwaitArgGuard = argGuard => + argGuard && typeof argGuard === 'object' && argGuard.klass === 'awaitArg'; + +const desync = methodGuard => { + const { argGuards, optionalArgGuards = [], restArgGuard } = methodGuard; + const rawArgGuards = [...argGuards, ...optionalArgGuards]; + + const awaitIndexes = []; + for (let i = 0; i < rawArgGuards.length; i += 1) { + const argGuard = rawArgGuards[i]; + if (isAwaitArgGuard(argGuard)) { + rawArgGuards[i] = argGuard.argGuard; + awaitIndexes.push(i); + } + } + return { + awaitIndexes, + rawMethodGuard: { + argGuards: rawArgGuards.slice(0, argGuards.length), + optionalArgGuards: rawArgGuards.slice(argGuards.length), + restArgGuard, + }, + }; +}; + +const defendAsyncMethod = (method, methodGuard, label) => { + const { returnGuard } = methodGuard; + const { awaitIndexes, rawMethodGuard } = desync(methodGuard); + const { asyncMethod } = { + // Note purposeful use of `this` and concise method syntax + asyncMethod(...args) { + const awaitList = awaitIndexes.map(i => args[i]); + const p = Promise.all(awaitList); + const rawArgs = [...args]; + const resultP = E.when(p, awaitedArgs => { + for (let j = 0; j < awaitIndexes.length; j += 1) { + rawArgs[awaitIndexes[j]] = awaitedArgs[j]; + } + defendSyncArgs(rawArgs, rawMethodGuard, label); + return apply(method, this, rawArgs); + }); + return E.when(resultP, result => { + fit(harden(result), returnGuard, `${label}: result`); + return result; + }); + }, + }; + return asyncMethod; +}; + +const defendMethod = (method, methodGuard, label) => { + const { klass, callKind } = methodGuard; + assert(klass === 'methodGuard'); + if (callKind === 'sync') { + return defendSyncMethod(method, methodGuard, label); + } else { + assert(callKind === 'async'); + return defendAsyncMethod(method, methodGuard, label); + } +}; + +const bindMethod = ( + methodTag, + contextMap, + behaviorMethod, + thisfulMethods = false, + methodGuard = undefined, +) => { + assert.typeof(behaviorMethod, 'function'); + + const getContext = self => { + const context = contextMap.get(self); + assert( + context, + X`${q(methodTag)} may only be applied to a valid instance: ${this}`, + ); + return context; + }; + + // Violating all Jessie rules to create representatives that inherit + // methods from a shared prototype. The bound method therefore needs + // to mention `this`. We define it using concise method syntax + // so that it will be `this` sensitive but not constructable. + // + // We normally consider `this` unsafe because of the hazard of a + // method of one abstraction being applied to an instance of + // another abstraction. To prevent that attack, the bound method + // checks that its `this` is in the map in which its representatives + // are registered. + let { method } = thisfulMethods + ? { + method(...args) { + const context = getContext(this); + return apply(behaviorMethod, context, args); + }, + } + : { + method(...args) { + const context = getContext(this); + return apply(behaviorMethod, null, [context, ...args]); + }, + }; + if (methodGuard) { + method = defendMethod(method, methodGuard, methodTag); + } + defineProperties(method, { + name: { value: methodTag }, + length: { + value: thisfulMethods ? behaviorMethod.length : behaviorMethod.length - 1, + }, + }); + return method; +}; + +/** + * @template T + * @param {string} tag + * @param {ContextMap} contextMap + * @param {any} behaviorMethods + * @param {boolean} [thisfulMethods] + * @param {InterfaceGuard} [interfaceGuard] + * @returns {T & RemotableBrand<{}, T>} + */ +export const defendPrototype = ( + tag, + contextMap, + behaviorMethods, + thisfulMethods = false, + interfaceGuard = undefined, +) => { + const prototype = {}; + const methodNames = ownKeys(behaviorMethods).filter( + // By ignoring any method named "constructor", we can use a + // class.prototype as a behaviorMethods. + name => name !== 'constructor', + ); + let methodGuards; + if (interfaceGuard) { + const { + klass, + interfaceName, + methodGuards: mg, + sloppy = false, + } = interfaceGuard; + methodGuards = mg; + assert.equal(klass, 'Interface'); + assert.typeof(interfaceName, 'string'); + { + const methodGuardNames = ownKeys(methodGuards); + const unimplemented = listDifference(methodGuardNames, methodNames); + assert( + unimplemented.length === 0, + X`methods ${q(unimplemented)} not implemented by ${q(tag)}`, + ); + if (!sloppy) { + const unguarded = listDifference(methodNames, methodGuardNames); + assert( + unguarded.length === 0, + X`methods ${q(unguarded)} not guarded by ${q(interfaceName)}`, + ); + } + } + } + for (const prop of methodNames) { + prototype[prop] = bindMethod( + `In ${q(prop)} method of (${tag})`, + contextMap, + behaviorMethods[prop], + thisfulMethods, + // TODO some tool does not yet understand the `?.[` syntax + methodGuards && methodGuards[prop], + ); + } + return Far(tag, prototype); +}; +harden(defendPrototype); diff --git a/packages/store/src/patterns/patternMatchers.js b/packages/store/src/patterns/patternMatchers.js index 410e67c69f6..149f744c280 100644 --- a/packages/store/src/patterns/patternMatchers.js +++ b/packages/store/src/patterns/patternMatchers.js @@ -1,3 +1,4 @@ +/* eslint-disable no-use-before-define */ // @ts-check import { @@ -8,7 +9,7 @@ import { passStyleOf, hasOwnPropertyOf, } from '@endo/marshal'; -import { applyLabelingError } from '@agoric/internal'; +import { applyLabelingError, listDifference } from '@agoric/internal'; import { compareRank, getPassStyleCover, @@ -24,14 +25,15 @@ import { checkScalarKey, isScalarKey, checkCopySet, - checkCopyBag, checkCopyMap, copyMapKeySet, + checkCopyBag, } from '../keys/checkKey.js'; /// const { quote: q, details: X } = assert; +const { entries, values } = Object; /** @type WeakSet */ const patternMemo = new WeakSet(); @@ -86,7 +88,7 @@ const makePatternKit = () => { case 'copyRecord': { // A copyRecord is a pattern iff all its children are // patterns - return Object.values(patt).every(checkIt); + return values(patt).every(checkIt); } case 'copyArray': { // A copyArray is a pattern iff all its children are @@ -102,35 +104,16 @@ const makePatternKit = () => { return matchHelper.checkIsMatcherPayload(patt.payload, check); } switch (tag) { - case 'copySet': { - if (!checkCopySet(patt, check)) { - return false; - } - // For a copySet to be a pattern, all its elements must be patterns. - // If all the elements are keys, then the copySet pattern is also - // a key and is already taken of. For the case where some elements - // are non-key patterns, general support is both hard and not - // currently needed. Currently, we only need a copySet of a single - // non-key pattern element. + case 'copySet': + case 'copyBag': { assert( - patt.payload.length === 1, - X`Non-singleton copySets with matcher not yet implemented: ${patt}`, + !isKey(patt), + X`internal: The key case should have been dealt with earlier: ${patt}`, + ); + return check( + false, + X`A ${q(tag)} must be a Key but was not: ${patt}`, ); - return checkPattern(patt.payload[0], check); - } - case 'copyBag': { - if (!checkCopyBag(patt, check)) { - return false; - } - // If it is a CopyBag, then it must also be a key and we - // should never get here. - if (isKey(patt)) { - assert.fail( - X`internal: The key case should have been dealt with earlier: ${patt}`, - ); - } else { - assert.fail(X`A CopyMap must be a Key but was not: ${patt}`); - } } case 'copyMap': { return ( @@ -307,29 +290,23 @@ const makePatternKit = () => { } const [specNames, specValues] = recordParts(specimen); const [pattNames, pattValues] = recordParts(patt); - if (!keyEQ(specNames, pattNames)) { - const specNameSet = new Set(specNames); - const missing = pattNames.filter(name => !specNameSet.has(name)); + { + const missing = listDifference(pattNames, specNames); if (missing.length >= 1) { return check( false, X`${specimen} - Must have missing properties ${q(missing)}`, ); } - const passNameSet = new Set(pattNames); - const unexpected = specNames.filter(name => !passNameSet.has(name)); - assert( - unexpected.length >= 1, - X`Internal: must have either missing or extra: ${q( - specNames, - )} vs ${q(pattNames)}`, - ); - return check( - false, - X`${specimen} - Must not have unexpected properties: ${q( - unexpected, - )}`, - ); + const unexpected = listDifference(specNames, pattNames); + if (unexpected.length >= 1) { + return check( + false, + X`${specimen} - Must not have unexpected properties: ${q( + unexpected, + )}`, + ); + } } return pattNames.every((label, i) => checkMatches(specValues[i], pattValues[i], check, label), @@ -341,37 +318,26 @@ const makePatternKit = () => { if (matchHelper) { return matchHelper.checkMatches(specimen, patt.payload, check); } - const msg = X`${specimen} - Only a ${q(pattTag)} matches a ${q( - pattTag, - )} pattern: ${patt}`; - if (specStyle !== 'tagged') { - return check(false, msg); - } - const specTag = getTag(specimen); - if (pattTag !== specTag) { - return check(false, msg); + if (specStyle !== 'tagged' || getTag(specimen) !== pattTag) { + return check( + false, + X`${specimen} - Only a ${q(pattTag)} matches a ${q( + pattTag, + )} pattern: ${patt}`, + ); } const { payload: pattPayload } = patt; const { payload: specPayload } = specimen; switch (pattTag) { - case 'copySet': { - if (!checkCopySet(specimen, check)) { - return false; - } - if (pattPayload.length !== specPayload.length) { - return check( - false, - X`copySet ${specimen} - Must have as many elements as copySet pattern: ${patt}`, - ); - } - // Should already be validated by checkPattern. But because this - // is a check that may loosen over time, we also assert everywhere - // we still rely on the restriction. + case 'copySet': + case 'copyBag': { assert( - patt.payload.length === 1, - X`Non-singleton copySets with matcher not yet implemented: ${patt}`, + !isKey(patt), + X`internal: The key case should have been dealt with earlier: ${patt}`, + ); + assert.fail( + X`internal: A ${q(pattTag)} must be a Key but was not: ${patt}`, ); - return checkMatches(specPayload[0], pattPayload[0], check, 0); } case 'copyMap': { if (!checkCopySet(specimen, check)) { @@ -589,18 +555,15 @@ const makePatternKit = () => { } } } - const details = X( - // quoting without quotes - [`${specimenKind} `, ` - Must be a ${kind}`], - specimen, - ); + // quoting without quotes + const details = X([`${specimenKind} `, ` - Must be a ${kind}`], specimen); return check(false, details); }; // /////////////////////// Match Helpers ///////////////////////////////////// /** @type {MatchHelper} */ - const matchAnyHelper = Far('M.any helper', { + const matchAnyHelper = Far('match.any helper', { checkMatches: (_specimen, _matcherPayload, _check) => true, checkIsMatcherPayload: (matcherPayload, check) => @@ -768,6 +731,41 @@ const makePatternKit = () => { }, }); + /** @type {MatchHelper} */ + const matchRemotableHelper = Far('match:remotable helper', { + checkMatches: (specimen, remotableDesc, check) => { + // Unfortunate duplication of checkKind logic, but no better choices. + if (checkKind(specimen, 'remotable', x => x)) { + return true; + } + let specimenKind = passStyleOf(specimen); + if (specimenKind === 'tagged') { + specimenKind = getTag(specimen); + } + const { label } = remotableDesc; + + // quoting without quotes + const details = X( + [`${specimenKind} `, ` - Must be a remotable (${label})`], + specimen, + ); + return check(false, details); + }, + + checkIsMatcherPayload: (allegedRemotableDesc, check) => + checkMatches( + allegedRemotableDesc, + harden({ label: M.string() }), + check, + 'match:remotable payload', + ), + + getRankCover: (_remotableDesc, _encodePassable) => + getPassStyleCover('remotable'), + + checkKeyPattern: (_remotableDesc, _check = x => x) => true, + }); + /** @type {MatchHelper} */ const matchLTEHelper = Far('match:lte helper', { checkMatches: (specimen, rightOperand, check) => @@ -854,38 +852,21 @@ const makePatternKit = () => { checkKeyPattern(rightOperand, check), }); - /** @type {MatchHelper} */ - const matchArrayOfHelper = Far('match:arrayOf helper', { - checkMatches: (specimen, subPatt, check) => - check( - passStyleOf(specimen) === 'copyArray', - X`${specimen} - Must be an array`, - ) && specimen.every((el, i) => checkMatches(el, subPatt, check, i)), - - checkIsMatcherPayload: checkPattern, - - getRankCover: () => getPassStyleCover('copyArray'), - - checkKeyPattern: (_, check) => - check(false, X`Arrays not yet supported as keys`), - }); - /** @type {MatchHelper} */ const matchRecordOfHelper = Far('match:recordOf helper', { checkMatches: (specimen, entryPatt, check) => - check( - passStyleOf(specimen) === 'copyRecord', - X`${specimen} - Must be a record`, - ) && - Object.entries(specimen).every(el => + checkKind(specimen, 'copyRecord', check) && + entries(specimen).every(el => checkMatches(harden(el), entryPatt, check, el[0]), ), checkIsMatcherPayload: (entryPatt, check) => - check( - passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, - X`${entryPatt} - Must be an pair of patterns`, - ) && checkPattern(entryPatt, check), + checkMatches( + entryPatt, + harden([M.pattern(), M.pattern()]), + check, + 'match:recordOf payload', + ), getRankCover: _entryPatt => getPassStyleCover('copyRecord'), @@ -893,13 +874,24 @@ const makePatternKit = () => { check(false, X`Records not yet supported as keys`), }); + /** @type {MatchHelper} */ + const matchArrayOfHelper = Far('match:arrayOf helper', { + checkMatches: (specimen, subPatt, check) => + checkKind(specimen, 'copyArray', check) && + specimen.every((el, i) => checkMatches(el, subPatt, check, i)), + + checkIsMatcherPayload: checkPattern, + + getRankCover: () => getPassStyleCover('copyArray'), + + checkKeyPattern: (_, check) => + check(false, X`Arrays not yet supported as keys`), + }); + /** @type {MatchHelper} */ const matchSetOfHelper = Far('match:setOf helper', { checkMatches: (specimen, keyPatt, check) => - check( - passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copySet', - X`${specimen} - Must be a a CopySet`, - ) && + checkKind(specimen, 'copySet', check) && specimen.payload.every((el, i) => checkMatches(el, keyPatt, check, i)), checkIsMatcherPayload: checkPattern, @@ -913,10 +905,7 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchBagOfHelper = Far('match:bagOf helper', { checkMatches: (specimen, [keyPatt, countPatt], check) => - check( - passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyBag', - X`${specimen} - Must be a a CopyBag`, - ) && + checkKind(specimen, 'copyBag', check) && specimen.payload.every( ([key, count], i) => checkMatches(key, keyPatt, check, `keys[${i}]`) && @@ -924,10 +913,12 @@ const makePatternKit = () => { ), checkIsMatcherPayload: (entryPatt, check) => - check( - passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, - X`${entryPatt} - Must be an pair of patterns`, - ) && checkPattern(entryPatt, check), + checkMatches( + entryPatt, + harden([M.pattern(), M.pattern()]), + check, + 'match:bagOf payload', + ), getRankCover: () => getPassStyleCover('tagged'), @@ -938,10 +929,7 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchMapOfHelper = Far('match:mapOf helper', { checkMatches: (specimen, [keyPatt, valuePatt], check) => - check( - passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyMap', - X`${specimen} - Must be a CopyMap`, - ) && + checkKind(specimen, 'copyMap', check) && specimen.payload.keys.every((k, i) => checkMatches(k, keyPatt, check, `keys[${i}]`), ) && @@ -950,10 +938,12 @@ const makePatternKit = () => { ), checkIsMatcherPayload: (entryPatt, check) => - check( - passStyleOf(entryPatt) === 'copyArray' && entryPatt.length === 2, - X`${entryPatt} - Must be an pair of patterns`, - ) && checkPattern(entryPatt, check), + checkMatches( + entryPatt, + harden([M.pattern(), M.pattern()]), + check, + 'match:mapOf payload', + ), getRankCover: _entryPatt => getPassStyleCover('tagged'), @@ -980,7 +970,7 @@ const makePatternKit = () => { // Not yet frozen! Mutated in place specB = {}; specR = {}; - for (const [name, value] of Object.entries(specimen)) { + for (const [name, value] of entries(specimen)) { if (hasOwnPropertyOf(base, name)) { specB[name] = value; } else { @@ -1033,26 +1023,42 @@ const makePatternKit = () => { } let specB; let specR; - let newBase = base; + let newBase; if (baseStyle === 'copyArray') { const { length: specLen } = specimen; const { length: baseLen } = base; if (specLen < baseLen) { newBase = harden(base.slice(0, specLen)); + specB = specimen; + // eslint-disable-next-line no-use-before-define + specR = []; + } else { + newBase = [...base]; + specB = specimen.slice(0, baseLen); + specR = specimen.slice(baseLen); + } + for (let i = 0; i < newBase.length; i += 1) { + // For the optional base array parts, an undefined specimen element + // matches unconditionally. + if (specB[i] === undefined) { + // eslint-disable-next-line no-use-before-define + newBase[i] = M.any(); + } } - // Frozen below - specB = specimen.slice(0, baseLen); - specR = specimen.slice(baseLen); } else { assert(baseStyle === 'copyRecord'); // Not yet frozen! Mutated in place specB = {}; specR = {}; newBase = {}; - for (const [name, value] of Object.entries(specimen)) { + for (const [name, value] of entries(specimen)) { if (hasOwnPropertyOf(base, name)) { - specB[name] = value; - newBase[name] = base[name]; + // For the optional base record parts, an undefined specimen value + // matches unconditionally. + if (value !== undefined) { + specB[name] = value; + newBase[name] = base[name]; + } } else { specR[name] = value; } @@ -1085,6 +1091,7 @@ const makePatternKit = () => { 'match:key': matchKeyHelper, 'match:pattern': matchPatternHelper, 'match:kind': matchKindHelper, + 'match:remotable': matchRemotableHelper, 'match:lt': matchLTHelper, 'match:lte': matchLTEHelper, @@ -1128,6 +1135,64 @@ const makePatternKit = () => { const PromiseShape = makeKindMatcher('promise'); const UndefinedShape = makeKindMatcher('undefined'); + const makeRemotableMatcher = (label = undefined) => + label === undefined + ? RemotableShape + : makeMatcher('match:remotable', harden({ label })); + + /** + * @param {'sync'|'async'} callKind + * @param {ArgGuard[]} argGuards + * @param {ArgGuard[]} [optionalArgGuards] + * @param {ArgGuard} [restArgGuard] + * @returns {MethodGuard} + */ + const makeMethodGuardMaker = ( + callKind, + argGuards, + optionalArgGuards = undefined, + restArgGuard = undefined, + ) => + harden({ + optional: (...optArgGuards) => { + assert( + optionalArgGuards === undefined, + X`Can only have one set of optional guards`, + ); + assert( + restArgGuard === undefined, + X`optional arg guards must come before rest arg`, + ); + return makeMethodGuardMaker(callKind, argGuards, optArgGuards); + }, + rest: rArgGuard => { + assert(restArgGuard === undefined, X`Can only have one rest arg`); + return makeMethodGuardMaker( + callKind, + argGuards, + optionalArgGuards, + rArgGuard, + ); + }, + returns: (returnGuard = UndefinedShape) => + harden({ + klass: 'methodGuard', + callKind, + argGuards, + optionalArgGuards, + restArgGuard, + returnGuard, + }), + }); + + const makeAwaitArgGuard = argGuard => + harden({ + klass: 'awaitArg', + argGuard, + }); + + // ////////////////// + /** @type {MatcherNamespace} */ const M = harden({ any: () => AnyShape, @@ -1150,7 +1215,7 @@ const makePatternKit = () => { set: () => SetShape, bag: () => BagShape, map: () => MapShape, - remotable: () => RemotableShape, + remotable: makeRemotableMatcher, error: () => ErrorShape, promise: () => PromiseShape, undefined: () => UndefinedShape, @@ -1178,6 +1243,28 @@ const makePatternKit = () => { makeMatcher('match:split', rest === undefined ? [base] : [base, rest]), partial: (base, rest = undefined) => makeMatcher('match:partial', rest === undefined ? [base] : [base, rest]), + + eref: t => M.or(t, M.promise()), + opt: t => M.or(t, M.undefined()), + + interface: (interfaceName, methodGuards, { sloppy = false } = {}) => { + for (const [_, methodGuard] of entries(methodGuards)) { + assert( + methodGuard.klass === 'methodGuard', + X`unrecognize method guard ${methodGuard}`, + ); + } + return harden({ + klass: 'Interface', + interfaceName, + methodGuards, + sloppy, + }); + }, + call: (...argGuards) => makeMethodGuardMaker('sync', argGuards), + callWhen: (...argGuards) => makeMethodGuardMaker('async', argGuards), + + await: argGuard => makeAwaitArgGuard(argGuard), }); return harden({ diff --git a/packages/store/src/types.js b/packages/store/src/types.js index 84c5d416d41..e1ff48b4c6b 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -500,7 +500,10 @@ * @property {() => Matcher} set A CopySet * @property {() => Matcher} bag A CopyBag * @property {() => Matcher} map A CopyMap - * @property {() => Matcher} remotable A far object or its remote presence + * @property {(label?: string) => Matcher} remotable + * A far object or its remote presence. The optional `label` is purely for + * diagnostic purpose. It does not enforce any constraint beyond the + * must-be-a-remotable constraint. * @property {() => Matcher} error * Error objects are passable, but are neither keys nor symbols. * They do not have a useful identity. @@ -555,8 +558,25 @@ * rest pattern if present. * Unlike `M.split`, `M.partial` ignores properties on the base * pattern that are not present on the specimen. + * + * @property {(t: Pattern) => Pattern} eref + * @property {(t: Pattern) => Pattern} opt + * + * @property {(interfaceName: string, + * methodGuards: Record, + * options?: {sloppy?: boolean} + * ) => InterfaceGuard} interface + * @property {(...argGuards: ArgGuard[]) => MethodGuardMaker} call + * @property {(...argGuards: ArgGuard[]) => MethodGuardMaker} callWhen + * + * @property {(argGuard: ArgGuard) => ArgGuard} await */ +/** @typedef {any} InterfaceGuard */ +/** @typedef {any} MethodGuardMaker */ +/** @typedef {any} MethodGuard */ +/** @typedef {any} ArgGuard */ + /** * @typedef {object} PatternKit * @property {(specimen: Passable, patt: Pattern) => boolean} matches diff --git a/packages/store/test/test-patterns.js b/packages/store/test/test-patterns.js index 647f26d8621..527faf60c83 100644 --- a/packages/store/test/test-patterns.js +++ b/packages/store/test/test-patterns.js @@ -264,6 +264,71 @@ const matchTests = harden([ ], ], }, + { + specimen: makeTagged('mysteryTag', 88), + yesPatterns: [M.any(), M.not(M.pattern())], + noPatterns: [ + [ + M.pattern(), + 'A passable tagged "mysteryTag" is not a pattern: "[mysteryTag]"', + ], + ], + }, + { + specimen: makeTagged('match:any', undefined), + yesPatterns: [M.any(), M.pattern()], + noPatterns: [ + [M.key(), 'A passable tagged "match:any" is not a key: "[match:any]"'], + ], + }, + { + specimen: makeTagged('match:any', 88), + yesPatterns: [M.any(), M.not(M.pattern())], + noPatterns: [[M.pattern(), 'Payload must be undefined: 88']], + }, + { + specimen: makeTagged('match:remotable', 88), + yesPatterns: [M.any(), M.not(M.pattern())], + noPatterns: [ + [ + M.pattern(), + 'match:remotable payload: 88 - Must be a copyRecord to match a copyRecord pattern: {"label":"[match:kind]"}', + ], + ], + }, + { + specimen: makeTagged('match:remotable', harden({ label: 88 })), + yesPatterns: [M.any(), M.not(M.pattern())], + noPatterns: [ + [ + M.pattern(), + 'match:remotable payload: label: number 88 - Must be a string', + ], + ], + }, + { + specimen: makeTagged('match:recordOf', harden([M.string(), M.nat()])), + yesPatterns: [M.pattern()], + noPatterns: [ + [ + M.key(), + 'A passable tagged "match:recordOf" is not a key: "[match:recordOf]"', + ], + ], + }, + { + specimen: makeTagged( + 'match:recordOf', + harden([M.string(), Promise.resolve(null)]), + ), + yesPatterns: [M.any(), M.not(M.pattern())], + noPatterns: [ + [ + M.pattern(), + 'match:recordOf payload: [1]: A "promise" cannot be a pattern', + ], + ], + }, ]); test('test simple matches', t => { @@ -300,3 +365,10 @@ test('masking match failure', t => { message: 'A passable tagged "match:kind" is not a key: "[match:kind]"', }); }); + +test('well formed patterns', t => { + // @ts-expect-error purposeful type violation for testing + t.throws(() => M.remotable(88), { + message: 'match:remotable payload: label: number 88 - Must be a string', + }); +}); diff --git a/packages/vat-data/src/far-class-utils.js b/packages/vat-data/src/far-class-utils.js new file mode 100644 index 00000000000..c74d1f1b89d --- /dev/null +++ b/packages/vat-data/src/far-class-utils.js @@ -0,0 +1,217 @@ +import { provideKindHandle } from './kind-utils.js'; +import { + defineKind, + defineKindMulti, + defineDurableKind, + defineDurableKindMulti, + provide, +} from './vat-data-bindings.js'; + +/** @template L,R @typedef {import('@endo/eventual-send').RemotableBrand} RemotableBrand */ +/** @template T @typedef {import('@endo/eventual-send').ERef} ERef */ +/** @typedef {import('@agoric/store').InterfaceGuard} InterfaceGuard */ +/** @typedef {import('./types.js').Baggage} Baggage */ +/** @template T @typedef {import('./types.js').DefineKindOptions} DefineKindOptions */ +/** @template T @typedef {import('./types.js').KindFacet} KindFacet */ +/** @template T @typedef {import('./types.js').KindFacets} KindFacets */ +/** @typedef {import('./types.js').DurableKindHandle} DurableKindHandle */ + +/** + * @template A,S,T + * @param {string} tag + * @param {any} interfaceGuard + * @param {(...args: A[]) => S} init + * @param {T} methods + * @param {DefineKindOptions} [options] + * @returns {(...args: A[]) => (T & RemotableBrand<{}, T>)} + */ +export const defineVirtualFarClass = ( + tag, + interfaceGuard, + init, + methods, + options, +) => + // @ts-expect-error The use of `thisfulMethods` to change + // the appropriate static type is the whole point of this method. + defineKind(tag, init, methods, { + ...options, + thisfulMethods: true, + interfaceGuard, + }); +harden(defineVirtualFarClass); + +/** + * @template A,S,T + * @param {string} tag + * @param {any} interfaceGuardKit + * @param {(...args: A[]) => S} init + * @param {T} facets + * @param {DefineKindOptions} [options] + * @returns {(...args: A[]) => (T & RemotableBrand<{}, T>)} + */ +export const defineVirtualFarClassKit = ( + tag, + interfaceGuardKit, + init, + facets, + options, +) => + // @ts-expect-error The use of `thisfulMethods` to change + // the appropriate static type is the whole point of this method. + defineKindMulti(tag, init, facets, { + ...options, + thisfulMethods: true, + interfaceGuard: interfaceGuardKit, + }); +harden(defineVirtualFarClass); + +/** + * @template A,S,T + * @param {DurableKindHandle} kindHandle + * @param {any} interfaceGuard + * @param {(...args: A[]) => S} init + * @param {T} methods + * @param {DefineKindOptions} [options] + * @returns {(...args: A[]) => (T & RemotableBrand<{}, T>)} + */ +export const defineDurableFarClass = ( + kindHandle, + interfaceGuard, + init, + methods, + options, +) => + // @ts-expect-error The use of `thisfulMethods` to change + // the appropriate static type is the whole point of this method. + defineDurableKind(kindHandle, init, methods, { + ...options, + thisfulMethods: true, + interfaceGuard, + }); +harden(defineDurableFarClass); + +/** + * @template A,S,T + * @param {DurableKindHandle} kindHandle + * @param {any} interfaceGuardKit + * @param {(...args: A[]) => S} init + * @param {T} facets + * @param {DefineKindOptions} [options] + * @returns {(...args: A[]) => (T & RemotableBrand<{}, T>)} + */ +export const defineDurableFarClassKit = ( + kindHandle, + interfaceGuardKit, + init, + facets, + options, +) => + // @ts-expect-error The use of `thisfulMethods` to change + // the appropriate static type is the whole point of this method. + defineDurableKindMulti(kindHandle, init, facets, { + ...options, + thisfulMethods: true, + interfaceGuard: interfaceGuardKit, + }); +harden(defineDurableFarClassKit); + +/** + * @template A,S,T + * @param {Baggage} baggage + * @param {string} kindName + * @param {any} interfaceGuard + * @param {(...args: A[]) => S} init + * @param {T} methods + * @param {DefineKindOptions} [options] + * @returns {(...args: A[]) => (T & RemotableBrand<{}, T>)} + */ +export const vivifyFarClass = ( + baggage, + kindName, + interfaceGuard, + init, + methods, + options = undefined, +) => + defineDurableFarClass( + provideKindHandle(baggage, kindName), + interfaceGuard, + init, + methods, + options, + ); +harden(vivifyFarClass); + +/** + * @template A,S,T + * @param {Baggage} baggage + * @param {string} kindName + * @param {any} interfaceGuardKit + * @param {(...args: A[]) => S} init + * @param {T} facets + * @param {DefineKindOptions} [options] + * @returns {(...args: A[]) => (T & RemotableBrand<{}, T>)} + */ +export const vivifyFarClassKit = ( + baggage, + kindName, + interfaceGuardKit, + init, + facets, + options = undefined, +) => + defineDurableFarClassKit( + provideKindHandle(baggage, kindName), + interfaceGuardKit, + init, + facets, + options, + ); +harden(vivifyFarClassKit); + +/** + * @template T,M + * @param {Baggage} baggage + * @param {string} kindName + * @param {InterfaceGuard|undefined} interfaceGuard + * @param {M} methods + * @param {DefineKindOptions} [options] + * @returns {T & RemotableBrand<{}, T>} + */ +export const vivifyFarInstance = ( + baggage, + kindName, + interfaceGuard, + methods, + options = undefined, +) => { + const makeSingleton = vivifyFarClass( + baggage, + kindName, + interfaceGuard, + () => ({}), + methods, + options, + ); + + return provide(baggage, `the_${kindName}`, () => makeSingleton()); +}; +harden(vivifyFarInstance); + +/** + * @deprecated Use vivifyFarInstance instead. + * @template T + * @param {Baggage} baggage + * @param {string} kindName + * @param {T} methods + * @param {DefineKindOptions} [options] + * @returns {T & RemotableBrand<{}, T>} + */ +export const vivifySingleton = ( + baggage, + kindName, + methods, + options = undefined, +) => vivifyFarInstance(baggage, kindName, undefined, methods, options); +harden(vivifySingleton); diff --git a/packages/vat-data/src/index.js b/packages/vat-data/src/index.js index 03b6b80c278..91f915dc264 100644 --- a/packages/vat-data/src/index.js +++ b/packages/vat-data/src/index.js @@ -1,3 +1,13 @@ /// export * from './vat-data-bindings.js'; export * from './kind-utils.js'; +export { + defineVirtualFarClass, + defineVirtualFarClassKit, + defineDurableFarClass, + defineDurableFarClassKit, + vivifyFarClass, + vivifyFarClassKit, + vivifyFarInstance, + vivifySingleton, +} from './far-class-utils.js'; diff --git a/packages/vat-data/src/kind-utils.js b/packages/vat-data/src/kind-utils.js index 90a9d2867b8..ca76aa6813a 100644 --- a/packages/vat-data/src/kind-utils.js +++ b/packages/vat-data/src/kind-utils.js @@ -1,4 +1,3 @@ -import { objectMap } from '@agoric/internal'; import { provide, defineDurableKind, @@ -14,13 +13,14 @@ import { /** @typedef {import('./types.js').DurableKindHandle} DurableKindHandle */ /** - * Make a version of the argument function that takes a kind context but ignores it. + * Make a version of the argument function that takes a kind context but + * ignores it. * * @type {(fn: T) => import('./types.js').PlusContext} */ export const ignoreContext = fn => - (context, ...args) => + (_context, ...args) => fn(...args); harden(ignoreContext); @@ -64,30 +64,3 @@ export const vivifyKindMulti = ( options, ); harden(vivifyKindMulti); - -/** - * @template T - * @param {Baggage} baggage - * @param {string} kindName - * @param {T} methods - * @param {DefineKindOptions} [options] - * @returns {T & RemotableBrand<{}, T>} - */ -export const vivifySingleton = ( - baggage, - kindName, - methods, - options = undefined, -) => { - const behavior = objectMap(methods, ignoreContext); - const makeSingleton = vivifyKind( - baggage, - kindName, - () => ({}), - behavior, - options, - ); - - return provide(baggage, `the_${kindName}`, () => makeSingleton()); -}; -harden(vivifySingleton); diff --git a/packages/vat-data/src/types.d.ts b/packages/vat-data/src/types.d.ts index c07b8e6d62c..7c08e576746 100644 --- a/packages/vat-data/src/types.d.ts +++ b/packages/vat-data/src/types.d.ts @@ -45,6 +45,8 @@ export type DurableKindHandle = DurableKindHandleClass; type DefineKindOptions = { finish?: (context: C) => void; durable?: boolean; + thisfulMethods?: boolean; + interfaceGuard?: object; // TODO type }; export type VatData = { diff --git a/packages/zoe/test/unitTests/contracts/loan/helpers.js b/packages/zoe/test/unitTests/contracts/loan/helpers.js index 77c88f303c8..e909c95f08d 100644 --- a/packages/zoe/test/unitTests/contracts/loan/helpers.js +++ b/packages/zoe/test/unitTests/contracts/loan/helpers.js @@ -79,16 +79,18 @@ export const checkPayouts = async ( message = '', ) => { const payouts = await E(seat).getPayouts(); - Object.entries(payouts).forEach(async ([keyword, paymentP]) => { - const kit = kitKeywordRecord[keyword]; - const amount = await kit.issuer.getAmountOf(paymentP); - const expected = expectedKeywordRecord[keyword]; - assertAmountsEqual(t, amount, expected); - t.truthy( - AmountMath.isEqual(amount, expected), - `amount value: ${amount.value}, expected value: ${expected.value}, message: ${message}`, - ); - }); + await Promise.all( + Object.entries(payouts).map(async ([keyword, paymentP]) => { + const kit = kitKeywordRecord[keyword]; + const amount = await kit.issuer.getAmountOf(paymentP); + const expected = expectedKeywordRecord[keyword]; + assertAmountsEqual(t, amount, expected); + t.truthy( + AmountMath.isEqual(amount, expected), + `amount value: ${amount.value}, expected value: ${expected.value}, message: ${message}`, + ); + }), + ); t.truthy(seat.hasExited()); }; From ef286d101dc3e7405da6c8b5b72a6281e908a516 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 31 Aug 2022 15:56:05 -0700 Subject: [PATCH 2/2] fix: review suggestions --- packages/ERTP/src/payment.js | 3 +- packages/ERTP/src/typeGuards.js | 20 ++-- packages/ERTP/test/unitTests/test-mintObj.js | 4 +- .../src/liveslots/virtualObjectManager.js | 5 +- .../SwingSet/src/liveslots/watchedPromises.js | 4 +- .../test/promise-watcher/vat-upton.js | 3 +- packages/SwingSet/test/upgrade/vat-ulrik-1.js | 11 ++- packages/SwingSet/test/upgrade/vat-ulrik-2.js | 5 +- .../virtualObjects/test-reachable-vrefs.js | 5 +- .../virtualObjects/test-retain-remotable.js | 5 +- .../test/virtualObjects/vat-orphan-bob.js | 3 +- packages/assert/src/assert.js | 15 +++ packages/inter-protocol/src/psm/psm.js | 18 ++-- .../src/vpool-xyk-amm/multipoolMarketMaker.js | 3 +- packages/store/src/index.js | 2 +- packages/store/src/keys/checkKey.js | 14 +-- .../store/src/patterns/interface-tools.js | 17 ++++ .../store/src/patterns/patternMatchers.js | 97 ++++++++++--------- packages/store/test/test-patterns.js | 6 +- packages/vat-data/src/far-class-utils.js | 6 +- packages/vat-data/src/index.js | 2 + packages/vat-data/src/kind-utils.js | 4 - packages/vat-data/src/types.d.ts | 44 +++++++++ packages/zoe/src/contractFacet/zcfZygote.js | 4 +- packages/zoe/src/makeHandle.js | 3 +- packages/zoe/src/zoeService/feeMint.js | 3 +- .../zoe/src/zoeService/installationStorage.js | 3 +- 27 files changed, 202 insertions(+), 107 deletions(-) diff --git a/packages/ERTP/src/payment.js b/packages/ERTP/src/payment.js index db3b9c6fbc7..ec6fc369330 100644 --- a/packages/ERTP/src/payment.js +++ b/packages/ERTP/src/payment.js @@ -1,5 +1,6 @@ // @ts-check +import { initEmpty } from '@agoric/store'; import { vivifyFarClass } from '@agoric/vat-data'; /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ @@ -17,7 +18,7 @@ export const vivifyPaymentKind = (issuerBaggage, name, brand, PaymentI) => { issuerBaggage, `${name} payment`, PaymentI, - () => ({}), + initEmpty, { getAllegedBrand() { return brand; diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index 6ee3f39498c..19613bd0161 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -114,7 +114,7 @@ harden(isCopyBagValue); // One GOOGOLth should be enough decimal places for anybody. export const MAX_ABSOLUTE_DECIMAL_PLACES = 100; -export const AssetValueShape = M.or('nat', 'set', 'copySet', 'copyBag'); +export const AssetKindShape = M.or('nat', 'set', 'copySet', 'copyBag'); export const DisplayInfoShape = M.partial( harden({ @@ -122,7 +122,7 @@ export const DisplayInfoShape = M.partial( M.gte(-MAX_ABSOLUTE_DECIMAL_PLACES), M.lte(MAX_ABSOLUTE_DECIMAL_PLACES), ), - assetKind: AssetValueShape, + assetKind: AssetKindShape, }), harden({ // Including this empty `rest` ensures that there are no other @@ -148,19 +148,19 @@ export const BrandI = M.interface('Brand', { }); /** - * @param {Pattern} [brand] - * @param {Pattern} [assetKind] + * @param {Pattern} [brandShape] + * @param {Pattern} [assetKindShape] * @param {Pattern} [amountShape] */ export const makeIssuerInterfaces = ( - brand = BrandShape, - assetKind = AssetValueShape, + brandShape = BrandShape, + assetKindShape = AssetKindShape, amountShape = AmountShape, ) => { const IssuerI = M.interface('Issuer', { - getBrand: M.call().returns(brand), + getBrand: M.call().returns(brandShape), getAllegedName: M.call().returns(M.string()), - getAssetKind: M.call().returns(assetKind), + getAssetKind: M.call().returns(assetKindShape), getDisplayInfo: M.call().returns(DisplayInfoShape), makeEmptyPurse: M.call().returns(PurseShape), @@ -190,11 +190,11 @@ export const makeIssuerInterfaces = ( }); const PaymentI = M.interface('Payment', { - getAllegedBrand: M.call().returns(brand), + getAllegedBrand: M.call().returns(brandShape), }); const PurseI = M.interface('Purse', { - getAllegedBrand: M.call().returns(brand), + getAllegedBrand: M.call().returns(brandShape), getCurrentAmount: M.call().returns(amountShape), getCurrentAmountNotifier: M.call().returns(NotifierShape), // PurseI does *not* delay `deposit` until `srcPayment` is fulfulled. diff --git a/packages/ERTP/test/unitTests/test-mintObj.js b/packages/ERTP/test/unitTests/test-mintObj.js index 12009da19dc..c3f1e45ec3b 100644 --- a/packages/ERTP/test/unitTests/test-mintObj.js +++ b/packages/ERTP/test/unitTests/test-mintObj.js @@ -1,7 +1,7 @@ // @ts-check import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; -import { M } from '@agoric/store'; +import { initEmpty, M } from '@agoric/store'; // eslint-disable-next-line import/no-extraneous-dependencies import { assert } from '@agoric/assert'; @@ -52,7 +52,7 @@ test('mint.mintPayment set w strings AssetKind', async t => { const makeDurableHandle = name => { const kindHandle = makeKindHandle(name); - const maker = defineDurableKind(kindHandle, () => ({}), {}); + const maker = defineDurableKind(kindHandle, initEmpty, {}); return maker(); }; diff --git a/packages/SwingSet/src/liveslots/virtualObjectManager.js b/packages/SwingSet/src/liveslots/virtualObjectManager.js index c4895a79f7a..524ecfb4307 100644 --- a/packages/SwingSet/src/liveslots/virtualObjectManager.js +++ b/packages/SwingSet/src/liveslots/virtualObjectManager.js @@ -500,9 +500,8 @@ export function makeVirtualObjectManager( * * @param {DefineKindOptions<*>} options * Additional options to configure the virtual object kind - * being defined. Currently the only supported option is `finish`, an - * optional finisher function that can perform post-creation initialization - * operations, such as inserting the new object in a cyclical object graph. + * being defined. See the documentation of DefineKindOptions + * for the meaning of each option. * * @param {boolean} isDurable A flag indicating whether or not the newly defined * kind should be a durable kind. diff --git a/packages/SwingSet/src/liveslots/watchedPromises.js b/packages/SwingSet/src/liveslots/watchedPromises.js index b559847fcc0..6618ef141c9 100644 --- a/packages/SwingSet/src/liveslots/watchedPromises.js +++ b/packages/SwingSet/src/liveslots/watchedPromises.js @@ -4,7 +4,7 @@ /* eslint-disable no-lonely-if */ import { assert } from '@agoric/assert'; -import { M } from '@agoric/store'; +import { initEmpty, M } from '@agoric/store'; import { parseVatSlot } from '../lib/parseVatSlots.js'; /** @@ -148,7 +148,7 @@ export function makeWatchedPromiseManager( assert.typeof(fulfillHandler, 'function'); assert.typeof(rejectHandler, 'function'); - const makeWatcher = defineDurableKind(kindHandle, () => ({}), { + const makeWatcher = defineDurableKind(kindHandle, initEmpty, { // @ts-expect-error TS is confused by the spread operator onFulfilled: (_context, res, ...args) => fulfillHandler(res, ...args), // @ts-expect-error diff --git a/packages/SwingSet/test/promise-watcher/vat-upton.js b/packages/SwingSet/test/promise-watcher/vat-upton.js index 8f786cc5c89..16b9bef78f3 100644 --- a/packages/SwingSet/test/promise-watcher/vat-upton.js +++ b/packages/SwingSet/test/promise-watcher/vat-upton.js @@ -1,5 +1,6 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; +import { initEmpty } from '@agoric/store'; import { makePromiseKit } from '@endo/promise-kit'; import { provideKindHandle, @@ -27,7 +28,7 @@ export function buildRootObject(vatPowers, vatParameters, baggage) { ); // prettier-ignore - const makeDK = defineDurableKindMulti(dkHandle, () => ({}), { + const makeDK = defineDurableKindMulti(dkHandle, initEmpty, { full: { onFulfilled: (_context, res, tag) => log(`${tag} resolved ${res} version ${vatParameters.version} via VDO`), diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-1.js b/packages/SwingSet/test/upgrade/vat-ulrik-1.js index 73af2e610dc..6e49c9ccd4f 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-1.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-1.js @@ -1,6 +1,7 @@ import { Far } from '@endo/marshal'; import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; +import { initEmpty } from '@agoric/store'; import { makeKindHandle, defineDurableKind, @@ -183,14 +184,14 @@ export const buildRootObject = (_vatPowers, vatParameters, baggage) => { switch (mode) { case 's2mFacetiousnessMismatch': { // upgrade should fail - defineDurableKind(mkh, () => ({}), { + defineDurableKind(mkh, initEmpty, { fooMethod: () => 1, }); break; } case 'facetCountMismatch': { // upgrade should fail - defineDurableKindMulti(mkh, () => ({}), { + defineDurableKindMulti(mkh, initEmpty, { foo: { fooMethod: () => 1, }, @@ -202,7 +203,7 @@ export const buildRootObject = (_vatPowers, vatParameters, baggage) => { } case 'facetNameMismatch': { // upgrade should fail - defineDurableKindMulti(mkh, () => ({}), { + defineDurableKindMulti(mkh, initEmpty, { foo: { fooMethod: () => 1, }, @@ -217,7 +218,7 @@ export const buildRootObject = (_vatPowers, vatParameters, baggage) => { } case 'facetOrderMismatch': { // upgrade should succeed since facet names get sorted - defineDurableKindMulti(mkh, () => ({}), { + defineDurableKindMulti(mkh, initEmpty, { baz: { bazMethod: () => 1, }, @@ -232,7 +233,7 @@ export const buildRootObject = (_vatPowers, vatParameters, baggage) => { } default: { // upgrade should succeed - defineDurableKindMulti(mkh, () => ({}), { + defineDurableKindMulti(mkh, initEmpty, { foo: { fooMethod: () => 1, }, diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-2.js b/packages/SwingSet/test/upgrade/vat-ulrik-2.js index 3e3e061aa76..1c7f48c3ff6 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-2.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-2.js @@ -1,6 +1,7 @@ import { Far } from '@endo/marshal'; import { E } from '@endo/eventual-send'; import { assert } from '@agoric/assert'; +import { initEmpty } from '@agoric/store'; import { defineDurableKind, defineDurableKindMulti } from '@agoric/vat-data'; const initialize = (name, imp, value) => { @@ -34,11 +35,11 @@ export const buildRootObject = (_vatPowers, vatParameters, baggage) => { if (baggage.has('mkh')) { const mkh = baggage.get('mkh'); if (vatParameters.mode === 'm2sFacetiousnessMismatch') { - defineDurableKind(mkh, () => ({}), { + defineDurableKind(mkh, initEmpty, { fooMethod: () => 2, }); } else { - defineDurableKindMulti(mkh, () => ({}), { + defineDurableKindMulti(mkh, initEmpty, { bar: { barMethod: () => 2, }, diff --git a/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js b/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js index a7621589efa..2da82ad799d 100644 --- a/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js +++ b/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js @@ -1,7 +1,8 @@ +// eslint-disable-next-line import/order import { test } from '../../tools/prepare-test-env-ava.js'; -// eslint-disable-next-line import/order import { Remotable } from '@endo/marshal'; +import { initEmpty } from '@agoric/store'; import { makeVatSlot } from '../../src/lib/parseVatSlots.js'; import { makeFakeVirtualStuff } from '../../tools/fakeVirtualSupport.js'; @@ -14,7 +15,7 @@ test('VOM tracks reachable vrefs', async t => { const weakStore = makeScalarBigWeakMapStore('test'); // empty object, used as weap map store key - const makeKey = defineKind('key', () => ({}), {}); + const makeKey = defineKind('key', initEmpty, {}); const makeHolder = defineKind('holder', held => ({ held }), { setHeld: ({ state }, held) => { state.held = held; diff --git a/packages/SwingSet/test/virtualObjects/test-retain-remotable.js b/packages/SwingSet/test/virtualObjects/test-retain-remotable.js index d3699db70be..9217f11d8ad 100644 --- a/packages/SwingSet/test/virtualObjects/test-retain-remotable.js +++ b/packages/SwingSet/test/virtualObjects/test-retain-remotable.js @@ -1,8 +1,9 @@ /* global WeakRef */ +// eslint-disable-next-line import/order import { test } from '../../tools/prepare-test-env-ava.js'; -// eslint-disable-next-line import/order import { Far } from '@endo/marshal'; +import { initEmpty } from '@agoric/store'; import engineGC from '../../src/lib-nodejs/engine-gc.js'; import { makeGcAndFinalize } from '../../src/lib-nodejs/gc-and-finalize.js'; @@ -60,7 +61,7 @@ test('remotables retained by virtualized data', async t => { const { makeScalarBigWeakMapStore } = cm; const weakStore = makeScalarBigWeakMapStore('ws'); // empty object, used as weak map store key - const makeKey = defineKind('key', () => ({}), {}); + const makeKey = defineKind('key', initEmpty, {}); const makeHolder = defineKind('holder', held => ({ held }), { setHeld: ({ state }, held) => { state.held = held; diff --git a/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js b/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js index 94216bb7dea..7d2661415a0 100644 --- a/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js +++ b/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js @@ -1,4 +1,5 @@ import { Far } from '@endo/marshal'; +import { initEmpty } from '@agoric/store'; import { defineKindMulti } from '@agoric/vat-data'; export function buildRootObject(vatPowers) { @@ -6,7 +7,7 @@ export function buildRootObject(vatPowers) { let extracted; - const makeThing = defineKindMulti('thing', () => ({}), { + const makeThing = defineKindMulti('thing', initEmpty, { regularFacet: { statelessMethod: () => 0, extractState: ({ state }) => { diff --git a/packages/assert/src/assert.js b/packages/assert/src/assert.js index 15a90dccb42..3a239ce85f5 100644 --- a/packages/assert/src/assert.js +++ b/packages/assert/src/assert.js @@ -20,6 +20,8 @@ // but we need to import it here as well. import './types.js'; +/** @typedef {import('@endo/marshal').Checker} Checker */ + const { freeze } = Object; /** @type {Assert} */ @@ -89,3 +91,16 @@ function an(str) { } freeze(an); export { an }; + +/** + * In the `assertFoo`/`isFoo`/`checkFoo` pattern, `checkFoo` has a `check` + * parameter of type `Checker`. `assertFoo` calls `checkFoo` passes + * `assertChecker` as the `check` argument. `isFoo` passes `identChecker` + * as the `check` argument. `identChecker` acts precisely like an + * identity function, but is typed as a `Checker` to indicate its + * intended use. + * + * @type {Checker} + */ +export const identChecker = (cond, _details) => cond; +harden(identChecker); diff --git a/packages/inter-protocol/src/psm/psm.js b/packages/inter-protocol/src/psm/psm.js index f96e1910c8d..3843fbcc889 100644 --- a/packages/inter-protocol/src/psm/psm.js +++ b/packages/inter-protocol/src/psm/psm.js @@ -10,6 +10,7 @@ import { floorMultiplyBy, } from '@agoric/zoe/src/contractSupport/index.js'; import { Far } from '@endo/marshal'; +import { initEmpty } from '@agoric/store'; import { handleParamGovernance, ParamTypes } from '@agoric/governance'; import { provide, vivifyKindMulti, M } from '@agoric/vat-data'; import { AmountMath } from '@agoric/ertp'; @@ -30,13 +31,18 @@ const { details: X } = assert; /** * @typedef {object} MetricsNotification - * Metrics naming scheme is that nouns are present values and past-participles are accumulative. + * Metrics naming scheme is that nouns are present values and past-participles + * are accumulative. * - * @property {Amount<'nat'>} anchorPoolBalance amount of Anchor token available to be swapped - * @property {Amount<'nat'>} feePoolBalance amount of Stable token fees available to be collected + * @property {Amount<'nat'>} anchorPoolBalance amount of Anchor token + * available to be swapped + * @property {Amount<'nat'>} feePoolBalance amount of Stable token + * fees available to be collected * - * @property {Amount<'nat'>} totalAnchorProvided running sum of Anchor ever given by this contract - * @property {Amount<'nat'>} totalStableProvided running sum of Stable ever given by this contract + * @property {Amount<'nat'>} totalAnchorProvided running sum of Anchor + * ever given by this contract + * @property {Amount<'nat'>} totalStableProvided running sum of Stable + * ever given by this contract */ /** @@ -274,7 +280,7 @@ export const start = async (zcf, privateArgs, baggage) => { const { limitedCreatorFacet, governorFacet } = // @ts-expect-error over-determined decl of creatorFacet makeVirtualGovernorFacet(creatorFacet); - const makePSM = vivifyKindMulti(baggage, 'PSM', () => ({}), { + const makePSM = vivifyKindMulti(baggage, 'PSM', initEmpty, { creatorFacet: governorFacet, limitedCreatorFacet, publicFacet: augmentVirtualPublicFacet(publicFacet), diff --git a/packages/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js b/packages/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js index d14a5b0bdb7..8b3856eb686 100644 --- a/packages/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js +++ b/packages/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js @@ -11,6 +11,7 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/far'; import { Far } from '@endo/marshal'; +import { initEmpty } from '@agoric/store'; import { provideDurableMapStore, provideDurableWeakMapStore, @@ -483,7 +484,7 @@ const start = async (zcf, privateArgs, baggage) => { }), ); - const makeAMM = vivifyKindMulti(baggage, 'AMM', () => ({}), { + const makeAMM = vivifyKindMulti(baggage, 'AMM', initEmpty, { publicFacet, creatorFacet: governorFacet, limitedCreatorFacet, diff --git a/packages/store/src/index.js b/packages/store/src/index.js index 294ca4e168a..c8a330437c2 100755 --- a/packages/store/src/index.js +++ b/packages/store/src/index.js @@ -55,7 +55,7 @@ export { matches, fit, } from './patterns/patternMatchers.js'; -export { defendPrototype } from './patterns/interface-tools.js'; +export { defendPrototype, initEmpty } from './patterns/interface-tools.js'; export { compareRank, isRankSorted, sortByRank } from './patterns/rankOrder.js'; export { makeDecodePassable, diff --git a/packages/store/src/keys/checkKey.js b/packages/store/src/keys/checkKey.js index 64704465466..f095c3188a1 100644 --- a/packages/store/src/keys/checkKey.js +++ b/packages/store/src/keys/checkKey.js @@ -11,6 +11,8 @@ import { makeTagged, passStyleOf, } from '@endo/marshal'; +import { identChecker } from '@agoric/assert'; + import { checkElements, makeSetOfElements } from './copySet.js'; import { checkBagEntries, makeBagOfEntries } from './copyBag.js'; import { @@ -43,7 +45,7 @@ const checkPrimitiveKey = (val, check) => { * @param {Passable} val * @returns {boolean} */ -export const isPrimitiveKey = val => checkPrimitiveKey(val, x => x); +export const isPrimitiveKey = val => checkPrimitiveKey(val, identChecker); harden(isPrimitiveKey); /** @@ -75,7 +77,7 @@ export const checkScalarKey = (val, check) => { * @param {Passable} val * @returns {boolean} */ -export const isScalarKey = val => checkScalarKey(val, x => x); +export const isScalarKey = val => checkScalarKey(val, identChecker); harden(isScalarKey); /** @@ -127,7 +129,7 @@ harden(checkKey); * @param {Passable} val * @returns {boolean} */ -export const isKey = val => checkKey(val, x => x); +export const isKey = val => checkKey(val, identChecker); harden(isKey); /** @@ -176,7 +178,7 @@ harden(checkCopySet); */ /** @type {IsCopySet} */ -export const isCopySet = s => checkCopySet(s, x => x); +export const isCopySet = s => checkCopySet(s, identChecker); harden(isCopySet); /** @@ -262,7 +264,7 @@ harden(checkCopyBag); */ /** @type {IsCopyBag} */ -export const isCopyBag = b => checkCopyBag(b, x => x); +export const isCopyBag = b => checkCopyBag(b, identChecker); harden(isCopyBag); /** @@ -389,7 +391,7 @@ harden(checkCopyMap); */ /** @type {IsCopyMap} */ -export const isCopyMap = m => checkCopyMap(m, x => x); +export const isCopyMap = m => checkCopyMap(m, identChecker); harden(isCopyMap); /** diff --git a/packages/store/src/patterns/interface-tools.js b/packages/store/src/patterns/interface-tools.js index 68cbbbcfbd7..f994a908f74 100644 --- a/packages/store/src/patterns/interface-tools.js +++ b/packages/store/src/patterns/interface-tools.js @@ -63,6 +63,10 @@ const isAwaitArgGuard = argGuard => const desync = methodGuard => { const { argGuards, optionalArgGuards = [], restArgGuard } = methodGuard; + assert( + !isAwaitArgGuard(restArgGuard), + X`Rest args may not be awaited: ${restArgGuard}`, + ); const rawArgGuards = [...argGuards, ...optionalArgGuards]; const awaitIndexes = []; @@ -234,3 +238,16 @@ export const defendPrototype = ( return Far(tag, prototype); }; harden(defendPrototype); + +const emptyRecord = harden({}); + +/** + * When calling `defineDurableKind` and + * its siblings, used as the `init` function argument to indicate that the + * state record of the (virtual/durable) instances of the kind/farClass + * should be empty, and that the returned maker function should have zero + * parameters. + * + * @returns {{}} + */ +export const initEmpty = () => emptyRecord; diff --git a/packages/store/src/patterns/patternMatchers.js b/packages/store/src/patterns/patternMatchers.js index 149f744c280..44634d4bfec 100644 --- a/packages/store/src/patterns/patternMatchers.js +++ b/packages/store/src/patterns/patternMatchers.js @@ -9,7 +9,9 @@ import { passStyleOf, hasOwnPropertyOf, } from '@endo/marshal'; +import { identChecker } from '@agoric/assert'; import { applyLabelingError, listDifference } from '@agoric/internal'; + import { compareRank, getPassStyleCover, @@ -112,7 +114,7 @@ const makePatternKit = () => { ); return check( false, - X`A ${q(tag)} must be a Key but was not: ${patt}`, + X`A ${q(tag)} - Must be a Key but was not: ${patt}`, ); } case 'copyMap': { @@ -153,7 +155,7 @@ const makePatternKit = () => { * @param {Passable} patt * @returns {boolean} */ - const isPattern = patt => checkPattern(patt, x => x); + const isPattern = patt => checkPattern(patt, identChecker); /** * @param {Pattern} patt @@ -227,7 +229,7 @@ const makePatternKit = () => { * @param {Passable} patt * @returns {boolean} */ - const isKeyPattern = patt => checkKeyPattern(patt, x => x); + const isKeyPattern = patt => checkKeyPattern(patt, identChecker); /** * @param {Pattern} patt @@ -262,11 +264,11 @@ const makePatternKit = () => { return check(keyEQ(specimen, patt), X`${specimen} - Must be: ${patt}`); } assertPattern(patt); - const specStyle = passStyleOf(specimen); + const specimenStyle = passStyleOf(specimen); const pattStyle = passStyleOf(patt); switch (pattStyle) { case 'copyArray': { - if (specStyle !== 'copyArray') { + if (specimenStyle !== 'copyArray') { return check( false, X`${specimen} - Must be a copyArray to match a copyArray pattern: ${patt}`, @@ -282,34 +284,32 @@ const makePatternKit = () => { return patt.every((p, i) => checkMatches(specimen[i], p, check, i)); } case 'copyRecord': { - if (specStyle !== 'copyRecord') { + if (specimenStyle !== 'copyRecord') { return check( false, X`${specimen} - Must be a copyRecord to match a copyRecord pattern: ${patt}`, ); } - const [specNames, specValues] = recordParts(specimen); + const [specimenNames, specimenValues] = recordParts(specimen); const [pattNames, pattValues] = recordParts(patt); - { - const missing = listDifference(pattNames, specNames); - if (missing.length >= 1) { - return check( - false, - X`${specimen} - Must have missing properties ${q(missing)}`, - ); - } - const unexpected = listDifference(specNames, pattNames); - if (unexpected.length >= 1) { - return check( - false, - X`${specimen} - Must not have unexpected properties: ${q( - unexpected, - )}`, - ); - } + const missing = listDifference(pattNames, specimenNames); + if (missing.length >= 1) { + return check( + false, + X`${specimen} - Must have missing properties ${q(missing)}`, + ); + } + const unexpected = listDifference(specimenNames, pattNames); + if (unexpected.length >= 1) { + return check( + false, + X`${specimen} - Must not have unexpected properties: ${q( + unexpected, + )}`, + ); } return pattNames.every((label, i) => - checkMatches(specValues[i], pattValues[i], check, label), + checkMatches(specimenValues[i], pattValues[i], check, label), ); } case 'tagged': { @@ -318,16 +318,16 @@ const makePatternKit = () => { if (matchHelper) { return matchHelper.checkMatches(specimen, patt.payload, check); } - if (specStyle !== 'tagged' || getTag(specimen) !== pattTag) { + const specimenTag = + specimenStyle === 'tagged' ? getTag(specimen) : undefined; + if (specimenStyle !== 'tagged' || specimenTag !== pattTag) { return check( false, - X`${specimen} - Only a ${q(pattTag)} matches a ${q( - pattTag, - )} pattern: ${patt}`, + X`${specimen} - Must be tagged as a ${pattTag}: ${specimenTag}`, ); } const { payload: pattPayload } = patt; - const { payload: specPayload } = specimen; + const { payload: specimenPayload } = specimen; switch (pattTag) { case 'copySet': case 'copyBag': { @@ -344,15 +344,15 @@ const makePatternKit = () => { return false; } const pattKeySet = copyMapKeySet(pattPayload); - const specKeySet = copyMapKeySet(specPayload); + const specimenKeySet = copyMapKeySet(specimenPayload); // Compare keys as copySets - if (checkMatches(specKeySet, pattKeySet, check)) { + if (checkMatches(specimenKeySet, pattKeySet, check)) { return false; } const pattValues = pattPayload.values; - const specValues = specPayload.values; + const specimenValues = specimenPayload.values; // compare values as copyArrays - return checkMatches(specValues, pattValues, check); + return checkMatches(specimenValues, pattValues, check); } default: { assert.fail(X`Unexpected tag ${q(pattTag)}`); @@ -370,7 +370,8 @@ const makePatternKit = () => { * @param {Pattern} patt * @returns {boolean} */ - const matches = (specimen, patt) => checkMatches(specimen, patt, x => x); + const matches = (specimen, patt) => + checkMatches(specimen, patt, identChecker); /** * Returning normally indicates success. Match failure is indicated by @@ -563,13 +564,13 @@ const makePatternKit = () => { // /////////////////////// Match Helpers ///////////////////////////////////// /** @type {MatchHelper} */ - const matchAnyHelper = Far('match.any helper', { + const matchAnyHelper = Far('match:any helper', { checkMatches: (_specimen, _matcherPayload, _check) => true, checkIsMatcherPayload: (matcherPayload, check) => check( matcherPayload === undefined, - X`Payload must be undefined: ${matcherPayload}`, + X`match:any payload: ${matcherPayload} - Must be undefined`, ), getRankCover: (_matchPayload, _encodePassable) => ['', '{'], @@ -639,7 +640,7 @@ const makePatternKit = () => { if (matches(specimen, patt)) { return check( false, - X`${specimen} - must fail negated pattern: ${patt}`, + X`${specimen} - Must fail negated pattern: ${patt}`, ); } else { return true; @@ -654,7 +655,7 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchScalarHelper = Far('M.scalar helper', { + const matchScalarHelper = Far('match:scalar helper', { checkMatches: (specimen, _matcherPayload, check) => checkScalarKey(specimen, check), @@ -666,7 +667,7 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchKeyHelper = Far('M.key helper', { + const matchKeyHelper = Far('match:key helper', { checkMatches: (specimen, _matcherPayload, check) => checkKey(specimen, check), @@ -678,7 +679,7 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchPatternHelper = Far('M.pattern helper', { + const matchPatternHelper = Far('match:pattern helper', { checkMatches: (specimen, _matcherPayload, check) => checkPattern(specimen, check), @@ -690,13 +691,13 @@ const makePatternKit = () => { }); /** @type {MatchHelper} */ - const matchKindHelper = Far('M.kind helper', { + const matchKindHelper = Far('match:kind helper', { checkMatches: checkKind, checkIsMatcherPayload: (allegedKeyKind, check) => check( typeof allegedKeyKind === 'string', - X`A kind name must be a string: ${allegedKeyKind}`, + X`match:kind: payload: ${allegedKeyKind} - A kind name must be a string`, ), getRankCover: (kind, _encodePassable) => { @@ -735,7 +736,7 @@ const makePatternKit = () => { const matchRemotableHelper = Far('match:remotable helper', { checkMatches: (specimen, remotableDesc, check) => { // Unfortunate duplication of checkKind logic, but no better choices. - if (checkKind(specimen, 'remotable', x => x)) { + if (checkKind(specimen, 'remotable', identChecker)) { return true; } let specimenKind = passStyleOf(specimen); @@ -763,7 +764,7 @@ const makePatternKit = () => { getRankCover: (_remotableDesc, _encodePassable) => getPassStyleCover('remotable'), - checkKeyPattern: (_remotableDesc, _check = x => x) => true, + checkKeyPattern: (_remotableDesc, _check) => true, }); /** @type {MatchHelper} */ @@ -1025,10 +1026,10 @@ const makePatternKit = () => { let specR; let newBase; if (baseStyle === 'copyArray') { - const { length: specLen } = specimen; + const { length: specimenLen } = specimen; const { length: baseLen } = base; - if (specLen < baseLen) { - newBase = harden(base.slice(0, specLen)); + if (specimenLen < baseLen) { + newBase = harden(base.slice(0, specimenLen)); specB = specimen; // eslint-disable-next-line no-use-before-define specR = []; diff --git a/packages/store/test/test-patterns.js b/packages/store/test/test-patterns.js index 527faf60c83..8dab91690d5 100644 --- a/packages/store/test/test-patterns.js +++ b/packages/store/test/test-patterns.js @@ -36,8 +36,8 @@ const matchTests = harden([ ], noPatterns: [ [4, '3 - Must be: 4'], - [M.not(3), '3 - must fail negated pattern: 3'], - [M.not(M.any()), '3 - must fail negated pattern: "[match:any]"'], + [M.not(3), '3 - Must fail negated pattern: 3'], + [M.not(M.any()), '3 - Must fail negated pattern: "[match:any]"'], [M.string(), 'number 3 - Must be a string'], [[3, 4], '3 - Must be: [3,4]'], [M.gte(7), '3 - Must be >= 7'], @@ -284,7 +284,7 @@ const matchTests = harden([ { specimen: makeTagged('match:any', 88), yesPatterns: [M.any(), M.not(M.pattern())], - noPatterns: [[M.pattern(), 'Payload must be undefined: 88']], + noPatterns: [[M.pattern(), 'match:any payload: 88 - Must be undefined']], }, { specimen: makeTagged('match:remotable', 88), diff --git a/packages/vat-data/src/far-class-utils.js b/packages/vat-data/src/far-class-utils.js index c74d1f1b89d..0c627c67558 100644 --- a/packages/vat-data/src/far-class-utils.js +++ b/packages/vat-data/src/far-class-utils.js @@ -1,3 +1,5 @@ +import { initEmpty } from '@agoric/store'; + import { provideKindHandle } from './kind-utils.js'; import { defineKind, @@ -64,7 +66,7 @@ export const defineVirtualFarClassKit = ( thisfulMethods: true, interfaceGuard: interfaceGuardKit, }); -harden(defineVirtualFarClass); +harden(defineVirtualFarClassKit); /** * @template A,S,T @@ -190,7 +192,7 @@ export const vivifyFarInstance = ( baggage, kindName, interfaceGuard, - () => ({}), + initEmpty, methods, options, ); diff --git a/packages/vat-data/src/index.js b/packages/vat-data/src/index.js index 91f915dc264..4d5c7bc0432 100644 --- a/packages/vat-data/src/index.js +++ b/packages/vat-data/src/index.js @@ -11,3 +11,5 @@ export { vivifyFarInstance, vivifySingleton, } from './far-class-utils.js'; + +/** @template T @typedef {import('./types.js').DefineKindOptions} DefineKindOptions */ diff --git a/packages/vat-data/src/kind-utils.js b/packages/vat-data/src/kind-utils.js index ca76aa6813a..9062e8a0ec2 100644 --- a/packages/vat-data/src/kind-utils.js +++ b/packages/vat-data/src/kind-utils.js @@ -5,11 +5,7 @@ import { makeKindHandle, } from './vat-data-bindings.js'; -/** @template L,R @typedef {import('@endo/eventual-send').RemotableBrand} RemotableBrand */ /** @typedef {import('./types.js').Baggage} Baggage */ -/** @template T @typedef {import('./types.js').DefineKindOptions} DefineKindOptions */ -/** @template T @typedef {import('./types.js').KindFacet} KindFacet */ -/** @template T @typedef {import('./types.js').KindFacets} KindFacets */ /** @typedef {import('./types.js').DurableKindHandle} DurableKindHandle */ /** diff --git a/packages/vat-data/src/types.d.ts b/packages/vat-data/src/types.d.ts index 7c08e576746..8de2fc794fa 100644 --- a/packages/vat-data/src/types.d.ts +++ b/packages/vat-data/src/types.d.ts @@ -42,10 +42,54 @@ declare class DurableKindHandleClass { } export type DurableKindHandle = DurableKindHandleClass; +/** + * Grab bag of options that can be provided to `defineDurableKind` and its + * siblings. Not all options are meaningful in all contexts. See the + * doc-comments on each option. + */ type DefineKindOptions = { + /** + * If provided, the `finish` function will be called after the instance is + * made and internally registered, but before it is returned. The finish + * function is to do any post-intantiation initialization that should be + * done before exposing the object to its clients. + */ finish?: (context: C) => void; + + /** + * Meaningful to `makeScalarBigMapStore` and its siblings. These maker + * fuctions will make either virtual or durable stores, depending on + * this flag. Defaults to off, making virtual but not durable collections. + * + * Generally, durable collections are provided with `provideDurableMapStore` + * and its sibling, which use this flag internally. If you do not make + * durable collections by other means, you can consider this as + * intended for internal use only. + */ durable?: boolean; + + /** + * Intended for internal use only. + * Should the raw methods receive their `context` argument as their first + * argument or as their `this` binding? For `defineDurableKind` and its + * siblings (including `vivifySingleton`), this defaults to off, meaning that + * their behavior methods receive `context` as their first argument. + * `vivifyFarClass` and its siblings (including `vivifyFarInstance`) use + * this flag internally to indicate that their methods receive `context` + * as their `this` binding. + */ thisfulMethods?: boolean; + + /** + * Intended for internal use only. + * If an `interfaceGuard` is provided, then the raw methods passed alongside + * it wrapped by a function that first checks that this method's guard + * pattern is satisfied before calling the raw method. + * + * In `defineDurableKind` and its siblings, this defaults to off. + * `vivifyFarClass` use this internally to protect their raw class methods + * using the provided interface. + */ interfaceGuard?: object; // TODO type }; diff --git a/packages/zoe/src/contractFacet/zcfZygote.js b/packages/zoe/src/contractFacet/zcfZygote.js index 4ae0496a442..42d3917a5e5 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -4,7 +4,7 @@ import { E } from '@endo/eventual-send'; import { Far, Remotable, passStyleOf } from '@endo/marshal'; import { AssetKind } from '@agoric/ertp'; import { makePromiseKit } from '@endo/promise-kit'; -import { assertPattern } from '@agoric/store'; +import { assertPattern, initEmpty } from '@agoric/store'; import { makeScalarBigMapStore, provideDurableMapStore, @@ -241,7 +241,7 @@ export const makeZCFZygote = async ( const makeHandleOfferObj = vivifyKind( zcfBaggage, 'handleOfferObj', - () => ({}), + initEmpty, { handleOffer: (_context, invitationHandle, seatData) => { const zcfSeat = makeZCFSeat(seatData); diff --git a/packages/zoe/src/makeHandle.js b/packages/zoe/src/makeHandle.js index ccf209c73e9..1aad9c06a94 100644 --- a/packages/zoe/src/makeHandle.js +++ b/packages/zoe/src/makeHandle.js @@ -1,6 +1,7 @@ // @ts-check import { assert } from '@agoric/assert'; +import { initEmpty } from '@agoric/store'; import { provide, defineDurableKind, makeKindHandle } from '@agoric/vat-data'; import { Far } from '@endo/marshal'; @@ -19,7 +20,7 @@ export const defineDurableHandle = (baggage, handleType) => { `${handleType}KindHandle`, () => makeKindHandle(`${handleType}Handle`), ); - const makeHandle = defineDurableKind(durableHandleKindHandle, () => ({}), {}); + const makeHandle = defineDurableKind(durableHandleKindHandle, initEmpty, {}); return /** @type {() => Handle} */ (makeHandle); }; harden(defineDurableHandle); diff --git a/packages/zoe/src/zoeService/feeMint.js b/packages/zoe/src/zoeService/feeMint.js index a972ddd7c88..52a34bf6e98 100644 --- a/packages/zoe/src/zoeService/feeMint.js +++ b/packages/zoe/src/zoeService/feeMint.js @@ -1,6 +1,7 @@ // @ts-check import { makeDurableIssuerKit, AssetKind } from '@agoric/ertp'; +import { initEmpty } from '@agoric/store'; import { vivifyKindMulti, provideDurableMapStore } from '@agoric/vat-data'; const FEE_MINT_KIT = 'FeeMintKit'; @@ -46,7 +47,7 @@ const vivifyFeeMint = (zoeBaggage, feeIssuerConfig, shutdownZoeVat) => { return mintBaggage.get(FEE_MINT_KIT); }; - const makeFeeMintKit = vivifyKindMulti(mintBaggage, 'FeeMint', () => ({}), { + const makeFeeMintKit = vivifyKindMulti(mintBaggage, 'FeeMint', initEmpty, { feeMint: { getFeeIssuerKit, getFeeMintAccess: ({ facets }) => facets.feeMintAccess, diff --git a/packages/zoe/src/zoeService/installationStorage.js b/packages/zoe/src/zoeService/installationStorage.js index 71edac7595b..fc3c1ab5e87 100644 --- a/packages/zoe/src/zoeService/installationStorage.js +++ b/packages/zoe/src/zoeService/installationStorage.js @@ -7,6 +7,7 @@ import { provideDurableWeakMapStore, vivifyKind, } from '@agoric/vat-data'; +import { initEmpty } from '@agoric/store'; /** @typedef { import('@agoric/swingset-vat').BundleID} BundleID */ /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ @@ -33,7 +34,7 @@ export const makeInstallationStorage = ( const makeBundleIDInstallation = vivifyKind( zoeBaggage, 'BundleIDInstallation', - () => ({}), + initEmpty, { getBundle: _context => assert.fail('bundleID-based Installation') }, );