Skip to content

Commit

Permalink
feat: add static amountMath. Backwards compatible with old amountMath (
Browse files Browse the repository at this point in the history
…#2561)

* chore: add new static amountMath. Deprecate old amountMath.
  • Loading branch information
katelynsills authored Mar 8, 2021
1 parent e0704eb commit 1620307
Show file tree
Hide file tree
Showing 46 changed files with 1,130 additions and 811 deletions.
309 changes: 179 additions & 130 deletions packages/ERTP/src/amountMath.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// @ts-check

import { assert, details as X } from '@agoric/assert';
import { Far } from '@agoric/marshal';
import { mustBeComparable } from '@agoric/same-structure';
import { passStyleOf, REMOTE_STYLE } from '@agoric/marshal';
import { Nat, isNat } from '@agoric/nat';

import './types';
import natMathHelpers from './mathHelpers/natMathHelpers';
import strSetMathHelpers from './mathHelpers/strSetMathHelpers';
import setMathHelpers from './mathHelpers/setMathHelpers';
import { makeAmountMath } from './deprecatedAmountMath';

// We want an enum, but narrowed to the AmountMathKind type.
/**
Expand All @@ -21,7 +22,6 @@ const MathKind = {
STRING_SET: 'strSet',
};
harden(MathKind);
export { MathKind };

/**
* Amounts describe digital assets. From an amount, you can learn the
Expand Down Expand Up @@ -54,138 +54,187 @@ export { MathKind };
* function `coerce` takes an amount and checks it, returning an amount (amount
* -> amount).
*
* `makeAmountMath` takes in a brand and the kind of amountMath to use.
*
* amountMath is not pass-by-copy, but everything it does can be done
* locally in each vat that needs the functionality. If the operations
* are done against a remote version, they have the same semantics, but
* require an extra messaging round-trip per call. The best way to use
* it is to make a local copy, which can be done by calling
* makeLocalAmountMath(issuer).
*
* AmountMath exports MathKind, which contains constants for the kinds:
* NAT, SET, and STRING_SET.
*
* Each issuer of digital assets has an associated brand in a one-to-one
* mapping. In untrusted contexts, such as in analyzing payments and
* amounts, we can get the brand and find the issuer which matches the
* brand. The issuer and the brand mutually validate each other.
*
* @param {Brand} brand
* @param {AmountMathKind} amountMathKind
* @returns {AmountMath}
*/
function makeAmountMath(brand, amountMathKind) {
mustBeComparable(brand);
assert.typeof(amountMathKind, 'string');

const mathHelpers = {
nat: natMathHelpers,
strSet: strSetMathHelpers,
set: setMathHelpers,
};
const helpers = mathHelpers[amountMathKind];

/** @type {{ nat: NatMathHelpers, set: SetMathHelpers, strSet: SetMathHelpers }} */
const helpers = {
nat: natMathHelpers,
set: setMathHelpers,
strSet: setMathHelpers,
};

/**
* @type {(value: NatValue | SetValue) => SetMathHelpers | NatMathHelpers }
*/
const getHelpersFromValue = value => {
if (Array.isArray(value)) {
return setMathHelpers;
}
assert(
typeof Nat(value) === 'bigint',
X`value ${value} must be a bigint or an array`,
);
return natMathHelpers;
};

/** @type {(amount: Amount ) => NatMathHelpers | SetMathHelpers} */
const getHelpersFromAmount = amount => {
// @ts-ignore
return getHelpersFromValue(amount.value);
};

/** @type {(leftAmount: Amount, rightAmount: Amount ) =>
* NatMathHelpers | SetMathHelpers } */
const getHelpers = (leftAmount, rightAmount) => {
const leftHelpers = getHelpersFromAmount(leftAmount);
const rightHelpers = getHelpersFromAmount(rightAmount);
assert.equal(leftHelpers, rightHelpers);
return leftHelpers;
};

/** @type {(amount: Amount, brand?: Brand) => void} */
const optionalBrandCheck = (amount, brand) => {
if (brand !== undefined) {
mustBeComparable(brand);
assert.equal(
amount.brand,
brand,
X`amount's brand ${amount.brand} did not match expected brand ${brand}`,
);
}
};

/** @type {(value: Value, brand: Brand) => Amount} */
const noCoerceMake = (value, brand) => {
const amount = harden({ brand, value });
return amount;
};

/** @type {(value: Value) => void} */
const assertLooksLikeValue = value => {
assert(
helpers !== undefined,
X`unrecognized amountMathKind: ${amountMathKind}`,
Array.isArray(value) || isNat(value),
X`value ${value} must be a Nat or an array`,
);
};

const brandMethods = ['isMyIssuer', 'getAllegedName', 'getDisplayInfo'];

const checkBrand = (brand, msg) => {
assert(passStyleOf(brand) === REMOTE_STYLE, msg);
const ownKeys = Reflect.ownKeys(brand);
const inBrandMethods = key => brandMethods.includes(key);
assert(
passStyleOf(brand) === REMOTE_STYLE && ownKeys.every(inBrandMethods),
X`The brand ${brand} doesn't look like a brand. It has these keys: ${ownKeys}`,
);
};

/** @type {(brand: Brand) => void} */
const assertLooksLikeBrand = brand => {
const msg = X`The brand ${brand} doesn't look like a brand.`;
checkBrand(brand, msg);
};

/**
* Give a better error message by logging the entire amount
* rather than just the brand
*
* @type {(amount: Amount) => void}
*/
const assertLooksLikeAmountBrand = amount => {
const msg = X`The amount ${amount} doesn't look like an amount. Did you pass a value instead?`;
checkBrand(amount.brand, msg);
};

const assertLooksLikeAmount = amount => {
assertLooksLikeAmountBrand(amount);
assertLooksLikeValue(amount.value);
};

const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => {
assertLooksLikeAmount(leftAmount);
assertLooksLikeAmount(rightAmount);
optionalBrandCheck(leftAmount, brand);
optionalBrandCheck(rightAmount, brand);
assert.equal(
leftAmount.brand,
rightAmount.brand,
X`Brands in left ${leftAmount.brand} and right ${rightAmount.brand} should match but do not`,
);
return getHelpers(leftAmount, rightAmount);
};

const coerceLR = (h, leftAmount, rightAmount) => {
return [h.doCoerce(leftAmount.value), h.doCoerce(rightAmount.value)];
};

/** @type {AmountMath} */
const amountMath = {
make: (allegedValue, brand) => {
assertLooksLikeBrand(brand);
assertLooksLikeValue(allegedValue);
// @ts-ignore
const value = getHelpersFromValue(allegedValue).doCoerce(allegedValue);
return harden({ brand, value });
},
coerce: (allegedAmount, brand) => {
assertLooksLikeAmount(allegedAmount);
assertLooksLikeBrand(brand);
assert(
brand === allegedAmount.brand,
X`The brand in the allegedAmount ${allegedAmount} in 'coerce' didn't match the specified brand ${brand}.`,
);
// Will throw on inappropriate value
return amountMath.make(allegedAmount.value, brand);
},
getValue: (amount, brand) => amountMath.coerce(amount, brand).value,
makeEmpty: (mathKind, brand) => {
assert(
helpers[mathKind],
X`${mathKind} must be MathKind.NAT or MathKind.SET. MathKind.STRING_SET is accepted but deprecated`,
);
assertLooksLikeBrand(brand);
return noCoerceMake(helpers[mathKind].doMakeEmpty(), brand);
},
isEmpty: (amount, brand = undefined) => {
assertLooksLikeAmount(amount);
optionalBrandCheck(amount, brand);
const h = getHelpersFromAmount(amount);
// @ts-ignore
return h.doIsEmpty(h.doCoerce(amount.value));
},
isGTE: (leftAmount, rightAmount, brand = undefined) => {
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
// @ts-ignore
return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount));
},
isEqual: (leftAmount, rightAmount, brand = undefined) => {
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
// @ts-ignore
return h.doIsEqual(...coerceLR(h, leftAmount, rightAmount));
},
add: (leftAmount, rightAmount, brand = undefined) => {
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
return noCoerceMake(
// @ts-ignore
h.doAdd(...coerceLR(h, leftAmount, rightAmount)),
leftAmount.brand,
);
},
subtract: (leftAmount, rightAmount, brand = undefined) => {
const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand);
return noCoerceMake(
// @ts-ignore
h.doSubtract(...coerceLR(h, leftAmount, rightAmount)),
leftAmount.brand,
);
},
};
harden(amountMath);

// Cache the amount if we can.
const cache = new WeakSet();

/** @type {AmountMath} */
const amountMath = Far('amountMath', {
getBrand: () => brand,
getAmountMathKind: () => amountMathKind,

/**
* Make an amount from a value by adding the brand.
*
* @param {Value} allegedValue
* @returns {Amount}
*/
make: allegedValue => {
const value = helpers.doCoerce(allegedValue);
const amount = harden({ brand, value });
cache.add(amount);
return amount;
},

/**
* Make sure this amount is valid and return it if so, throwing if invalid.
*
* @param {Amount} allegedAmount
* @returns {Amount} or throws if invalid
*/
coerce: allegedAmount => {
// If the cache already has the allegedAmount, that
// means it is a valid amount.
if (cache.has(allegedAmount)) {
return allegedAmount;
}
const { brand: allegedBrand, value } = allegedAmount;
assert(
allegedBrand !== undefined,
X`The brand in allegedAmount ${allegedAmount} is undefined. Did you pass a value rather than an amount?`,
);
assert(
brand === allegedBrand,
X`The brand in the allegedAmount ${allegedAmount} in 'coerce' didn't match the amountMath brand ${brand}.`,
);
// Will throw on inappropriate value
return amountMath.make(value);
},

// Get the value from the amount.
getValue: amount => amountMath.coerce(amount).value,

// Represents the empty set/mathematical identity.
// eslint-disable-next-line no-use-before-define
getEmpty: () => empty,

// Is the amount equal to the empty set?
isEmpty: amount => helpers.doIsEmpty(amountMath.getValue(amount)),

// Is leftAmount greater than or equal to rightAmount? In other
// words, is everything in the rightAmount included in the
// leftAmount?
isGTE: (leftAmount, rightAmount) =>
helpers.doIsGTE(
amountMath.getValue(leftAmount),
amountMath.getValue(rightAmount),
),

// Is leftAmount equal to rightAmount?
isEqual: (leftAmount, rightAmount) =>
helpers.doIsEqual(
amountMath.getValue(leftAmount),
amountMath.getValue(rightAmount),
),

// Combine leftAmount and rightAmount.
add: (leftAmount, rightAmount) =>
amountMath.make(
helpers.doAdd(
amountMath.getValue(leftAmount),
amountMath.getValue(rightAmount),
),
),

// Return the amount included in leftAmount but not included in
// rightAmount. If leftAmount does not include all of rightAmount,
// error.
subtract: (leftAmount, rightAmount) =>
amountMath.make(
helpers.doSubtract(
amountMath.getValue(leftAmount),
amountMath.getValue(rightAmount),
),
),
});
const empty = amountMath.make(helpers.doGetEmpty());
return amountMath;
}

harden(makeAmountMath);

export { makeAmountMath };
export { amountMath, MathKind, makeAmountMath };
Loading

0 comments on commit 1620307

Please sign in to comment.