diff --git a/packages/ERTP/package.json b/packages/ERTP/package.json index f1356c5c4e8..8b301305465 100644 --- a/packages/ERTP/package.json +++ b/packages/ERTP/package.json @@ -48,6 +48,7 @@ "@endo/far": "^1.1.10", "@endo/marshal": "^1.6.3", "@endo/nat": "^5.0.14", + "@endo/pass-style": "^1.4.8", "@endo/patterns": "^1.4.8", "@endo/promise-kit": "^1.1.9" }, diff --git a/packages/ERTP/src/amountMath.js b/packages/ERTP/src/amountMath.js index 33ba194495d..a9d919cb064 100644 --- a/packages/ERTP/src/amountMath.js +++ b/packages/ERTP/src/amountMath.js @@ -1,15 +1,16 @@ import { q, Fail } from '@endo/errors'; -import { passStyleOf, assertRemotable, assertRecord } from '@endo/marshal'; +import { assertRemotable, assertRecord } from '@endo/pass-style'; +import { kindOf } from '@endo/patterns'; -import { M, matches } from '@agoric/store'; import { natMathHelpers } from './mathHelpers/natMathHelpers.js'; import { setMathHelpers } from './mathHelpers/setMathHelpers.js'; import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js'; import { copyBagMathHelpers } from './mathHelpers/copyBagMathHelpers.js'; /** + * @import {Passable} from '@endo/pass-style' * @import {CopyBag, CopySet} from '@endo/patterns'; - * @import {Amount, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js'; + * @import {Amount, AmountBound, AmountValueBound, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js'; */ // NB: AssetKind is both a constant for enumerated values and a type for those values. @@ -75,29 +76,35 @@ const helpers = { copyBag: copyBagMathHelpers, }; -/** @type {(value: unknown) => 'nat' | 'set' | 'copySet' | 'copyBag'} } */ -const assertValueGetAssetKind = value => { - const passStyle = passStyleOf(value); - if (passStyle === 'bigint') { - return 'nat'; - } - if (passStyle === 'copyArray') { - return 'set'; - } - if (matches(value, M.set())) { - return 'copySet'; - } - if (matches(value, M.bag())) { - return 'copyBag'; +/** + * @template {AmountValueBound} V=AmountValueBound + * @param {V} value + * @param {AssetKind} [defaultKind] + * @returns {AssetKind} + */ +const assertValueBoundGetAssetKind = (value, defaultKind = undefined) => { + const kind = kindOf(value); + switch (kind) { + case 'bigint': { + return 'nat'; + } + case 'copyArray': { + return 'set'; + } + case 'copySet': + case 'copyBag': { + return kind; + } + case 'match:has': { + if (defaultKind === undefined) { + throw Fail`defaultKind expected for right has-bound ${value}`; + } + return defaultKind; + } + default: { + throw Fail`value ${value} must be an AmountValueBound, not ${q(kind)}`; + } } - // TODO This isn't quite the right error message, in case valuePassStyle - // is 'tagged'. We would need to distinguish what kind of tagged - // object it is. - // Also, this kind of manual listing is a maintenance hazard we - // (TODO) will encounter when we extend the math helpers further. - throw Fail`value ${value} must be a bigint, copySet, copyBag, or an array, not ${q( - passStyle, - )}`; }; /** @@ -105,13 +112,14 @@ const assertValueGetAssetKind = value => { * * Made available only for testing, but it is harmless for other uses. * - * @template V + * @template {AmountValueBound} V=AmountValueBound * @param {V} value + * @param {AssetKind} [defaultKind] * @returns {MathHelpers} */ -export const assertValueGetHelpers = value => +export const assertValueBoundGetHelpers = (value, defaultKind = undefined) => // @ts-expect-error cast - helpers[assertValueGetAssetKind(value)]; + helpers[assertValueBoundGetAssetKind(value, defaultKind)]; /** @type {(allegedBrand: Brand, brand?: Brand) => void} */ const optionalBrandCheck = (allegedBrand, brand) => { @@ -127,15 +135,19 @@ const optionalBrandCheck = (allegedBrand, brand) => { /** * @template {AssetKind} K * @param {Amount} leftAmount - * @param {Amount} rightAmount + * @param {AmountBound} rightAmountBound * @param {Brand | undefined} brand * @returns {MathHelpers} */ -const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => { +const checkLRAndGetHelpers = ( + leftAmount, + rightAmountBound, + brand = undefined, +) => { assertRecord(leftAmount, 'leftAmount'); - assertRecord(rightAmount, 'rightAmount'); - const { value: leftValue, brand: leftBrand } = leftAmount; - const { value: rightValue, brand: rightBrand } = rightAmount; + assertRecord(rightAmountBound, 'rightAmountBound'); + const { brand: leftBrand, value: leftValue } = leftAmount; + const { brand: rightBrand, value: rightValueBound } = rightAmountBound; assertRemotable(leftBrand, 'leftBrand'); assertRemotable(rightBrand, 'rightBrand'); optionalBrandCheck(leftBrand, brand); @@ -144,41 +156,41 @@ const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => { Fail`Brands in left ${q(leftBrand)} and right ${q( rightBrand, )} should match but do not`; - const leftHelpers = assertValueGetHelpers(leftValue); - const rightHelpers = assertValueGetHelpers(rightValue); - leftHelpers === rightHelpers || - Fail`The left ${leftAmount} and right amount ${rightAmount} had different assetKinds`; - return leftHelpers; + const leftKind = assertValueBoundGetAssetKind(leftValue); + const rightKind = assertValueBoundGetAssetKind(rightValueBound, leftKind); + leftKind === rightKind || + Fail`The left ${leftAmount} and right amount ${rightAmountBound} had different assetKinds: ${q(leftKind)} vs ${q(rightKind)}`; + return helpers[leftKind]; }; /** * @template {AssetKind} K * @param {MathHelpers>} h * @param {Amount} leftAmount - * @param {Amount} rightAmount + * @param {AmountBound} rightAmountBound * @returns {[K, K]} */ -const coerceLR = (h, leftAmount, rightAmount) => { +const coerceLR = (h, leftAmount, rightAmountBound) => { // @ts-expect-error could be arbitrary subtype - return [h.doCoerce(leftAmount.value), h.doCoerce(rightAmount.value)]; + return [h.doCoerce(leftAmount.value), h.doCoerce(rightAmountBound.value)]; }; /** - * Returns true if the leftAmount is greater than or equal to the rightAmount. - * The notion of "greater than or equal to" depends on the kind of amount, as - * defined by the MathHelpers. For example, whether rectangle A is greater than - * rectangle B depends on whether rectangle A includes rectangle B as defined by - * the logic in MathHelpers. + * Returns true if the leftAmount is greater than or equal to the + * rightAmountBound. The notion of "greater than or equal to" depends on the + * kind of amount, as defined by the MathHelpers. For example, whether rectangle + * A is greater than rectangle B depends on whether rectangle A includes + * rectangle B as defined by the logic in MathHelpers. * * @template {AssetKind} K * @param {Amount} leftAmount - * @param {Amount} rightAmount + * @param {AmountBound} rightAmountBound * @param {Brand} [brand] * @returns {boolean} */ -const isGTE = (leftAmount, rightAmount, brand = undefined) => { - const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); - return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount)); +const isGTE = (leftAmount, rightAmountBound, brand = undefined) => { + const h = checkLRAndGetHelpers(leftAmount, rightAmountBound, brand); + return h.doIsGTE(...coerceLR(h, leftAmount, rightAmountBound)); }; /** @@ -215,7 +227,7 @@ export const AmountMath = { */ make: (brand, allegedValue) => { assertRemotable(brand, 'brand'); - const h = assertValueGetHelpers(allegedValue); + const h = assertValueBoundGetHelpers(allegedValue); const value = h.doCoerce(allegedValue); // @ts-expect-error cast return harden({ brand, value }); @@ -275,7 +287,7 @@ export const AmountMath = { makeEmptyFromAmount: amount => { assertRecord(amount, 'amount'); const { brand, value } = amount; - const assetKind = assertValueGetAssetKind(value); + const assetKind = assertValueBoundGetAssetKind(value); // @ts-expect-error different subtype return AmountMath.makeEmpty(brand, assetKind); }, @@ -291,7 +303,7 @@ export const AmountMath = { const { brand: allegedBrand, value } = amount; assertRemotable(allegedBrand, 'brand'); optionalBrandCheck(allegedBrand, brand); - const h = assertValueGetHelpers(value); + const h = assertValueBoundGetHelpers(value); return h.doIsEmpty(h.doCoerce(value)); }, isGTE, @@ -387,6 +399,6 @@ harden(AmountMath); export const getAssetKind = amount => { assertRecord(amount, 'amount'); const { value } = amount; - return assertValueGetAssetKind(value); + return assertValueBoundGetAssetKind(value); }; harden(getAssetKind); diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index f66267ec0d2..41bf167c7fd 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -2,7 +2,7 @@ import { M, matches, getInterfaceGuardPayload } from '@endo/patterns'; /** - * @import {AmountValue, Ratio} from './types.js' + * @import {AmountValue, Amount, AmountValueHasBound, AmountValueBound, AmountBound, Ratio} from './types.js' * @import {TypedPattern} from '@agoric/internal' */ @@ -68,6 +68,10 @@ const SetValueShape = M.arrayOf(M.key()); */ const CopyBagValueShape = M.bag(); +/** + * @see {AmountValue} + * @see {AssetValueForKind} + */ const AmountValueShape = M.or( NatValueShape, CopySetValueShape, @@ -75,6 +79,7 @@ const AmountValueShape = M.or( CopyBagValueShape, ); +/** @see {Amount} */ export const AmountShape = { brand: BrandShape, value: AmountValueShape }; harden(AmountShape); @@ -91,6 +96,22 @@ harden(AmountShape); */ export const AmountPatternShape = M.pattern(); +/** @see {AmountValueHasBound} */ +const AmountValueHasBoundShape = M.tagged( + 'match:has', + M.splitArray([M.pattern(), M.bigint()], [M.record()]), +); + +/** @see {AmountValueBound} */ +const AmountValueBoundShape = M.or(AmountValueShape, AmountValueHasBoundShape); + +/** @see {AmountBound} */ +export const AmountBoundShape = { + brand: BrandShape, + value: AmountValueBoundShape, +}; +harden(AmountBoundShape); + /** @type {TypedPattern} */ export const RatioShape = { numerator: AmountShape, denominator: AmountShape }; harden(RatioShape); diff --git a/packages/ERTP/src/types.ts b/packages/ERTP/src/types.ts index 4e8a28785c5..207d23c9d94 100644 --- a/packages/ERTP/src/types.ts +++ b/packages/ERTP/src/types.ts @@ -1,7 +1,7 @@ import type { LatestTopic } from '@agoric/notifier'; import type { ERef } from '@endo/far'; -import type { RemotableObject } from '@endo/pass-style'; -import type { CopyBag, CopySet, Key, Pattern } from '@endo/patterns'; +import type { RemotableObject, CopyTagged } from '@endo/pass-style'; +import type { CopyBag, CopySet, Key, Pattern, Limits } from '@endo/patterns'; import type { TypeTag } from '@agoric/internal/src/tagged.js'; import type { AssetKind } from './amountMath.js'; @@ -69,11 +69,8 @@ export type Amount< * once or more times, i.e., some positive bigint number of times, * representing that quantity of the asset represented by that key. */ -export type AmountValue = - | NatValue - | SetValue - | CopySet - | import('@endo/patterns').CopyBag; +export type AmountValue = NatValue | SetValue | CopySet | CopyBag; + /** * See doc-comment * for `AmountValue`. @@ -90,16 +87,35 @@ export type AssetValueForKind< : K extends 'copyBag' ? CopyBag : never; + export type AssetKindForValue = V extends NatValue ? 'nat' : V extends SetValue ? 'set' : V extends CopySet ? 'copySet' - : V extends import('@endo/patterns').CopyBag + : V extends CopyBag ? 'copyBag' : never; +export type AmountValueHasBound = CopyTagged< + 'match:has', + [elementPatt: Pattern, countPatt: bigint, limits?: Limits] +>; + +export type AmountValueBound< + K extends AssetKind = AssetKind, + M extends Key = Key, +> = AssetValueForKind | AmountValueHasBound; + +export type AmountBound< + K extends AssetKind = AssetKind, + M extends Key = Key, +> = { + brand: Brand; + value: AmountValueBound; +}; + export type Ratio = { numerator: Amount<'nat'>; denominator: Amount<'nat'> }; /** @deprecated */ diff --git a/packages/ERTP/test/unitTests/mintObj.test.js b/packages/ERTP/test/unitTests/mintObj.test.js index 0b03fc4b413..80909299d31 100644 --- a/packages/ERTP/test/unitTests/mintObj.test.js +++ b/packages/ERTP/test/unitTests/mintObj.test.js @@ -124,9 +124,9 @@ test('non-fungible tokens example', async t => { mint: balletTicketMint, issuer: balletTicketIssuer, brand, - } = /** - * @type {IssuerKit<'set', { seat: number; show: string; start: string }>} - */ (makeIssuerKit('Agoric Ballet Opera tickets', AssetKind.SET)); + } = /** @type {IssuerKit<'set', { seat: number; show: string; start: string }>} */ ( + makeIssuerKit('Agoric Ballet Opera tickets', AssetKind.SET) + ); const startDateString = new Date(2020, 1, 17, 20, 30).toISOString();