Skip to content

Commit 2e4d932

Browse files
committed
feat(ertp): ertp-only part of minimal want-patterns
1 parent 4841c87 commit 2e4d932

23 files changed

+480
-149
lines changed

packages/ERTP/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@
4444
"@agoric/store": "^0.9.2",
4545
"@agoric/vat-data": "^0.5.2",
4646
"@agoric/zone": "^0.2.2",
47+
"@endo/common": "^1.2.10",
4748
"@endo/eventual-send": "^1.3.1",
4849
"@endo/far": "^1.1.11",
4950
"@endo/marshal": "^1.6.4",
5051
"@endo/nat": "^5.1.0",
52+
"@endo/pass-style": "^1.5.0",
5153
"@endo/patterns": "^1.5.0",
5254
"@endo/promise-kit": "^1.1.10"
5355
},

packages/ERTP/src/amountMath.js

Lines changed: 192 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { q, Fail } from '@endo/errors';
2-
import { passStyleOf, assertRemotable, assertRecord } from '@endo/marshal';
2+
import { assertRemotable, assertRecord, assertChecker } from '@endo/pass-style';
3+
import { identChecker } from '@endo/common/ident-checker.js';
4+
import { containerHasSplit, kindOf, mustMatch } from '@endo/patterns';
35

4-
import { M, matches } from '@agoric/store';
56
import { natMathHelpers } from './mathHelpers/natMathHelpers.js';
67
import { setMathHelpers } from './mathHelpers/setMathHelpers.js';
78
import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js';
89
import { copyBagMathHelpers } from './mathHelpers/copyBagMathHelpers.js';
10+
import { AmountShape } from './typeGuards.js';
911

1012
/**
11-
* @import {CopyBag, CopySet} from '@endo/patterns';
12-
* @import {Amount, AmountValue, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
13+
* @import {Checker} from '@endo/common/ident-checker.js'
14+
* @import {Key, CopyBag, CopySet} from '@endo/patterns';
15+
* @import {Amount, AmountBound, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue, HasBound, AmountValue} from './types.js';
1316
*/
1417

1518
// NB: AssetKind is both a constant for enumerated values and a type for those values.
@@ -75,39 +78,42 @@ const helpers = {
7578
copyBag: copyBagMathHelpers,
7679
};
7780

78-
/** @type {(value: unknown) => 'nat' | 'set' | 'copySet' | 'copyBag'} } */
79-
const assertValueGetAssetKind = value => {
80-
const passStyle = passStyleOf(value);
81-
if (passStyle === 'bigint') {
82-
return 'nat';
83-
}
84-
if (passStyle === 'copyArray') {
85-
return 'set';
86-
}
87-
if (matches(value, M.set())) {
88-
return 'copySet';
89-
}
90-
if (matches(value, M.bag())) {
91-
return 'copyBag';
81+
/**
82+
* @template {AssetKind} K=AssetKind
83+
* @template {Key} M=Key
84+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
85+
* @param {V} value
86+
* @returns {AssetKind}
87+
*/
88+
export const assertValueGetAssetKind = value => {
89+
const kind = kindOf(value);
90+
switch (kind) {
91+
case 'bigint': {
92+
return 'nat';
93+
}
94+
case 'copyArray': {
95+
return 'set';
96+
}
97+
case 'copySet':
98+
case 'copyBag': {
99+
return kind;
100+
}
101+
default: {
102+
throw Fail`value ${value} must be an AmountValue, not ${q(kind)}`;
103+
}
92104
}
93-
// TODO This isn't quite the right error message, in case valuePassStyle
94-
// is 'tagged'. We would need to distinguish what kind of tagged
95-
// object it is.
96-
// Also, this kind of manual listing is a maintenance hazard we
97-
// (TODO) will encounter when we extend the math helpers further.
98-
throw Fail`value ${value} must be a bigint, copySet, copyBag, or an array, not ${q(
99-
passStyle,
100-
)}`;
101105
};
102106

103107
/**
104108
* Asserts that value is a valid AmountMath and returns the appropriate helpers.
105109
*
106110
* Made available only for testing, but it is harmless for other uses.
107111
*
108-
* @template {AmountValue} V
112+
* @template {AssetKind} K=AssetKind
113+
* @template {Key} M=Key
114+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
109115
* @param {V} value
110-
* @returns {MathHelpers<V>}
116+
* @returns {MathHelpers<K, M, V>}
111117
*/
112118
export const assertValueGetHelpers = value =>
113119
// @ts-expect-error cast
@@ -127,62 +133,168 @@ const optionalBrandCheck = (allegedBrand, brand) => {
127133
};
128134

129135
/**
130-
* @template {AssetKind} K
136+
* @template {AssetKind} K=AssetKind
137+
* @template {Key} M=Key
138+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
131139
* @param {Amount<K>} leftAmount
132140
* @param {Amount<K>} rightAmount
133141
* @param {Brand<K> | undefined} brand
134-
* @returns {MathHelpers<any>}
142+
* @returns {MathHelpers<K, M, V>}
135143
*/
136144
const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => {
137-
assertRecord(leftAmount, 'leftAmount');
138-
assertRecord(rightAmount, 'rightAmount');
139-
const { value: leftValue, brand: leftBrand } = leftAmount;
140-
const { value: rightValue, brand: rightBrand } = rightAmount;
141-
assertRemotable(leftBrand, 'leftBrand');
142-
assertRemotable(rightBrand, 'rightBrand');
145+
mustMatch(leftAmount, AmountShape, 'left amount');
146+
mustMatch(rightAmount, AmountShape, 'right amount');
147+
const { brand: leftBrand, value: leftValue } = leftAmount;
148+
const { brand: rightBrand, value: rightValue } = rightAmount;
143149
optionalBrandCheck(leftBrand, brand);
144150
optionalBrandCheck(rightBrand, brand);
145151
leftBrand === rightBrand ||
146152
Fail`Brands in left ${q(leftBrand)} and right ${q(
147153
rightBrand,
148154
)} should match but do not`;
149-
const leftHelpers = assertValueGetHelpers(leftValue);
150-
const rightHelpers = assertValueGetHelpers(rightValue);
151-
leftHelpers === rightHelpers ||
152-
Fail`The left ${leftAmount} and right amount ${rightAmount} had different assetKinds`;
153-
return leftHelpers;
155+
const leftKind = assertValueGetAssetKind(leftValue);
156+
const rightKind = assertValueGetAssetKind(rightValue);
157+
leftKind === rightKind ||
158+
Fail`The left ${leftAmount} and right amounts ${rightAmount} had different assetKinds: ${q(leftKind)} vs ${q(rightKind)}`;
159+
// @ts-expect-error cast
160+
return helpers[leftKind];
154161
};
155162

156163
/**
157-
* @template {AssetKind} K
158-
* @param {MathHelpers<AssetValueForKind<K>>} h
164+
* @template {AssetKind} K=AssetKind
165+
* @template {Key} M=Key
166+
* @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
167+
* @param {MathHelpers<K, M, V>} h
159168
* @param {Amount<K>} leftAmount
160169
* @param {Amount<K>} rightAmount
161-
* @returns {[AssetValueForKind<K>, AssetValueForKind<K>]}
170+
* @returns {[V, V]}
162171
*/
163172
const coerceLR = (h, leftAmount, rightAmount) => {
164173
// @ts-expect-error could be arbitrary subtype
165174
return [h.doCoerce(leftAmount.value), h.doCoerce(rightAmount.value)];
166175
};
167176

168177
/**
169-
* Returns true if the leftAmount is greater than or equal to the rightAmount.
170-
* The notion of "greater than or equal to" depends on the kind of amount, as
171-
* defined by the MathHelpers. For example, whether rectangle A is greater than
172-
* rectangle B depends on whether rectangle A includes rectangle B as defined by
173-
* the logic in MathHelpers.
178+
* If `leftAmount` >= `rightAmountBound`
179+
*
180+
* - then return a pair of optional amounts:
181+
*
182+
* - the in amount, if `needInAmount` is true. Else undefined
183+
* - the out amount, if `needOutAmount` is true. Else undefined
184+
* - else return false
174185
*
175186
* @template {AssetKind} K
176187
* @param {Amount<K>} leftAmount
177-
* @param {Amount<K>} rightAmount
188+
* @param {AmountBound<K>} rightAmountBound
178189
* @param {Brand<K>} [brand]
179-
* @returns {boolean}
190+
* @param {boolean} [needInAmount]
191+
* @param {boolean} [needOutAmount]
192+
* @param {Checker} [check]
193+
* @returns {[Amount | undefined, Amount | undefined] | false}
180194
*/
181-
const isGTE = (leftAmount, rightAmount, brand = undefined) => {
182-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
183-
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
195+
const amountSplit = (
196+
leftAmount,
197+
rightAmountBound,
198+
brand = undefined,
199+
needInAmount = false,
200+
needOutAmount = false,
201+
check = identChecker,
202+
) => {
203+
mustMatch(leftAmount, AmountShape, 'left amount');
204+
const { brand: leftBrand, value: leftValue } = leftAmount;
205+
const { brand: rightBrand, value: rightValueBound } = rightAmountBound;
206+
optionalBrandCheck(leftBrand, brand);
207+
optionalBrandCheck(rightBrand, brand);
208+
leftBrand === rightBrand ||
209+
Fail`Brands in left ${q(leftBrand)} and right ${q(
210+
rightBrand,
211+
)} should match but do not`;
212+
brand = /** @type {Brand<K>} */ (leftBrand);
213+
const leftKind = assertValueGetAssetKind(leftValue);
214+
const h = helpers[leftKind];
215+
// @ts-expect-error param type of doCoerce should not be never
216+
const lv = h.doCoerce(leftValue);
217+
218+
if (kindOf(rightValueBound) === 'match:containerHas') {
219+
leftKind !== 'nat' ||
220+
Fail`can only use M.containerHas on non-fungible or semi-fungible assets ('set', 'copySet', 'copyBag'), not fungible assets ('nat'): ${leftValue}`;
221+
const {
222+
payload: [elementPatt, bound],
223+
} = /** @type {HasBound} */ (rightValueBound);
224+
const containerPair = containerHasSplit(
225+
lv,
226+
elementPatt,
227+
bound,
228+
needInAmount,
229+
needOutAmount,
230+
check,
231+
);
232+
if (containerPair) {
233+
const [inContainer, outContainer] = containerPair;
234+
return harden([
235+
inContainer && { brand, value: inContainer },
236+
outContainer && { brand, value: outContainer },
237+
]);
238+
} else {
239+
return false;
240+
}
241+
}
242+
const rightAmount = /** @type {Amount<K>} */ (rightAmountBound);
243+
// @ts-expect-error param type of doCoerce should not be never
244+
const rv = h.doCoerce(rightValueBound);
245+
const rightKind = assertValueGetAssetKind(rv);
246+
leftKind === rightKind ||
247+
Fail`The left ${leftAmount} and right amounts ${rightAmount} had different assetKinds: ${q(leftKind)} vs ${q(rightKind)}`;
248+
249+
// @ts-expect-error cast?
250+
if (h.doIsGTE(lv, rv)) {
251+
// @ts-expect-error type inference too weak
252+
return harden([
253+
needInAmount ? leftAmount : undefined,
254+
needOutAmount
255+
? {
256+
brand,
257+
// @ts-expect-error Where did type "never" come from?
258+
value: h.doSubtract(lv, rv),
259+
}
260+
: undefined,
261+
]);
262+
} else if (check === identChecker) {
263+
return false;
264+
} else {
265+
// @ts-expect-error Where did type "never" come from?
266+
h.doSubtract(lv, rv); // Just to get a better error message
267+
throw Fail`${lv} must be >= ${rv}`;
268+
}
184269
};
185270

271+
/**
272+
* Returns true if the leftAmount is greater than or equal to the
273+
* rightAmountBound. The notion of "greater than or equal to" depends on the
274+
* kind of amount, as defined by the MathHelpers. For example, whether rectangle
275+
* A is greater than rectangle B depends on whether rectangle A includes
276+
* rectangle B as defined by the logic in MathHelpers.
277+
*
278+
* For non-fungible or sem-fungible amounts, the right operand can also be an
279+
* `AmountBound` which can a normal concrete `Amount` or a specialized pattern:
280+
* A `RecordPattern` of a normal concrete `brand: Brand` and a `value:
281+
* HasBound`, as made by `M.containerHas(elementPattern)` or
282+
* `M.containerHas(elementPattern, bigint)`. This represents those elements of
283+
* the value collection that match the elementPattern, if that number is exactly
284+
* the same as the bigint argument. If the second argument of `M.containerHas`
285+
* is omitted, it defaults to `1n`. IOW, the left operand is `>=` such a bound
286+
* if the total number of elements in the left operand that match the element
287+
* pattern is `>=` the bigint argument in the `M.containerHas` pattern.
288+
*
289+
* @template {AssetKind} K
290+
* @param {Amount<K>} leftAmount
291+
* @param {AmountBound<K>} rightAmountBound
292+
* @param {Brand<K>} [brand]
293+
* @returns {boolean}
294+
*/
295+
const isGTE = (leftAmount, rightAmountBound, brand = undefined) =>
296+
!!amountSplit(leftAmount, rightAmountBound, brand);
297+
186298
/**
187299
* Logic for manipulating amounts.
188300
*
@@ -296,6 +408,7 @@ export const AmountMath = {
296408
const h = assertValueGetHelpers(value);
297409
return h.doIsEmpty(h.doCoerce(value));
298410
},
411+
amountSplit,
299412
isGTE,
300413
/**
301414
* Returns true if the leftAmount equals the rightAmount. We assume that if
@@ -331,24 +444,35 @@ export const AmountMath = {
331444
return harden({ brand: leftAmount.brand, value });
332445
},
333446
/**
334-
* Returns a new amount that is the leftAmount minus the rightAmount (i.e.
335-
* everything in the leftAmount that is not in the rightAmount). If leftAmount
336-
* doesn't include rightAmount (subtraction results in a negative), throw an
337-
* error. Because the left amount must include the right amount, this is NOT
338-
* equivalent to set subtraction.
447+
* Returns a new amount that is the leftAmount minus the rightAmountBound
448+
* (i.e. everything in the leftAmount that is not in the rightAmountBound). If
449+
* leftAmount doesn't include rightAmountBound (subtraction results in a
450+
* negative), throw an error. Because the left amount must include the right
451+
* amount bound, this is NOT equivalent to set subtraction.
339452
*
340-
* @template {Amount} L
341-
* @template {Amount} R
453+
* @template {AssetKind} K
454+
* @template {Amount<K>} L
455+
* @template {AmountBound<K>} R
342456
* @param {L} leftAmount
343-
* @param {R} rightAmount
457+
* @param {R} rightAmountBound
344458
* @param {Brand} [brand]
345-
* @returns {L extends R ? L : never}
459+
* @returns {L}
346460
*/
347-
subtract: (leftAmount, rightAmount, brand = undefined) => {
348-
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
349-
const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount));
350-
// @ts-expect-error different subtype
351-
return harden({ brand: leftAmount.brand, value });
461+
subtract: (leftAmount, rightAmountBound, brand = undefined) => {
462+
// @ts-expect-error passing in `assertChecker` as the `check` argument
463+
// guarantees that amountSplit returns a pair rather than `false`.
464+
// It would have thrown first.
465+
// In addition, passing in `true` as the `needOutAmount` argument
466+
// guarantees that `result` is not `undefined`.
467+
const [_, result] = amountSplit(
468+
leftAmount,
469+
rightAmountBound,
470+
brand,
471+
false,
472+
true,
473+
assertChecker,
474+
);
475+
return result;
352476
},
353477
/**
354478
* Returns the min value between x and y using isGTE

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

Lines changed: 15 additions & 1 deletion
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);

0 commit comments

Comments
 (0)