UNPKG

@agoric/ertp

Version:

Electronic Rights Transfer Protocol (ERTP). A smart contract framework for exchanging electronic rights

396 lines (380 loc) • 13.7 kB
// @ts-check import { passStyleOf, assertRemotable, assertRecord } from '@endo/marshal'; import './types.js'; import { M, matches } from '@agoric/store'; import { natMathHelpers } from './mathHelpers/natMathHelpers.js'; import { setMathHelpers } from './mathHelpers/setMathHelpers.js'; import { copySetMathHelpers } from './mathHelpers/copySetMathHelpers.js'; import { copyBagMathHelpers } from './mathHelpers/copyBagMathHelpers.js'; const { details: X, quote: q } = assert; /** * Constants for the kinds of assets we support. * * @type {{ NAT: 'nat', SET: 'set', COPY_SET: 'copySet', COPY_BAG: 'copyBag' }} */ const AssetKind = harden({ NAT: 'nat', SET: 'set', COPY_SET: 'copySet', COPY_BAG: 'copyBag', }); const assetKindNames = harden(Object.values(AssetKind).sort()); /** @type {AssertAssetKind} */ const assertAssetKind = allegedAK => { assetKindNames.includes(allegedAK) || assert.fail( X`The assetKind ${allegedAK} must be one of ${q(assetKindNames)}`, ); }; harden(assertAssetKind); /** * Amounts describe digital assets. From an amount, you can learn the * brand of digital asset as well as "how much" or "how many". Amounts * have two parts: a brand (loosely speaking, the type of digital * asset) and the value (the answer to "how much"). For example, in * the phrase "5 bucks", "bucks" takes the role of the brand and the * value is 5. Amounts can describe fungible and non-fungible digital * assets. Amounts are pass-by-copy and can be made by and sent to * anyone. * * The issuer is the authoritative source of the amount in payments * and purses. The issuer must be able to do things such as add * digital assets to a purse and withdraw digital assets from a purse. * To do so, it must know how to add and subtract digital assets. * Rather than hard-coding a particular solution, we chose to * parameterize the issuer with a collection of polymorphic functions, * which we call `AmountMath`. These math functions include concepts * like addition, subtraction, and greater than or equal to. * * We also want to make sure there is no confusion as to what kind of * asset we are using. Thus, AmountMath includes checks of the * `brand`, the unique identifier for the type of digital asset. If * the wrong brand is used in AmountMath, an error is thrown and the * operation does not succeed. * * AmountMath uses mathHelpers to do most of the work, but then adds * the brand to the result. The function `value` gets the value from * the amount by removing the brand (amount -> value), and the * function `make` adds the brand to produce an amount (value -> * amount). The function `coerce` takes an amount and checks it, * returning an amount (amount -> amount). * * 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. */ /** @type {{ * nat: NatMathHelpers, * set: SetMathHelpers, * copySet: CopySetMathHelpers, * copyBag: CopyBagMathHelpers * }} */ const helpers = { nat: natMathHelpers, set: setMathHelpers, copySet: copySetMathHelpers, copyBag: copyBagMathHelpers, }; /** * @template {AmountValue} V * @type {(value: V) => AssetKindForValue<V>} */ const assertValueGetAssetKind = value => { const passStyle = passStyleOf(value); if (passStyle === 'bigint') { // @ts-expect-error cast return 'nat'; } if (passStyle === 'copyArray') { // @ts-expect-error cast return 'set'; } if (matches(value, M.set())) { // @ts-expect-error cast return 'copySet'; } if (matches(value, M.bag())) { // @ts-expect-error cast return 'copyBag'; } assert.fail( // TODO This isn't quite the right error message, in case valuePassStyle // is 'tagged'. We would need to distinguish what kind of tagged // object it is. // Also, this kind of manual listing is a maintenance hazard we // (TODO) will encounter when we extend the math helpers further. X`value ${value} must be a bigint, copySet, copyBag, or an array, not ${passStyle}`, ); }; /** * * Asserts that value is a valid AmountMath and returns the appropriate helpers. * * Made available only for testing, but it is harmless for other uses. * * @template {AmountValue} V * @param {V} value * @returns {MathHelpers<V>} */ export const assertValueGetHelpers = value => // @ts-expect-error cast helpers[assertValueGetAssetKind(value)]; /** @type {(allegedBrand: Brand, brand?: Brand) => void} */ const optionalBrandCheck = (allegedBrand, brand) => { if (brand !== undefined) { assertRemotable(brand, 'brand'); assert.equal( allegedBrand, brand, X`amount's brand ${allegedBrand} did not match expected brand ${brand}`, ); } }; /** * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} leftAmount * @param {Amount<K>} rightAmount * @param {Brand<K> | undefined} brand * @returns {MathHelpers<*>} */ const checkLRAndGetHelpers = (leftAmount, rightAmount, brand = undefined) => { assertRecord(leftAmount, 'leftAmount'); assertRecord(rightAmount, 'rightAmount'); const { value: leftValue, brand: leftBrand } = leftAmount; const { value: rightValue, brand: rightBrand } = rightAmount; assertRemotable(leftBrand, 'leftBrand'); assertRemotable(rightBrand, 'rightBrand'); optionalBrandCheck(leftBrand, brand); optionalBrandCheck(rightBrand, brand); assert.equal( leftBrand, rightBrand, X`Brands in left ${leftBrand} and right ${rightBrand} should match but do not`, ); const leftHelpers = assertValueGetHelpers(leftValue); const rightHelpers = assertValueGetHelpers(rightValue); assert.equal( leftHelpers, rightHelpers, X`The left ${leftAmount} and right amount ${rightAmount} had different assetKinds`, ); return leftHelpers; }; /** * @template {AssetKind} K * @param {MathHelpers<AssetValueForKind<K>>} h * @param {Amount<K>} leftAmount * @param {Amount<K>} rightAmount * @returns {[K, K]} */ const coerceLR = (h, leftAmount, rightAmount) => { // @ts-expect-error could be arbitrary subtype return [h.doCoerce(leftAmount.value), h.doCoerce(rightAmount.value)]; }; /** * Logic for manipulating amounts. * * Amounts are the canonical description of tradable goods. They are manipulated * by issuers and mints, and represent the goods and currency carried by purses * and * payments. They can be used to represent things like currency, stock, and the * abstract right to participate in a particular exchange. */ const AmountMath = { /** * Make an amount from a value by adding the brand. * * @template {AssetKind} [K=AssetKind] * @param {Brand<K>} brand * @param {AssetValueForKind<K>} allegedValue * @returns {Amount<K>} */ // allegedValue has a conditional expression for type widening, to prevent V being bound to a a literal like 1n make: (brand, allegedValue) => { assertRemotable(brand, 'brand'); const h = assertValueGetHelpers(allegedValue); const value = h.doCoerce(allegedValue); return harden({ brand, value }); }, /** * Make sure this amount is valid enough, and return a corresponding * valid amount if so. * * @template {AssetKind} [K=AssetKind] * @param {Brand} brand * @param {Amount<K>} allegedAmount * @returns {Amount<K>} */ coerce: (brand, allegedAmount) => { assertRemotable(brand, 'brand'); assertRecord(allegedAmount, 'amount'); const { brand: allegedBrand, value: allegedValue } = allegedAmount; brand === allegedBrand || assert.fail( X`The brand in the allegedAmount ${allegedAmount} in 'coerce' didn't match the specified brand ${brand}.`, ); // Will throw on inappropriate value return AmountMath.make(brand, allegedValue); }, /** * Extract and return the value. * * @template {AssetKind} [K=AssetKind] * @param {Brand<K>} brand * @param {Amount<K>} amount * @returns {AssetValueForKind<K>} */ getValue: (brand, amount) => AmountMath.coerce(brand, amount).value, /** * Return the amount representing an empty amount. This is the * identity element for MathHelpers.add and MatHelpers.subtract. * * @template {AssetKind} [K='nat'] * @param {Brand<K>} brand * @param {K} [assetKind] * @returns {Amount<K>} */ // @ts-expect-error TS/jsdoc thinks 'nat' can't be assigned to K subclassing AssetKind // If we were using TypeScript we'd simply overload the function definition for each case. makeEmpty: (brand, assetKind = 'nat') => { assertRemotable(brand, 'brand'); assertAssetKind(assetKind); const value = helpers[assetKind].doMakeEmpty(); // @ts-expect-error TS/jsdoc thinks 'nat' can't be assigned to K subclassing AssetKind return harden({ brand, value }); }, /** * Return the amount representing an empty amount, using another * amount as the template for the brand and assetKind. * * @template {AssetKind} K * @param {Amount<K>} amount * @returns {Amount<K>} */ makeEmptyFromAmount: amount => { assertRecord(amount, 'amount'); const { brand, value } = amount; // @ts-expect-error cast const assetKind = assertValueGetAssetKind(value); // @ts-expect-error cast (ignore b/c erroring in CI but not my IDE) return AmountMath.makeEmpty(brand, assetKind); }, /** * Return true if the Amount is empty. Otherwise false. * * @param {Amount} amount * @param {Brand=} brand * @returns {boolean} */ isEmpty: (amount, brand = undefined) => { assertRecord(amount, 'amount'); const { brand: allegedBrand, value } = amount; assertRemotable(allegedBrand, 'brand'); optionalBrandCheck(allegedBrand, brand); const h = assertValueGetHelpers(value); return h.doIsEmpty(h.doCoerce(value)); }, /** * Returns true if the leftAmount is greater than or equal to the * rightAmount. For non-scalars, "greater than or equal to" depends * on the kind of amount, as defined by the MathHelpers. For example, * whether rectangle A is greater than rectangle B depends on whether rectangle * A includes rectangle B as defined by the logic in MathHelpers. * * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} leftAmount * @param {Amount<K>} rightAmount * @param {Brand<K>=} brand * @returns {boolean} */ isGTE: (leftAmount, rightAmount, brand = undefined) => { const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); return h.doIsGTE(...coerceLR(h, leftAmount, rightAmount)); }, /** * Returns true if the leftAmount equals the rightAmount. We assume * that if isGTE is true in both directions, isEqual is also true * * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} leftAmount * @param {Amount<K>} rightAmount * @param {Brand<K>=} brand * @returns {boolean} */ isEqual: (leftAmount, rightAmount, brand = undefined) => { const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); return h.doIsEqual(...coerceLR(h, leftAmount, rightAmount)); }, /** * Returns a new amount that is the union of both leftAmount and rightAmount. * * For fungible amount this means adding the values. For other kinds of * amount, it usually means including all of the elements from both * left and right. * * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} leftAmount * @param {Amount<K>} rightAmount * @param {Brand<K>=} brand * @returns {Amount<K>} */ add: (leftAmount, rightAmount, brand = undefined) => { const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); const value = h.doAdd(...coerceLR(h, leftAmount, rightAmount)); return harden({ brand: leftAmount.brand, value }); }, /** * Returns a new amount that is the leftAmount minus the rightAmount * (i.e. everything in the leftAmount that is not in the * rightAmount). If leftAmount doesn't include rightAmount * (subtraction results in a negative), throw an error. Because the * left amount must include the right amount, this is NOT equivalent * to set subtraction. * * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} leftAmount * @param {Amount<K>} rightAmount * @param {Brand<K>=} brand * @returns {Amount<K>} */ subtract: (leftAmount, rightAmount, brand = undefined) => { const h = checkLRAndGetHelpers(leftAmount, rightAmount, brand); const value = h.doSubtract(...coerceLR(h, leftAmount, rightAmount)); return harden({ brand: leftAmount.brand, value }); }, /** * Returns the min value between x and y using isGTE * * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} x * @param {Amount<K>} y * @param {Brand<K>=} brand * @returns {Amount<K>} */ min: (x, y, brand = undefined) => (AmountMath.isGTE(x, y, brand) ? y : x), /** * Returns the max value between x and y using isGTE * * @template {AssetKind} [K=AssetKind] * @param {Amount<K>} x * @param {Amount<K>} y * @param {Brand<K>=} brand * @returns {Amount<K>} */ max: (x, y, brand = undefined) => (AmountMath.isGTE(x, y, brand) ? x : y), }; harden(AmountMath); /** * * @param {Amount} amount */ const getAssetKind = amount => { assertRecord(amount, 'amount'); const { value } = amount; // @ts-expect-error cast (ignore b/c erroring in CI but not my IDE) return assertValueGetAssetKind(value); }; harden(getAssetKind); export { AmountMath, AssetKind, getAssetKind, assertAssetKind };