Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ERTP/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
120 changes: 66 additions & 54 deletions packages/ERTP/src/amountMath.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -75,43 +76,50 @@ 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,
)}`;
};

/**
* Asserts that value is a valid AmountMath and returns the appropriate helpers.
*
* 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<V>}
*/
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) => {
Expand All @@ -127,15 +135,19 @@ const optionalBrandCheck = (allegedBrand, brand) => {
/**
* @template {AssetKind} K
* @param {Amount<K>} leftAmount
* @param {Amount<K>} rightAmount
* @param {AmountBound<K>} rightAmountBound
* @param {Brand<K> | undefined} brand
* @returns {MathHelpers<any>}
*/
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);
Expand All @@ -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<AssetValueForKind<K>>} h
* @param {Amount<K>} leftAmount
* @param {Amount<K>} rightAmount
* @param {AmountBound<K>} 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<K>} leftAmount
* @param {Amount<K>} rightAmount
* @param {AmountBound<K>} rightAmountBound
* @param {Brand<K>} [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));
};

/**
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
},
Expand All @@ -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,
Expand Down Expand Up @@ -387,6 +399,6 @@ harden(AmountMath);
export const getAssetKind = amount => {
assertRecord(amount, 'amount');
const { value } = amount;
return assertValueGetAssetKind(value);
return assertValueBoundGetAssetKind(value);
};
harden(getAssetKind);
23 changes: 22 additions & 1 deletion packages/ERTP/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
*/

Expand Down Expand Up @@ -68,13 +68,18 @@ const SetValueShape = M.arrayOf(M.key());
*/
const CopyBagValueShape = M.bag();

/**
* @see {AmountValue}
* @see {AssetValueForKind}
*/
const AmountValueShape = M.or(
NatValueShape,
CopySetValueShape,
SetValueShape,
CopyBagValueShape,
);

/** @see {Amount} */
export const AmountShape = { brand: BrandShape, value: AmountValueShape };
harden(AmountShape);

Expand All @@ -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<Ratio>} */
export const RatioShape = { numerator: AmountShape, denominator: AmountShape };
harden(RatioShape);
Expand Down
32 changes: 24 additions & 8 deletions packages/ERTP/src/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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`.
Expand All @@ -90,16 +87,35 @@ export type AssetValueForKind<
: K extends 'copyBag'
? CopyBag<M>
: never;

export type AssetKindForValue<V extends AmountValue> = 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<K, M> | AmountValueHasBound;

export type AmountBound<
K extends AssetKind = AssetKind,
M extends Key = Key,
> = {
brand: Brand<K>;
value: AmountValueBound<K, M>;
};

export type Ratio = { numerator: Amount<'nat'>; denominator: Amount<'nat'> };

/** @deprecated */
Expand Down
6 changes: 3 additions & 3 deletions packages/ERTP/test/unitTests/mintObj.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading