11import { 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' ;
56import { natMathHelpers } from './mathHelpers/natMathHelpers.js' ;
67import { setMathHelpers } from './mathHelpers/setMathHelpers.js' ;
78import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js' ;
89import { 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 */
112118export 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 */
136144const 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 */
163172const 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
0 commit comments