Skip to content

Commit 20b287d

Browse files
committed
fixup! refactor amountMath vs helpers
1 parent 12de3f0 commit 20b287d

File tree

11 files changed

+217
-122
lines changed

11 files changed

+217
-122
lines changed

packages/ERTP/src/amountMath.js

+75-50
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { q, Fail } from '@endo/errors';
2-
import { assertRemotable, assertRecord } from '@endo/pass-style';
3-
import { isKey, kindOf, mustMatch } from '@endo/patterns';
2+
import { assertRemotable, assertRecord, assertChecker } from '@endo/pass-style';
3+
import { containerHasSplit, kindOf, mustMatch } from '@endo/patterns';
44

55
import { natMathHelpers } from './mathHelpers/natMathHelpers.js';
66
import { setMathHelpers } from './mathHelpers/setMathHelpers.js';
77
import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js';
88
import { copyBagMathHelpers } from './mathHelpers/copyBagMathHelpers.js';
9-
import { AmountBoundShape, AmountShape } from './typeGuards.js';
9+
import { AmountShape } from './typeGuards.js';
1010

1111
/**
1212
* @import {Passable} from '@endo/pass-style'
1313
* @import {Key, CopyBag, CopySet} from '@endo/patterns';
14-
* @import {Amount, AmountBound, AmountValue, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
14+
* @import {Amount, AmountBound, AmountHasBound, AmountValue, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
1515
*/
1616

1717
// NB: AssetKind is both a constant for enumerated values and a type for those values.
@@ -183,13 +183,13 @@ const coerceLR = (h, leftAmount, rightAmount) => {
183183
* For non-fungible or sem-fungible amounts, the right operand can also be an
184184
* `AmountBound` which can a normal concrete `Amount` or a specialized pattern:
185185
* A `RecordPattern` of a normal concrete `brand: Brand` and a `value:
186-
* AmountValueHasBound`, as made by `M.has(elementPattern)` or
187-
* `M.has(elementPattern, bigint)`. This represents those elements of the value
188-
* collection that match the elementPattern, if that number is exactly the same
189-
* as the bigint argument. If the second argument of `M.has` is omitted, it
190-
* defaults to `1n`. IOW, the left operand is `>=` such a bound if the total
191-
* number of elements in the left operand that match the element pattern is `>=`
192-
* the bigint argument in the `M.has` pattern.
186+
* HasBound`, as made by `M.containerHas(elementPattern)` or
187+
* `M.containerHas(elementPattern, bigint)`. This represents those elements of
188+
* the value collection that match the elementPattern, if that number is exactly
189+
* the same as the bigint argument. If the second argument of `M.containerHas`
190+
* is omitted, it defaults to `1n`. IOW, the left operand is `>=` such a bound
191+
* if the total number of elements in the left operand that match the element
192+
* pattern is `>=` the bigint argument in the `M.containerHas` pattern.
193193
*
194194
* @template {AssetKind} K
195195
* @param {Amount<K>} leftAmount
@@ -198,26 +198,33 @@ const coerceLR = (h, leftAmount, rightAmount) => {
198198
* @returns {boolean}
199199
*/
200200
const isGTE = (leftAmount, rightAmountBound, brand = undefined) => {
201-
if (isKey(rightAmountBound)) {
202-
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
203-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
204-
// @ts-expect-error cast?
205-
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
201+
if (kindOf(rightAmountBound) === 'match:containerHas') {
202+
mustMatch(leftAmount, AmountShape, 'left amount');
203+
const { brand: leftBrand, value: leftValue } = leftAmount;
204+
const {
205+
brand: rightBrand,
206+
value: {
207+
payload: [elementPatt, bound],
208+
},
209+
} = /** @type {AmountHasBound} */ (rightAmountBound);
210+
optionalBrandCheck(leftBrand, brand);
211+
optionalBrandCheck(rightBrand, brand);
212+
leftBrand === rightBrand ||
213+
Fail`Brands in left ${q(leftBrand)} and right ${q(
214+
rightBrand,
215+
)} should match but do not`;
216+
const leftKind = assertValueGetAssetKind(leftValue);
217+
leftKind !== 'nat' ||
218+
Fail`can only use M.containerHas on container assets, not nat: ${leftValue}`;
219+
const h = helpers[leftKind];
220+
// @ts-expect-error param type of doCoerce should not be never
221+
const lv = h.doCoerce(leftValue);
222+
return !!containerHasSplit(lv, elementPatt, bound);
206223
}
207-
mustMatch(leftAmount, AmountShape, 'left amount');
208-
mustMatch(rightAmountBound, AmountBoundShape, 'right amount bound');
209-
const { brand: leftBrand, value: leftValue } = leftAmount;
210-
const { brand: rightBrand, value: rightValueHasBound } = rightAmountBound;
211-
optionalBrandCheck(leftBrand, brand);
212-
optionalBrandCheck(rightBrand, brand);
213-
leftBrand === rightBrand ||
214-
Fail`Brands in left ${q(leftBrand)} and right ${q(
215-
rightBrand,
216-
)} should match but do not`;
217-
const leftKind = assertValueGetAssetKind(leftValue);
218-
const h = helpers[leftKind];
224+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
225+
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
219226
// @ts-expect-error cast?
220-
return h.doIsGTE(h.doCoerce(leftValue), rightValueHasBound);
227+
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
221228
};
222229

223230
/**
@@ -385,30 +392,48 @@ export const AmountMath = {
385392
* @returns {L}
386393
*/
387394
subtract: (leftAmount, rightAmountBound, brand = undefined) => {
388-
if (isKey(rightAmountBound)) {
389-
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
390-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
395+
if (kindOf(rightAmountBound) === 'match:containerHas') {
396+
mustMatch(leftAmount, AmountShape, 'left amount');
397+
const { brand: leftBrand, value: leftValue } = leftAmount;
398+
const {
399+
brand: rightBrand,
400+
value: {
401+
payload: [elementPatt, bound],
402+
},
403+
} = /** @type {AmountHasBound} */ (rightAmountBound);
404+
optionalBrandCheck(leftBrand, brand);
405+
optionalBrandCheck(rightBrand, brand);
406+
leftBrand === rightBrand ||
407+
Fail`Brands in left ${q(leftBrand)} and right ${q(
408+
rightBrand,
409+
)} should match but do not`;
410+
const leftKind = assertValueGetAssetKind(leftValue);
411+
leftKind !== 'nat' ||
412+
Fail`can only use M.containerHas on container assets, not nat: ${leftValue}`;
413+
const h = helpers[leftKind];
414+
// @ts-expect-error param type of doCoerce should not be never
415+
const lv = h.doCoerce(leftValue);
416+
// Passing in `assertChecker` as the `check` argument should guarantee
417+
// that `value` is not `undefined`. It would have thrown first.
418+
const [_, value] = containerHasSplit(
419+
lv,
420+
elementPatt,
421+
bound,
422+
false,
423+
true,
424+
assertChecker,
425+
);
391426
// @ts-expect-error cast?
392-
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
393-
// @ts-expect-error different subtype
394-
return harden({ brand: leftAmount.brand, value });
427+
return harden({ brand: leftBrand, value });
395428
}
396-
mustMatch(leftAmount, AmountShape, 'left amount');
397-
mustMatch(rightAmountBound, AmountBoundShape, 'right amount bound');
398-
const { brand: leftBrand, value: leftValue } = leftAmount;
399-
const { brand: rightBrand, value: rightValueHasBound } = rightAmountBound;
400-
optionalBrandCheck(leftBrand, brand);
401-
optionalBrandCheck(rightBrand, brand);
402-
leftBrand === rightBrand ||
403-
Fail`Brands in left ${q(leftBrand)} and right ${q(
404-
rightBrand,
405-
)} should match but do not`;
406-
const leftKind = assertValueGetAssetKind(leftValue);
407-
const h = helpers[leftKind];
408-
// @ts-expect-error cast?
409-
const value = h.doSubtract(h.doCoerce(leftValue), rightValueHasBound);
429+
// @ts-expect-error I don't know why TS complains here but not in
430+
// the identical case in isGTE
431+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
432+
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
410433
// @ts-expect-error cast?
411-
return harden({ brand: leftBrand, value });
434+
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
435+
// @ts-expect-error different subtype
436+
return harden({ brand: leftAmount.brand, value });
412437
},
413438
/**
414439
* Returns the min value between x and y using isGTE

packages/ERTP/src/legacy-payment-helpers.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const claim = async (
4040
) => {
4141
const srcPayment = await srcPaymentP;
4242
return E.when(E(recoveryPurse).deposit(srcPayment, optAmountShape), amount =>
43+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
44+
// of AmountBound?
4345
E(recoveryPurse).withdraw(amount),
4446
);
4547
};
@@ -87,6 +89,8 @@ export const combine = async (
8789
if (optTotalAmount !== undefined) {
8890
mustMatch(total, optTotalAmount, 'amount');
8991
}
92+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
93+
// of AmountBound?
9094
return E(recoveryPurse).withdraw(total);
9195
};
9296
harden(combine);
@@ -111,7 +115,11 @@ export const split = async (recoveryPurse, srcPaymentP, paymentAmountA) => {
111115
const srcAmount = await E(recoveryPurse).deposit(srcPayment);
112116
const paymentAmountB = AmountMath.subtract(srcAmount, paymentAmountA);
113117
return Promise.all([
118+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
119+
// of AmountBound?
114120
E(recoveryPurse).withdraw(paymentAmountA),
121+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
122+
// of AmountBound?
115123
E(recoveryPurse).withdraw(paymentAmountB),
116124
]);
117125
};
@@ -147,6 +155,12 @@ export const splitMany = async (recoveryPurse, srcPaymentP, amounts) => {
147155
AmountMath.isEqual(srcAmount, total) ||
148156
Fail`rights were not conserved: ${total} vs ${srcAmount}`;
149157

150-
return Promise.all(amounts.map(amount => E(recoveryPurse).withdraw(amount)));
158+
return Promise.all(
159+
amounts.map(amount =>
160+
// @ts-expect-error Why doesn't TS know that an Amount is a kind
161+
// of AmountBound?
162+
E(recoveryPurse).withdraw(amount),
163+
),
164+
);
151165
};
152166
harden(splitMany);

packages/ERTP/src/mathHelpers/copySetMathHelpers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
setIsSuperset,
1010
setDisjointUnion,
1111
setDisjointSubtract,
12-
} from '@agoric/store';
12+
} from '@endo/patterns';
1313

1414
/**
1515
* @import {Key, CopySet} from '@endo/patterns'
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
// @jessie-check
22

3-
import { Fail } from '@endo/errors';
4-
import { passStyleOf, assertChecker } from '@endo/pass-style';
5-
import { isKey, assertKey, elementsHasSplit, kindOf } from '@endo/patterns';
3+
import { passStyleOf } from '@endo/pass-style';
64
import {
5+
assertKey,
76
elementsIsSuperset,
87
elementsDisjointUnion,
98
elementsDisjointSubtract,
109
coerceToElements,
1110
elementsCompare,
12-
} from '@agoric/store';
11+
} from '@endo/patterns';
1312

1413
/**
1514
* @import {Key} from '@endo/patterns'
@@ -37,37 +36,8 @@ export const setMathHelpers = harden({
3736
},
3837
doMakeEmpty: () => empty,
3938
doIsEmpty: list => passStyleOf(list) === 'copyArray' && list.length === 0,
40-
doIsGTE: (left, rightBound) => {
41-
if (isKey(rightBound)) {
42-
return elementsIsSuperset(left, rightBound);
43-
}
44-
kindOf(rightBound) === 'match:has' ||
45-
Fail`rightBound must either be a key or an M.has pattern ${rightBound}`;
46-
const {
47-
payload: [elementPatt, bound],
48-
} = rightBound;
49-
return elementsHasSplit(left, elementPatt, bound);
50-
},
39+
doIsGTE: elementsIsSuperset,
5140
doIsEqual: (x, y) => elementsCompare(x, y) === 0,
5241
doAdd: elementsDisjointUnion,
53-
doSubtract: (left, rightBound) => {
54-
if (isKey(rightBound)) {
55-
return elementsDisjointSubtract(left, rightBound);
56-
}
57-
kindOf(rightBound) === 'match:has' ||
58-
Fail`rightBound must either be a key or an M.has pattern ${rightBound}`;
59-
const {
60-
payload: [elementPatt, bound],
61-
} = rightBound;
62-
const result = [];
63-
elementsHasSplit(
64-
left,
65-
elementPatt,
66-
bound,
67-
undefined,
68-
result,
69-
assertChecker,
70-
);
71-
return harden(result);
72-
},
42+
doSubtract: elementsDisjointSubtract,
7343
});

packages/ERTP/src/paymentLedger.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// @jessie-check
22

3-
/// <reference types="@agoric/store/exported.js" />
4-
53
import { X, q, Fail, annotateError } from '@endo/errors';
64
import { isPromise } from '@endo/promise-kit';
7-
import { mustMatch, M, keyEQ } from '@agoric/store';
5+
import { mustMatch, M, keyEQ } from '@endo/patterns';
86
import { AmountMath } from './amountMath.js';
97
import { preparePaymentKind } from './payment.js';
108
import { preparePurseKind } from './purse.js';
@@ -13,7 +11,7 @@ import { BrandI, makeIssuerInterfaces } from './typeGuards.js';
1311

1412
/**
1513
* @import {Key, Pattern} from '@endo/patterns';
16-
* @import {Amount, AssetKind, DisplayInfo, PaymentLedger, Payment, Brand, RecoverySetsOption, Purse, Issuer, Mint} from './types.js'
14+
* @import {Amount, AssetKind, DisplayInfo, PaymentLedger, Payment, Brand, RecoverySetsOption, Purse, Issuer, Mint, AmountBound} from './types.js'
1715
* @import {ShutdownWithFailure} from '@agoric/swingset-vat'
1816
* @import {TypedPattern} from '@agoric/internal';
1917
*/

packages/ERTP/src/purse.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,16 @@ export const preparePurseKind = (
108108
updateBalance(purse, balanceStore.getAmount());
109109
return srcPaymentBalance;
110110
},
111-
withdraw(amount) {
111+
withdraw(amountBound) {
112112
const { state } = this;
113113
const { purse } = this.facets;
114114

115115
const optRecoverySet = maybeRecoverySet(state);
116116
const balanceStore = makeAmountStore(state, 'currentBalance');
117-
// Note COMMIT POINT within withdraw.
117+
// Note COMMIT POINT within withdrawInternal.
118118
const payment = withdrawInternal(
119119
balanceStore,
120-
amount,
120+
amountBound,
121121
optRecoverySet,
122122
);
123123
updateBalance(purse, balanceStore.getAmount());

packages/ERTP/src/typeGuards.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,15 @@ harden(AmountShape);
9797
*/
9898
export const AmountPatternShape = M.pattern();
9999

100-
/** @see {HasBound} */
101-
export const HasBoundShape = M.tagged('match:has');
100+
/**
101+
* TODO: need a pattern to test `kindOf(specimen) === 'match:containerHas'` to
102+
* to ensure that the pattern-level invariants are met.
103+
*
104+
* TODO: check all uses of M.tagged to see if they have the same weakness.
105+
*
106+
* @see {HasBound}
107+
*/
108+
export const HasBoundShape = M.tagged('match:containerHas');
102109

103110
/** @see {AmountValueBound} */
104111
const AmountValueBoundShape = M.or(AmountValueShape, HasBoundShape);
@@ -110,6 +117,11 @@ export const AmountBoundShape = {
110117
};
111118
harden(AmountBoundShape);
112119

120+
export const AmountHasBoundShape = {
121+
brand: BrandShape,
122+
value: HasBoundShape,
123+
};
124+
113125
/** @type {TypedPattern<Ratio>} */
114126
export const RatioShape = { numerator: AmountShape, denominator: AmountShape };
115127
harden(RatioShape);
@@ -242,7 +254,7 @@ export const makeIssuerInterfaces = (
242254
.optional(AmountPatternShape)
243255
.returns(amountShape),
244256
getDepositFacet: M.call().returns(DepositFacetShape),
245-
withdraw: M.call(amountShape).returns(PaymentShape),
257+
withdraw: M.call(AmountBoundShape).returns(PaymentShape),
246258
getRecoverySet: M.call().returns(M.setOf(PaymentShape)),
247259
recoverAll: M.call().returns(amountShape),
248260
});

0 commit comments

Comments
 (0)