11import { q , Fail } from '@endo/errors' ;
2- import { passStyleOf , assertRemotable , assertRecord } from '@endo/marshal' ;
2+ import { assertRemotable , assertRecord } from '@endo/pass-style' ;
3+ import { isKey , kindOf , mustMatch } from '@endo/patterns' ;
34
4- import { M , matches } from '@agoric/store' ;
55import { natMathHelpers } from './mathHelpers/natMathHelpers.js' ;
66import { setMathHelpers } from './mathHelpers/setMathHelpers.js' ;
77import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js' ;
88import { copyBagMathHelpers } from './mathHelpers/copyBagMathHelpers.js' ;
9+ import {
10+ AmountBoundShape ,
11+ AmountShape ,
12+ AmountValueHasBoundShape ,
13+ } from './typeGuards.js' ;
914
1015/**
11- * @import {CopyBag, CopySet} from '@endo/patterns';
12- * @import {Amount, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
16+ * @import {Passable} from '@endo/pass-style'
17+ * @import {Key, CopyBag, CopySet} from '@endo/patterns';
18+ * @import {Amount, AmountBound, AmountValue, AssetValueForKind, Brand, CopyBagAmount, CopySetAmount, MathHelpers, NatAmount, NatValue, SetAmount, SetValue} from './types.js';
1319 */
1420
1521// NB: AssetKind is both a constant for enumerated values and a type for those values.
@@ -75,39 +81,42 @@ const helpers = {
7581 copyBag : copyBagMathHelpers ,
7682} ;
7783
78- /** @type {(value: unknown) => 'nat' | 'set' | 'copySet' | 'copyBag' } } */
84+ /**
85+ * @template {AssetKind} K=AssetKind
86+ * @template {Key} M=Key
87+ * @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
88+ * @param {V } value
89+ * @returns {AssetKind }
90+ */
7991const 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' ;
92+ const kind = kindOf ( value ) ;
93+ switch ( kind ) {
94+ case 'bigint' : {
95+ return 'nat' ;
96+ }
97+ case 'copyArray' : {
98+ return 'set' ;
99+ }
100+ case 'copySet' :
101+ case 'copyBag' : {
102+ return kind ;
103+ }
104+ default : {
105+ throw Fail `value ${ value } must be an AmountValue, not ${ q ( kind ) } ` ;
106+ }
92107 }
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- ) } `;
101108} ;
102109
103110/**
104111 * Asserts that value is a valid AmountMath and returns the appropriate helpers.
105112 *
106113 * Made available only for testing, but it is harmless for other uses.
107114 *
108- * @template V
115+ * @template {AssetKind} K=AssetKind
116+ * @template {Key} M=Key
117+ * @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
109118 * @param {V } value
110- * @returns {MathHelpers<V> }
119+ * @returns {MathHelpers<K, M, V> }
111120 */
112121export const assertValueGetHelpers = value =>
113122 // @ts -expect-error cast
@@ -125,35 +134,38 @@ const optionalBrandCheck = (allegedBrand, brand) => {
125134} ;
126135
127136/**
128- * @template {AssetKind} K
137+ * @template {AssetKind} K=AssetKind
138+ * @template {Key} M=Key
139+ * @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
129140 * @param {Amount<K> } leftAmount
130141 * @param {Amount<K> } rightAmount
131142 * @param {Brand<K> | undefined } brand
132- * @returns {MathHelpers<any > }
143+ * @returns {MathHelpers<K, M, V > }
133144 */
134145const checkLRAndGetHelpers = ( leftAmount , rightAmount , brand = undefined ) => {
135- assertRecord ( leftAmount , 'leftAmount' ) ;
136- assertRecord ( rightAmount , 'rightAmount' ) ;
137- const { value : leftValue , brand : leftBrand } = leftAmount ;
138- const { value : rightValue , brand : rightBrand } = rightAmount ;
139- assertRemotable ( leftBrand , 'leftBrand' ) ;
140- assertRemotable ( rightBrand , 'rightBrand' ) ;
146+ mustMatch ( leftAmount , AmountShape , 'left amount' ) ;
147+ mustMatch ( rightAmount , AmountShape , 'right amount' ) ;
148+ const { brand : leftBrand , value : leftValue } = leftAmount ;
149+ const { brand : rightBrand , value : rightValue } = rightAmount ;
141150 optionalBrandCheck ( leftBrand , brand ) ;
142151 optionalBrandCheck ( rightBrand , brand ) ;
143152 leftBrand === rightBrand ||
144153 Fail `Brands in left ${ q ( leftBrand ) } and right ${ q (
145154 rightBrand ,
146155 ) } should match but do not`;
147- const leftHelpers = assertValueGetHelpers ( leftValue ) ;
148- const rightHelpers = assertValueGetHelpers ( rightValue ) ;
149- leftHelpers === rightHelpers ||
150- Fail `The left ${ leftAmount } and right amount ${ rightAmount } had different assetKinds` ;
151- return leftHelpers ;
156+ const leftKind = assertValueGetAssetKind ( leftValue ) ;
157+ const rightKind = assertValueGetAssetKind ( rightValue ) ;
158+ leftKind === rightKind ||
159+ Fail `The left ${ leftAmount } and right amounts ${ rightAmount } had different assetKinds: ${ q ( leftKind ) } vs ${ q ( rightKind ) } ` ;
160+ // @ts -expect-error cast
161+ return helpers [ leftKind ] ;
152162} ;
153163
154164/**
155- * @template {AssetKind} K
156- * @param {MathHelpers<AssetValueForKind<K>> } h
165+ * @template {AssetKind} K=AssetKind
166+ * @template {Key} M=Key
167+ * @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
168+ * @param {MathHelpers<K, M, V> } h
157169 * @param {Amount<K> } leftAmount
158170 * @param {Amount<K> } rightAmount
159171 * @returns {[K, K] }
@@ -164,21 +176,53 @@ const coerceLR = (h, leftAmount, rightAmount) => {
164176} ;
165177
166178/**
167- * Returns true if the leftAmount is greater than or equal to the rightAmount.
168- * The notion of "greater than or equal to" depends on the kind of amount, as
169- * defined by the MathHelpers. For example, whether rectangle A is greater than
170- * rectangle B depends on whether rectangle A includes rectangle B as defined by
171- * the logic in MathHelpers.
179+ * Returns true if the leftAmount is greater than or equal to the
180+ * rightAmountBound. The notion of "greater than or equal to" depends on the
181+ * kind of amount, as defined by the MathHelpers. For example, whether rectangle
182+ * A is greater than rectangle B depends on whether rectangle A includes
183+ * rectangle B as defined by the logic in MathHelpers.
184+ *
185+ * For non-fungible or sem-fungible amounts, the right operand can also be an
186+ * `AmountBound` which can a normal concrete `Amount` or a specialized pattern:
187+ * A `RecordPattern` of a normal concrete `brand: Brand` and a `value:
188+ * AmountValueHasBound`, as made by `M.has(elementPattern)` or
189+ * `M.has(elementPattern, bigint)`. This represents those elements of the value
190+ * collection that match the elementPattern, if that number is exactly the same
191+ * as the bigint argument. If the second argument of `M.has` is omitted, it
192+ * defaults to `1n`. IOW, the left operand is `>=` such a bound if the total
193+ * number of elements in the left operand that match the element pattern is `>=`
194+ * the bigint argument in the `M.has` pattern.
172195 *
173196 * @template {AssetKind} K
174197 * @param {Amount<K> } leftAmount
175- * @param {Amount <K> } rightAmount
198+ * @param {AmountBound <K> } rightAmountBound
176199 * @param {Brand<K> } [brand]
177200 * @returns {boolean }
178201 */
179- const isGTE = ( leftAmount , rightAmount , brand = undefined ) => {
180- const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
181- return h . doIsGTE ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
202+ const isGTE = ( leftAmount , rightAmountBound , brand = undefined ) => {
203+ if ( isKey ( rightAmountBound ) ) {
204+ const rightAmount = /** @type {Amount<K> } */ ( rightAmountBound ) ;
205+ const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
206+ // @ts -expect-error cast?
207+ return h . doIsGTE ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
208+ }
209+ mustMatch ( leftAmount , AmountShape , 'left amount' ) ;
210+ mustMatch ( rightAmountBound , AmountBoundShape , 'right amount bound' ) ;
211+ const { brand : leftBrand , value : leftValue } = leftAmount ;
212+ const { brand : rightBrand , value : rightValueHasBound } = rightAmountBound ;
213+ optionalBrandCheck ( leftBrand , brand ) ;
214+ optionalBrandCheck ( rightBrand , brand ) ;
215+ leftBrand === rightBrand ||
216+ Fail `Brands in left ${ q ( leftBrand ) } and right ${ q (
217+ rightBrand ,
218+ ) } should match but do not`;
219+ const leftKind = assertValueGetAssetKind ( leftValue ) ;
220+ // If it were anything else, it would have been a Key and so taken care of
221+ // in the first case above.
222+ mustMatch ( rightValueHasBound , AmountValueHasBoundShape , 'right value bound' ) ;
223+ const h = helpers [ leftKind ] ;
224+ // @ts -expect-error cast?
225+ return h . doIsGTE ( h . doCoerce ( leftValue ) , rightValueHasBound ) ;
182226} ;
183227
184228/**
@@ -307,6 +351,7 @@ export const AmountMath = {
307351 */
308352 isEqual : ( leftAmount , rightAmount , brand = undefined ) => {
309353 const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
354+ // @ts -expect-error cast?
310355 return h . doIsEqual ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
311356 } ,
312357 /**
@@ -324,29 +369,58 @@ export const AmountMath = {
324369 */
325370 add : ( leftAmount , rightAmount , brand = undefined ) => {
326371 const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
372+ // @ts -expect-error cast?
327373 const value = h . doAdd ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
328374 // @ts -expect-error different subtype
329375 return harden ( { brand : leftAmount . brand , value } ) ;
330376 } ,
331377 /**
332- * Returns a new amount that is the leftAmount minus the rightAmount (i.e.
333- * everything in the leftAmount that is not in the rightAmount ). If leftAmount
334- * doesn't include rightAmount (subtraction results in a negative), throw an
335- * error. Because the left amount must include the right amount, this is NOT
336- * equivalent to set subtraction.
378+ * Returns a new amount that is the leftAmount minus the rightAmountBound
379+ * (i.e. everything in the leftAmount that is not in the rightAmountBound ). If
380+ * leftAmount doesn't include rightAmountBound (subtraction results in a
381+ * negative), throw an error. Because the left amount must include the right
382+ * amount bound, this is NOT equivalent to set subtraction.
337383 *
338- * @template {Amount} L
339- * @template {Amount} R
384+ * @template {AssetKind} K
385+ * @template {Amount<K>} L
386+ * @template {AmountBound<K>} R
340387 * @param {L } leftAmount
341- * @param {R } rightAmount
388+ * @param {R } rightAmountBound
342389 * @param {Brand } [brand]
343- * @returns {L extends R ? L : never }
390+ * @returns {L }
344391 */
345- subtract : ( leftAmount , rightAmount , brand = undefined ) => {
346- const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
347- const value = h . doSubtract ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
348- // @ts -expect-error different subtype
349- return harden ( { brand : leftAmount . brand , value } ) ;
392+ subtract : ( leftAmount , rightAmountBound , brand = undefined ) => {
393+ if ( isKey ( rightAmountBound ) ) {
394+ const rightAmount = /** @type {Amount<K> } */ ( rightAmountBound ) ;
395+ const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
396+ // @ts -expect-error cast?
397+ const value = h . doSubtract ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
398+ // @ts -expect-error different subtype
399+ return harden ( { brand : leftAmount . brand , value } ) ;
400+ }
401+ mustMatch ( leftAmount , AmountShape , 'left amount' ) ;
402+ mustMatch ( rightAmountBound , AmountBoundShape , 'right amount bound' ) ;
403+ const { brand : leftBrand , value : leftValue } = leftAmount ;
404+ const { brand : rightBrand , value : rightValueHasBound } = rightAmountBound ;
405+ optionalBrandCheck ( leftBrand , brand ) ;
406+ optionalBrandCheck ( rightBrand , brand ) ;
407+ leftBrand === rightBrand ||
408+ Fail `Brands in left ${ q ( leftBrand ) } and right ${ q (
409+ rightBrand ,
410+ ) } should match but do not`;
411+ const leftKind = assertValueGetAssetKind ( leftValue ) ;
412+ // If it were anything else, it would have been a Key and so taken care of
413+ // in the first case above.
414+ mustMatch (
415+ rightValueHasBound ,
416+ AmountValueHasBoundShape ,
417+ 'right value bound' ,
418+ ) ;
419+ const h = helpers [ leftKind ] ;
420+ // @ts -expect-error cast?
421+ const value = h . doSubtract ( h . doCoerce ( leftValue ) , rightValueHasBound ) ;
422+ // @ts -expect-error cast?
423+ return harden ( { brand : leftBrand , value } ) ;
350424 } ,
351425 /**
352426 * Returns the min value between x and y using isGTE
0 commit comments