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, AmountValue, 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 {AmountValue} 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
@@ -127,35 +136,38 @@ const optionalBrandCheck = (allegedBrand, brand) => {
127136} ;
128137
129138/**
130- * @template {AssetKind} K
139+ * @template {AssetKind} K=AssetKind
140+ * @template {Key} M=Key
141+ * @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
131142 * @param {Amount<K> } leftAmount
132143 * @param {Amount<K> } rightAmount
133144 * @param {Brand<K> | undefined } brand
134- * @returns {MathHelpers<any > }
145+ * @returns {MathHelpers<K, M, V > }
135146 */
136147const 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' ) ;
148+ mustMatch ( leftAmount , AmountShape , 'left amount' ) ;
149+ mustMatch ( rightAmount , AmountShape , 'right amount' ) ;
150+ const { brand : leftBrand , value : leftValue } = leftAmount ;
151+ const { brand : rightBrand , value : rightValue } = rightAmount ;
143152 optionalBrandCheck ( leftBrand , brand ) ;
144153 optionalBrandCheck ( rightBrand , brand ) ;
145154 leftBrand === rightBrand ||
146155 Fail `Brands in left ${ q ( leftBrand ) } and right ${ q (
147156 rightBrand ,
148157 ) } 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 ;
158+ const leftKind = assertValueGetAssetKind ( leftValue ) ;
159+ const rightKind = assertValueGetAssetKind ( rightValue ) ;
160+ leftKind === rightKind ||
161+ Fail `The left ${ leftAmount } and right amounts ${ rightAmount } had different assetKinds: ${ q ( leftKind ) } vs ${ q ( rightKind ) } ` ;
162+ // @ts -expect-error cast
163+ return helpers [ leftKind ] ;
154164} ;
155165
156166/**
157- * @template {AssetKind} K
158- * @param {MathHelpers<AssetValueForKind<K>> } h
167+ * @template {AssetKind} K=AssetKind
168+ * @template {Key} M=Key
169+ * @template {AssetValueForKind<K, M>} V=AssetValueForKind<K, M>
170+ * @param {MathHelpers<K, M, V> } h
159171 * @param {Amount<K> } leftAmount
160172 * @param {Amount<K> } rightAmount
161173 * @returns {[K, K] }
@@ -166,21 +178,53 @@ const coerceLR = (h, leftAmount, rightAmount) => {
166178} ;
167179
168180/**
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.
181+ * Returns true if the leftAmount is greater than or equal to the
182+ * rightAmountBound. The notion of "greater than or equal to" depends on the
183+ * kind of amount, as defined by the MathHelpers. For example, whether rectangle
184+ * A is greater than rectangle B depends on whether rectangle A includes
185+ * rectangle B as defined by the logic in MathHelpers.
186+ *
187+ * For non-fungible or sem-fungible amounts, the right operand can also be an
188+ * `AmountBound` which can a normal concrete `Amount` or a specialized pattern:
189+ * A `RecordPattern` of a normal concrete `brand: Brand` and a `value:
190+ * AmountValueHasBound`, as made by `M.has(elementPattern)` or
191+ * `M.has(elementPattern, bigint)`. This represents those elements of the value
192+ * collection that match the elementPattern, if that number is exactly the same
193+ * as the bigint argument. If the second argument of `M.has` is omitted, it
194+ * defaults to `1n`. IOW, the left operand is `>=` such a bound if the total
195+ * number of elements in the left operand that match the element pattern is `>=`
196+ * the bigint argument in the `M.has` pattern.
174197 *
175198 * @template {AssetKind} K
176199 * @param {Amount<K> } leftAmount
177- * @param {Amount <K> } rightAmount
200+ * @param {AmountBound <K> } rightAmountBound
178201 * @param {Brand<K> } [brand]
179202 * @returns {boolean }
180203 */
181- const isGTE = ( leftAmount , rightAmount , brand = undefined ) => {
182- const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
183- return h . doIsGTE ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
204+ const isGTE = ( leftAmount , rightAmountBound , brand = undefined ) => {
205+ if ( isKey ( rightAmountBound ) ) {
206+ const rightAmount = /** @type {Amount<K> } */ ( rightAmountBound ) ;
207+ const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
208+ // @ts -expect-error cast?
209+ return h . doIsGTE ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
210+ }
211+ mustMatch ( leftAmount , AmountShape , 'left amount' ) ;
212+ mustMatch ( rightAmountBound , AmountBoundShape , 'right amount bound' ) ;
213+ const { brand : leftBrand , value : leftValue } = leftAmount ;
214+ const { brand : rightBrand , value : rightValueHasBound } = rightAmountBound ;
215+ optionalBrandCheck ( leftBrand , brand ) ;
216+ optionalBrandCheck ( rightBrand , brand ) ;
217+ leftBrand === rightBrand ||
218+ Fail `Brands in left ${ q ( leftBrand ) } and right ${ q (
219+ rightBrand ,
220+ ) } should match but do not`;
221+ const leftKind = assertValueGetAssetKind ( leftValue ) ;
222+ // If it were anything else, it would have been a Key and so taken care of
223+ // in the first case above.
224+ mustMatch ( rightValueHasBound , AmountValueHasBoundShape , 'right value bound' ) ;
225+ const h = helpers [ leftKind ] ;
226+ // @ts -expect-error cast?
227+ return h . doIsGTE ( h . doCoerce ( leftValue ) , rightValueHasBound ) ;
184228} ;
185229
186230/**
@@ -309,6 +353,7 @@ export const AmountMath = {
309353 */
310354 isEqual : ( leftAmount , rightAmount , brand = undefined ) => {
311355 const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
356+ // @ts -expect-error cast?
312357 return h . doIsEqual ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
313358 } ,
314359 /**
@@ -326,29 +371,58 @@ export const AmountMath = {
326371 */
327372 add : ( leftAmount , rightAmount , brand = undefined ) => {
328373 const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
374+ // @ts -expect-error cast?
329375 const value = h . doAdd ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
330376 // @ts -expect-error different subtype
331377 return harden ( { brand : leftAmount . brand , value } ) ;
332378 } ,
333379 /**
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.
380+ * Returns a new amount that is the leftAmount minus the rightAmountBound
381+ * (i.e. everything in the leftAmount that is not in the rightAmountBound ). If
382+ * leftAmount doesn't include rightAmountBound (subtraction results in a
383+ * negative), throw an error. Because the left amount must include the right
384+ * amount bound, this is NOT equivalent to set subtraction.
339385 *
340- * @template {Amount} L
341- * @template {Amount} R
386+ * @template {AssetKind} K
387+ * @template {Amount<K>} L
388+ * @template {AmountBound<K>} R
342389 * @param {L } leftAmount
343- * @param {R } rightAmount
390+ * @param {R } rightAmountBound
344391 * @param {Brand } [brand]
345- * @returns {L extends R ? L : never }
392+ * @returns {L }
346393 */
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 } ) ;
394+ subtract : ( leftAmount , rightAmountBound , brand = undefined ) => {
395+ if ( isKey ( rightAmountBound ) ) {
396+ const rightAmount = /** @type {Amount<K> } */ ( rightAmountBound ) ;
397+ const h = checkLRAndGetHelpers ( leftAmount , rightAmount , brand ) ;
398+ // @ts -expect-error cast?
399+ const value = h . doSubtract ( ...coerceLR ( h , leftAmount , rightAmount ) ) ;
400+ // @ts -expect-error different subtype
401+ return harden ( { brand : leftAmount . brand , value } ) ;
402+ }
403+ mustMatch ( leftAmount , AmountShape , 'left amount' ) ;
404+ mustMatch ( rightAmountBound , AmountBoundShape , 'right amount bound' ) ;
405+ const { brand : leftBrand , value : leftValue } = leftAmount ;
406+ const { brand : rightBrand , value : rightValueHasBound } = rightAmountBound ;
407+ optionalBrandCheck ( leftBrand , brand ) ;
408+ optionalBrandCheck ( rightBrand , brand ) ;
409+ leftBrand === rightBrand ||
410+ Fail `Brands in left ${ q ( leftBrand ) } and right ${ q (
411+ rightBrand ,
412+ ) } should match but do not`;
413+ const leftKind = assertValueGetAssetKind ( leftValue ) ;
414+ // If it were anything else, it would have been a Key and so taken care of
415+ // in the first case above.
416+ mustMatch (
417+ rightValueHasBound ,
418+ AmountValueHasBoundShape ,
419+ 'right value bound' ,
420+ ) ;
421+ const h = helpers [ leftKind ] ;
422+ // @ts -expect-error cast?
423+ const value = h . doSubtract ( h . doCoerce ( leftValue ) , rightValueHasBound ) ;
424+ // @ts -expect-error cast?
425+ return harden ( { brand : leftBrand , value } ) ;
352426 } ,
353427 /**
354428 * Returns the min value between x and y using isGTE
0 commit comments