diff --git a/packages/ERTP/src/amountMath.js b/packages/ERTP/src/amountMath.js index 665f0185ae8..4f7ec4a17ba 100644 --- a/packages/ERTP/src/amountMath.js +++ b/packages/ERTP/src/amountMath.js @@ -1,8 +1,12 @@ // @ts-check -import { assert, details } from '@agoric/assert'; +import { assert, details as d } from '@agoric/assert'; -import { mustBeComparable } from '@agoric/same-structure'; +import { + mustBeComparable, + patternKindOf, + STAR_PATTERN, +} from '@agoric/same-structure'; import './types'; import natMathHelpers from './mathHelpers/natMathHelpers'; @@ -87,7 +91,7 @@ function makeAmountMath(brand, amountMathKind) { const helpers = mathHelpers[amountMathKind]; assert( helpers !== undefined, - details`unrecognized amountMathKind: ${amountMathKind}`, + d`unrecognized amountMathKind: ${amountMathKind}`, ); // Cache the amount if we can. @@ -126,11 +130,11 @@ function makeAmountMath(brand, amountMathKind) { const { brand: allegedBrand, value } = allegedAmount; assert( allegedBrand !== undefined, - details`alleged brand is undefined. Did you pass a value rather than an amount?`, + d`alleged brand is undefined. Did you pass a value rather than an amount?`, ); assert( brand === allegedBrand, - details`the brand in the allegedAmount in 'coerce' didn't match the amountMath brand`, + d`the brand in the allegedAmount in 'coerce' didn't match the amountMath brand`, ); // Will throw on inappropriate value return amountMath.make(value); @@ -181,6 +185,69 @@ function makeAmountMath(brand, amountMathKind) { amountMath.getValue(rightAmount), ), ), + + /** + * TODO explain. + * + * @param {ValuePattern} valuePattern + * @returns {AmountPattern} + */ + makePattern: valuePattern => { + mustBeComparable(valuePattern); + const patternKind = patternKindOf(valuePattern); + if (patternKind === undefined) { + return amountMath.make(valuePattern); + } + return harden({ brand, value: valuePattern }); + }, + + /** + * TODO explain. + * + * @returns {AmountPattern} + */ + makeStarPattern: () => { + return amountMath.makePattern(STAR_PATTERN); + }, + + /** + * TODO explain. + * + * @param {AmountPattern} allegedAmountPattern + * @returns {AmountPattern} or throws if invalid + */ + coercePattern: allegedAmountPattern => { + const { brand: allegedBrand, value: valuePattern } = allegedAmountPattern; + assert( + allegedBrand !== undefined, + d`alleged brand is undefined. Did you pass a value rather than an amount?`, + ); + assert( + brand === allegedBrand, + d`the brand in the allegedAmount in 'coerce' didn't match the amountMath brand`, + ); + // Will throw on inappropriate value + return amountMath.makePattern(valuePattern); + }, + + // TODO explain. + getValuePattern: amountPattern => + amountMath.coercePattern(amountPattern).value, + + frugalSplit: (pattern, specimen) => { + const split = helpers.doFrugalSplit( + amountMath.getValuePattern(pattern), + amountMath.getValue(specimen), + ); + if (split === undefined) { + return undefined; + } + const { matched: valueMatched, change: valueChange } = split; + return harden({ + matched: amountMath.make(valueMatched), + change: amountMath.make(valueChange), + }); + }, }); const empty = amountMath.make(helpers.doGetEmpty()); return amountMath; diff --git a/packages/ERTP/src/mathHelpers/natMathHelpers.js b/packages/ERTP/src/mathHelpers/natMathHelpers.js index 16611189819..fe523070b56 100644 --- a/packages/ERTP/src/mathHelpers/natMathHelpers.js +++ b/packages/ERTP/src/mathHelpers/natMathHelpers.js @@ -1,6 +1,8 @@ // @ts-check import Nat from '@agoric/nat'; +import { assert, details as d, q } from '@agoric/assert'; +import { patternKindOf } from '@agoric/same-structure'; import '../types'; @@ -26,6 +28,31 @@ const natMathHelpers = harden({ doIsEqual: (left, right) => left === right, doAdd: (left, right) => Nat(left + right), doSubtract: (left, right) => Nat(left - right), + + doFrugalSplit: (pattern, specimen) => { + const patternKind = patternKindOf(pattern); + if (patternKind === undefined) { + Nat(pattern); + if (specimen >= pattern) { + return harden({ + matched: pattern, + change: specimen - pattern, + }); + } + return undefined; + } + switch (patternKind) { + case '*': { + return harden({ + matched: identity, + change: specimen, + }); + } + default: { + throw assert.fail(d`Unexpected patternKind ${q(patternKind)}`); + } + } + }, }); harden(natMathHelpers); diff --git a/packages/ERTP/src/mathHelpers/setMathHelpers.js b/packages/ERTP/src/mathHelpers/setMathHelpers.js index f924ed64916..f5974ad79f0 100644 --- a/packages/ERTP/src/mathHelpers/setMathHelpers.js +++ b/packages/ERTP/src/mathHelpers/setMathHelpers.js @@ -1,11 +1,13 @@ // @ts-check import { passStyleOf } from '@agoric/marshal'; -import { assert, details } from '@agoric/assert'; -import { sameStructure } from '@agoric/same-structure'; +import { assert, details as d } from '@agoric/assert'; +import { sameStructure, isGround, match } from '@agoric/same-structure'; import '../types'; +const { entries } = Object; + // Operations for arrays with unique objects identifying and providing // information about digital assets. Used for Zoe invites. const identity = harden([]); @@ -43,7 +45,7 @@ const checkForDupes = buckets => { for (let j = i + 1; j < maybeMatches.length; j += 1) { assert( !sameStructure(maybeMatches[i], maybeMatches[j]), - details`value has duplicates: ${maybeMatches[i]} and ${maybeMatches[j]}`, + d`value has duplicates: ${maybeMatches[i]} and ${maybeMatches[j]}`, ); } } @@ -92,12 +94,44 @@ const setMathHelpers = harden({ right.forEach(rightElem => { assert( hasElement(leftBuckets, rightElem), - details`right element ${rightElem} was not in left`, + d`right element ${rightElem} was not in left`, ); }); const leftElemNotInRight = leftElem => !hasElement(rightBuckets, leftElem); return harden(left.filter(leftElemNotInRight)); }, + + // Do a case split among easy cases, and error on the rest for now. + // TODO Actually implement this correctly. + doFrugalSplit: (pattern, specimen) => { + if (isGround(pattern)) { + if (setMathHelpers.doIsGTE(specimen, pattern)) { + return harden({ + matched: pattern, + change: setMathHelpers.doSubtract(specimen, pattern), + }); + } + return undefined; + } + // Check for the special case where the pattern is a singleton array + if (Array.isArray(pattern) && pattern.length === 1) { + const subPattern = pattern[0]; + for (const [i, elem] of entries(specimen)) { + if (match(subPattern, elem)) { + // This just takes the first match, which is rather arbitrary. + // At the level of abstraction where these lists represent + // unordered sets, this is choice is non-deterministic, which + // is inevitable. + return harden({ + matched: [elem], + change: [...specimen.slice(0, i), ...specimen.slice(i + 1)], + }); + } + } + return undefined; + } + throw assert.fail(d`Only singleton patterns supported for now`); + }, }); harden(setMathHelpers); diff --git a/packages/ERTP/src/mathHelpers/strSetMathHelpers.js b/packages/ERTP/src/mathHelpers/strSetMathHelpers.js index 812f24b1adf..81af18e5bb5 100644 --- a/packages/ERTP/src/mathHelpers/strSetMathHelpers.js +++ b/packages/ERTP/src/mathHelpers/strSetMathHelpers.js @@ -1,13 +1,14 @@ // @ts-check import { passStyleOf } from '@agoric/marshal'; -import { assert, details } from '@agoric/assert'; +import { assert, details as d, q } from '@agoric/assert'; +import { patternKindOf } from '@agoric/same-structure'; const identity = harden([]); const checkForDupes = list => { const set = new Set(list); - assert(set.size === list.length, details`value has duplicates: ${list}`); + assert(set.size === list.length, d`value has duplicates: ${list}`); }; /** @@ -43,10 +44,7 @@ const strSetMathHelpers = harden({ doAdd: (left, right) => { const union = new Set(left); const addToUnion = elem => { - assert( - !union.has(elem), - details`left and right have same element ${elem}`, - ); + assert(!union.has(elem), d`left and right have same element ${elem}`); union.add(elem); }; right.forEach(addToUnion); @@ -58,10 +56,68 @@ const strSetMathHelpers = harden({ const allRemovedCorrectly = right.every(remove); assert( allRemovedCorrectly, - details`some of the elements in right (${right}) were not present in left (${left})`, + d`some of the elements in right (${right}) were not present in left (${left})`, ); return harden(Array.from(leftSet)); }, + + // TODO Reform awful code! + // Expanded this in place this way only as part of an expedient + // spike. It is indeed a horrible clump of code that must be broken up. + doFrugalSplit: (pattern, specimen) => { + const patternKind = patternKindOf(pattern); + if (patternKind === undefined) { + const changeSet = new Set(specimen); + let starCount = 0; + const remove = subPattern => { + const subPatternKind = patternKindOf(subPattern); + if (subPatternKind === undefined) { + return changeSet.delete(subPattern); + } + switch (subPatternKind) { + case '*': { + starCount += 1; + break; + } + default: { + throw assert.fail( + d`Unexpected subPatternKind ${q(subPatternKind)}`, + ); + } + } + return true; + }; + if (!pattern.every(remove)) { + return undefined; + } + if (changeSet.size < starCount) { + return undefined; + } + for (const choice of changeSet) { + changeSet.delete(choice); + starCount -= 1; + if (starCount === 0) { + break; + } + } + const change = harden(Array.from(changeSet)); + return harden({ + matched: strSetMathHelpers.doSubtract(specimen, change), + change, + }); + } + switch (patternKind) { + case '*': { + return harden({ + matched: identity, + change: specimen, + }); + } + default: { + throw assert.fail(d`Unexpected patternKind ${q(patternKind)}`); + } + } + }, }); harden(strSetMathHelpers); diff --git a/packages/ERTP/src/types.js b/packages/ERTP/src/types.js index ff01e9d8125..94afa7d4d85 100644 --- a/packages/ERTP/src/types.js +++ b/packages/ERTP/src/types.js @@ -12,7 +12,7 @@ */ /** - * @typedef {Object} Amount + * @typedef {Ground} Amount * Amounts are descriptions of digital assets, answering the questions * "how much" and "of what kind". Amounts are values labeled with a brand. * AmountMath executes the logic of how amounts are changed when digital @@ -27,27 +27,52 @@ */ /** - * @typedef {any} Value + * @typedef {Ground} Value * Values describe the value of something that can be owned or shared. * Fungible values are normally represented by natural numbers. Other * values may be represented as strings naming a particular right, or * an arbitrary object that sensibly represents the rights at issue. * - * Value must be Comparable. (Would be nice to type this correctly.) + * Value must be Comparable. + */ + +/** + * @typedef {Amount & Pattern} AmountPattern + * TODO explain + * + * @property {Brand} brand + * @property {ValuePattern} value + */ + +/** + * @typedef {Value & Pattern} ValuePattern + * TODO explain */ /** * @typedef {'nat' | 'set' | 'strSet'} AmountMathKind */ +/** + * @typedef AmountSplit + * @property {Amount} matched + * @property {Amount} change + */ + +/** + * @typedef ValueSplit + * @property {Value} matched + * @property {Value} change + */ + /** * @typedef {Object} AmountMath * 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. + * 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. * * @property {() => Brand} getBrand Return the brand. * @property {() => AmountMathKind} getAmountMathKind @@ -95,6 +120,25 @@ * (subtraction results in a negative), throw an error. Because the * left amount must include the right amount, this is NOT equivalent * to set subtraction. + * + * + * @property {(valuePattern: ValuePattern) => AmountPattern} makePattern + * TODO explain + * + * @property {() => AmountPattern} makeStarPattern + * TODO explain + * + * @property {(allegedAmountPattern: AmountPattern) => AmountPattern} coercePattern + * TODO explain + * + * @property {(amountPattern: AmountPattern) => ValuePattern} getValuePattern + * TODO explain + * + * @property {(pattern: AmountPattern, specimen: Amount) => AmountSplit|undefined} frugalSplit + * Try to find a smallish portion of the specimen that matches the pattern. + * The smaller the better. If successful, return an AmountSplit with + * the matched portion in `matched` and the remainder in `change`. The + * specific deterministic rules will depend on a given MathKind. */ /** @@ -335,6 +379,12 @@ * @property {(left: Value, right: Value) => Value} doSubtract * Return what remains after removing the right from the left. If * something in the right was not in the left, we throw an error. + * + * @property {(pattern: ValuePattern, specimen: Value) => ValueSplit|undefined} doFrugalSplit + * Try to find a smallish portion of the specimen that matches the pattern. + * The smaller the better. If successful, return a ValueSplit with + * the matched portion in `matched` and the remainder in `change`. The + * specific deterministic rules will depend on a given MathKind. */ /** diff --git a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js index 116d26b4e99..1b9ec27501e 100644 --- a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js +++ b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js @@ -1,5 +1,6 @@ import test from 'ava'; +import { PATTERN } from '@agoric/same-structure'; import { makeAmountMath, MathKind } from '../../../src'; // The "unit tests" for MathHelpers actually make the calls through @@ -13,7 +14,7 @@ const mockBrand = harden({ const amountMath = makeAmountMath(mockBrand, MathKind.NAT); -test('natMathHelpers', t => { +test('natMathHelpers ground', t => { const { getBrand, getAmountMathKind, @@ -115,3 +116,25 @@ test('natMathHelpers', t => { // subtract t.deepEqual(subtract(make(6), make(1)), make(5), `6 - 1 = 5`); }); + +test('natMathHelpers patterns', t => { + const { + makePattern, + makeStarPattern, + // coercePattern, TODO + // getValuePattern, TODO + // frugalSplit, TODO + } = amountMath; + + // makePattern + t.deepEqual(makePattern(harden({ [PATTERN]: '*' })), { + brand: mockBrand, + value: { [PATTERN]: '*' }, + }); + + // makeStarPattern + t.deepEqual(makeStarPattern(), { + brand: mockBrand, + value: { [PATTERN]: '*' }, + }); +}); diff --git a/packages/same-structure/index.js b/packages/same-structure/index.js index d6eac589bbe..964f9944236 100644 --- a/packages/same-structure/index.js +++ b/packages/same-structure/index.js @@ -4,3 +4,10 @@ export { mustBeSameStructure, mustBeComparable, } from './src/sameStructure'; +export { + PATTERN, + STAR_PATTERN, + patternKindOf, + match, + isGround, +} from './src/match'; diff --git a/packages/same-structure/src/match.js b/packages/same-structure/src/match.js new file mode 100644 index 00000000000..89e42ed9335 --- /dev/null +++ b/packages/same-structure/src/match.js @@ -0,0 +1,195 @@ +// @ts-check + +import { sameValueZero, passStyleOf, REMOTE_STYLE } from '@agoric/marshal'; +import { assert, details as d, q } from '@agoric/assert'; +import { sameStructure } from './sameStructure'; + +import './types'; + +const { values } = Object; + +/** + * Special property name within a copyRecord that marks the copyRecord as + * representing a non-literal pattern record. See `isGround`. + */ +const PATTERN_KIND = '@pattern'; + +/** + * A pattern record that matches any specimen, i.e., a wildcard. + */ +const STAR_PATTERN = harden({ [PATTERN_KIND]: '*' }); + +/** + * If `passable` is a pattern record, return the string identifying what kind + * of pattern this pattern record represents. Else return undefined. + * + * @param {Passable} passable + */ +const patternKindOf = passable => { + const passStyle = passStyleOf(passable); + if (passStyle === 'copyRecord' && PATTERN_KIND in passable) { + const patternKind = passable[PATTERN_KIND]; + assert.string(patternKind); + return patternKind; + } + return undefined; +}; +harden(patternKindOf); + +/** + * A *passable* object is a pass-by-copy superstructure ending in + * non-pass-by-copy leaves, each of which is either a promise or a + * REMOTE_STYLE. A passable object in which none of the leaves are promises + * is a *comparable*. A comparable object in which none of the + * copyRecords are pattern records is *ground*. All other comparables are + * *non-ground*. + * + * Only some contexts care about the distinction between ground and non-ground, + * such as the arguments to the `match` function below. For most other purposes, + * these are simply passable and comparable objects. However, some uses of + * `sameStructure` to compare comparables should probably either be guarded by + * `isGround` tests or converted to `match` calls. + * + * @param {Passable} passable + * @returns {boolean} + */ +function isGround(passable) { + const passStyle = passStyleOf(passable); + switch (passStyle) { + case 'null': + case 'undefined': + case 'string': + case 'boolean': + case 'number': + case 'bigint': + case REMOTE_STYLE: + case 'copyError': { + return true; + } + case 'promise': { + return false; + } + case 'copyArray': { + return passable.every(isGround); + } + case 'copyRecord': { + const patternKind = patternKindOf(passable); + if (patternKind !== undefined) { + return false; + } + return values(passable).every(isGround); + } + default: { + throw new TypeError(`unrecognized passStyle ${passStyle}`); + } + } +} +harden(isGround); + +/** + * @param {Pattern} outerPattern + * @param {Ground} outerSpecimen + * @returns {Bindings | undefined} + */ +function match(outerPattern, outerSpecimen) { + assert( + isGround(outerSpecimen), + d`Can only match against ground comparables for now`, + ); + + // Although it violates Jessie, don't harden `bindings` yet + const bindings = { __proto__: null }; + + // TODO Reduce redundancy with sameStructure. + function matchInternal(pattern, specimen) { + const patternKind = patternKindOf(pattern); + if (patternKind !== undefined) { + switch (patternKind) { + case '*': { + // wildcard. matches anything. + return true; + } + case 'bind': { + const { name } = pattern; + // binds specimen to bindings[name] + assert.string(name); + if (name in bindings) { + // Note: sameStructure rather than match, as both sides came from + // the outerSpecimen. + return sameStructure(bindings[name], specimen); + } + bindings[name] = specimen; + return true; + } + default: { + throw assert.fail(d`unrecognized pattern kind ${q(patternKind)}`); + } + } + } + const patternStyle = passStyleOf(pattern); + const specimenStyle = passStyleOf(specimen); + assert( + patternStyle !== 'promise', + d`Cannot structurally compare promises: ${pattern}`, + ); + assert( + specimenStyle !== 'promise', + d`Cannot structurally compare promises: ${specimen}`, + ); + + if (patternStyle !== specimenStyle) { + return false; + } + switch (patternStyle) { + case 'null': + case 'undefined': + case 'string': + case 'boolean': + case 'number': + case 'bigint': + case REMOTE_STYLE: { + return sameValueZero(pattern, specimen); + } + case 'copyRecord': + case 'copyArray': { + const leftNames = Object.getOwnPropertyNames(pattern); + const rightNames = Object.getOwnPropertyNames(specimen); + if (leftNames.length !== rightNames.length) { + return false; + } + for (const name of leftNames) { + // TODO: Better hasOwnProperty check + if (!Object.getOwnPropertyDescriptor(specimen, name)) { + return false; + } + // TODO: Make cycle tolerant + if (!match(pattern[name], specimen[name])) { + return false; + } + } + return true; + } + case 'copyError': { + return ( + pattern.name === specimen.name && pattern.message === specimen.message + ); + } + default: { + throw new TypeError(`unrecognized passStyle ${patternStyle}`); + } + } + } + if (matchInternal(outerPattern, outerSpecimen)) { + return harden(bindings); + } + return undefined; +} +harden(match); + +export { + PATTERN_KIND as PATTERN, + STAR_PATTERN, + patternKindOf, + match, + isGround, +}; diff --git a/packages/same-structure/src/sameStructure.js b/packages/same-structure/src/sameStructure.js index fbad09c7e6a..7b97859dfc7 100644 --- a/packages/same-structure/src/sameStructure.js +++ b/packages/same-structure/src/sameStructure.js @@ -1,8 +1,12 @@ // @ts-check +// @ts-check + import { sameValueZero, passStyleOf, REMOTE_STYLE } from '@agoric/marshal'; import { assert, details, q } from '@agoric/assert'; +import './types'; + // Shim of Object.fromEntries from // https://github.com/tc39/proposal-object-from-entries/blob/master/polyfill.js // TODO reconcile and dedup with the Object.fromEntries ponyfill in diff --git a/packages/same-structure/src/types.js b/packages/same-structure/src/types.js new file mode 100644 index 00000000000..78a1e15ada9 --- /dev/null +++ b/packages/same-structure/src/types.js @@ -0,0 +1,40 @@ +/** + * @typedef {"*" | "bind" | string} PatternKind + */ + +/** + * @typedef {Comparable} PatternRecord + * + * A pass-by-copy record with a property named '@pattern' (aka `PATTERN_KIND`) + * whose value is a PatternKind. + * TODO How do I declare a property whose name is not an indentifier? + */ + +/** + * @typedef {Comparable} Ground + * + * A Comparable is Ground if its pass-by-copy superstructure has no + * PatternRecords. Therefore, a Passable is only Ground if it has + * neither PatternRecords nor Promises. + */ + +/** + * @typedef {Comparable} Pattern + * + * We say that a Comparable is a Pattern when it is used in a context + * sensitive to whether it is ground. + * + * In these contexts, a Pattern represents the abstract set of Ground + * Comparables that match it. If the Pattern is itself Ground, then it matches + * only Ground Comparables that are `sameStructure` equivalent to it. If + * the Pattern is non-Ground, then it matches or not according to + * the Pattern's embedded PatternRecords. + */ + +/** + * @typedef {Record} Bindings + * + * The result of a successful match is typically an empty object. But the + * PatternRecords may extract corresponding portions of the specimen + * it is matched against. + */ diff --git a/packages/zoe/src/cleanProposal.js b/packages/zoe/src/cleanProposal.js index 7a48bd139cc..8f096cd7e0f 100644 --- a/packages/zoe/src/cleanProposal.js +++ b/packages/zoe/src/cleanProposal.js @@ -51,7 +51,7 @@ const cleanKeys = (allowedKeys, record) => { export const getKeywords = keywordRecord => harden(Object.getOwnPropertyNames(keywordRecord)); -export const coerceAmountKeywordRecord = ( +const coerceAmountKeywordRecord = ( getAmountMath, allegedAmountKeywordRecord, ) => { @@ -69,6 +69,24 @@ export const coerceAmountKeywordRecord = ( return arrayToObj(coercedAmounts, keywords); }; +const coerceAmountPatternKeywordRecord = ( + getAmountMath, + allegedAmountPatternKeywordRecord, +) => { + const keywords = getKeywords(allegedAmountPatternKeywordRecord); + keywords.forEach(assertKeywordName); + + const amountPatterns = Object.values(allegedAmountPatternKeywordRecord); + // Check that each pattern can be coerced using the amountMath + // indicated by brand. `AmountMath.coercePattern` throws if coercion fails. + const coercedAmountpatterns = amountPatterns.map(amountPattern => + getAmountMath(amountPattern.brand).coercePattern(amountPattern), + ); + + // Recreate the amountPatternKeywordRecord with coercedAmountPatterns. + return arrayToObj(coercedAmountpatterns, keywords); +}; + export const cleanKeywords = keywordRecord => { // `getOwnPropertyNames` returns all the non-symbol properties // (both enumerable and non-enumerable). @@ -112,7 +130,7 @@ export const cleanProposal = (getAmountMath, proposal) => { let { want = harden({}), give = harden({}) } = proposal; const { exit = harden({ onDemand: null }) } = proposal; - want = coerceAmountKeywordRecord(getAmountMath, want); + want = coerceAmountPatternKeywordRecord(getAmountMath, want); give = coerceAmountKeywordRecord(getAmountMath, give); // Check exit diff --git a/packages/zoe/src/contractFacet/offerSafety.js b/packages/zoe/src/contractFacet/offerSafety.js index 69152a72bc2..d97dfb56c88 100644 --- a/packages/zoe/src/contractFacet/offerSafety.js +++ b/packages/zoe/src/contractFacet/offerSafety.js @@ -6,19 +6,23 @@ * keyword of giveOrWant? * * @param {(brand: Brand) => AmountMath} getAmountMath - * @param {AmountKeywordRecord} giveOrWant + * @param {AmountPatternKeywordRecord} giveOrWant * @param {AmountKeywordRecord} allocation */ const satisfiesInternal = (getAmountMath, giveOrWant = {}, allocation) => { - const isGTEByKeyword = ([keyword, requiredAmount]) => { + const isGTEByKeyword = ([keyword, requiredAmountPattern]) => { // If there is no allocation for a keyword, we know the giveOrWant // is not satisfied without checking further. if (allocation[keyword] === undefined) { return false; } - const amountMath = getAmountMath(requiredAmount.brand); + const amountMath = getAmountMath(requiredAmountPattern.brand); const allocationAmount = allocation[keyword]; - return amountMath.isGTE(allocationAmount, requiredAmount); + const split = amountMath.frugalSplit( + requiredAmountPattern, + allocationAmount, + ); + return split !== undefined; }; return Object.entries(giveOrWant).every(isGTEByKeyword); }; diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 1643b235d0f..cd50a2cbfda 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -208,11 +208,11 @@ export const swap = ( zcf, { seat: leftSeat, - gains: leftSeat.getProposal().want, + gains: leftSeat.getProposal().want, // TODO }, { seat: rightSeat, - gains: rightSeat.getProposal().want, + gains: rightSeat.getProposal().want, // TODO }, leftHasExitedMsg, rightHasExitedMsg, @@ -247,12 +247,12 @@ export const swapExact = ( zcf, { seat: leftSeat, - gains: leftSeat.getProposal().want, + gains: leftSeat.getProposal().want, // TODO losses: leftSeat.getProposal().give, }, { seat: rightSeat, - gains: rightSeat.getProposal().want, + gains: rightSeat.getProposal().want, // TODO losses: rightSeat.getProposal().give, }, leftHasExitedMsg, diff --git a/packages/zoe/src/contracts/auction/assertBidSeat.js b/packages/zoe/src/contracts/auction/assertBidSeat.js index c0852faacae..86bb88d3455 100644 --- a/packages/zoe/src/contracts/auction/assertBidSeat.js +++ b/packages/zoe/src/contracts/auction/assertBidSeat.js @@ -4,7 +4,7 @@ export const assertBidSeat = (zcf, sellSeat, bidSeat) => { const { maths: { Ask: bidMath, Asset: assetMath }, } = zcf.getTerms(); - const minBid = sellSeat.getProposal().want.Ask; + const minBid = sellSeat.getProposal().want.Ask; // TODO const bid = bidSeat.getAmountAllocated('Bid', minBid.brand); assert( bidMath.isGTE(bid, minBid), diff --git a/packages/zoe/src/contracts/auction/secondPriceAuction.js b/packages/zoe/src/contracts/auction/secondPriceAuction.js index 0e464bb3fb4..cdac7e1e215 100644 --- a/packages/zoe/src/contracts/auction/secondPriceAuction.js +++ b/packages/zoe/src/contracts/auction/secondPriceAuction.js @@ -70,7 +70,7 @@ const start = zcf => { const customProperties = harden({ auctionedAssets: sellSeat.getProposal().give.Asset, - minimumBid: sellSeat.getProposal().want.Ask, + minimumBid: sellSeat.getProposal().want.Ask, // TODO closesAfter, timeAuthority, }); diff --git a/packages/zoe/src/contracts/auction/secondPriceLogic.js b/packages/zoe/src/contracts/auction/secondPriceLogic.js index 0262bfc24f1..b9a3beed465 100644 --- a/packages/zoe/src/contracts/auction/secondPriceLogic.js +++ b/packages/zoe/src/contracts/auction/secondPriceLogic.js @@ -1,7 +1,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { const { give: { Asset: assetAmount }, - want: { Ask: minBid }, + want: { Ask: minBid }, // TODO } = sellSeat.getProposal(); const bidMath = zcf.getAmountMath(minBid.brand); const assetMath = zcf.getAmountMath(assetAmount.brand); diff --git a/packages/zoe/src/contracts/autoswap.js b/packages/zoe/src/contracts/autoswap.js index 2b33db4dcef..48799c16052 100644 --- a/packages/zoe/src/contracts/autoswap.js +++ b/packages/zoe/src/contracts/autoswap.js @@ -139,7 +139,7 @@ const start = async zcf => { const { give: { In: amountIn }, - want: { Out: wantedAmountOut }, + want: { Out: wantedAmountOut }, // TODO } = swapSeat.getProposal(); const outputValue = getInputPrice( @@ -168,7 +168,7 @@ const start = async zcf => { const { give: { In: amountIn }, - want: { Out: wantedAmountOut }, + want: { Out: wantedAmountOut }, // TODO } = swapSeat.getProposal(); const tradePrice = getOutputPrice( diff --git a/packages/zoe/src/contracts/barterExchange.js b/packages/zoe/src/contracts/barterExchange.js index a55aa1c9683..1996f350edc 100644 --- a/packages/zoe/src/contracts/barterExchange.js +++ b/packages/zoe/src/contracts/barterExchange.js @@ -13,7 +13,7 @@ import { trade, satisfies } from '../contractSupport'; * https://agoric.com/documentation/zoe/guide/contracts/barter-exchange.html * * The Barter Exchange only accepts offers that look like - * { give: { In: amount }, want: { Out: amount} } + * { give: { In: amount }, want: { Out: amount} } // TODO * The want amount will be matched, while the give amount is a maximum. Each * successful trader gets their `want` and may trade with counter-parties who * specify any amount up to their specified `give`. @@ -105,7 +105,7 @@ const start = zcf => { function extractOfferDetails(seat) { const { give: { In: amountIn }, - want: { Out: amountOut }, + want: { Out: amountOut }, // TODO } = seat.getProposal(); return { diff --git a/packages/zoe/src/contracts/coveredCall.js b/packages/zoe/src/contracts/coveredCall.js index 70e61dab7bf..e2d90a694b8 100644 --- a/packages/zoe/src/contracts/coveredCall.js +++ b/packages/zoe/src/contracts/coveredCall.js @@ -85,7 +85,7 @@ const start = zcf => { expirationDate: sellSeat.getProposal().exit.afterDeadline.deadline, timeAuthority: sellSeat.getProposal().exit.afterDeadline.timer, underlyingAssets: sellSeat.getProposal().give, - strikePrice: sellSeat.getProposal().want, + strikePrice: sellSeat.getProposal().want, // TODO }); return zcf.makeInvitation(exerciseOption, 'exerciseOption', customProps); }; diff --git a/packages/zoe/src/contracts/multipoolAutoswap/removeLiquidity.js b/packages/zoe/src/contracts/multipoolAutoswap/removeLiquidity.js index 066c93108e3..bf244b9cfc4 100644 --- a/packages/zoe/src/contracts/multipoolAutoswap/removeLiquidity.js +++ b/packages/zoe/src/contracts/multipoolAutoswap/removeLiquidity.js @@ -18,7 +18,7 @@ export const makeMakeRemoveLiquidityInvitation = (zcf, getPool) => { }, }); // Get the brand of the secondary token so we can identify the liquidity pool. - const secondaryBrand = seat.getProposal().want.Secondary.brand; + const secondaryBrand = seat.getProposal().want.Secondary.brand; // TODO const pool = getPool(secondaryBrand); return pool.removeLiquidity(seat); }; diff --git a/packages/zoe/src/contracts/multipoolAutoswap/swap.js b/packages/zoe/src/contracts/multipoolAutoswap/swap.js index 428661a65a4..3dff0765f4f 100644 --- a/packages/zoe/src/contracts/multipoolAutoswap/swap.js +++ b/packages/zoe/src/contracts/multipoolAutoswap/swap.js @@ -22,7 +22,7 @@ export const makeMakeSwapInvitation = ( }); const { give: { In: amountIn }, - want: { Out: wantedAmountOut }, + want: { Out: wantedAmountOut }, // TODO } = seat.getProposal(); const { brand: brandIn, value: inputValue } = amountIn; const brandOut = wantedAmountOut.brand; @@ -135,7 +135,7 @@ export const makeMakeSwapInvitation = ( // The offer's amountOut is exact; the offeredAmountIn is a max. const { give: { In: offeredAmountIn }, - want: { Out: amountOut }, + want: { Out: amountOut }, // TODO } = seat.getProposal(); const { brand: brandOut, value: outputValue } = amountOut; const brandIn = offeredAmountIn.brand; diff --git a/packages/zoe/src/contracts/sellItems.js b/packages/zoe/src/contracts/sellItems.js index 296a8ddeb27..121607ac4d5 100644 --- a/packages/zoe/src/contracts/sellItems.js +++ b/packages/zoe/src/contracts/sellItems.js @@ -69,7 +69,7 @@ const start = zcf => { const providedMoney = buyerSeat.getAmountAllocated('Money'); const { - want: { Items: wantedItems }, + want: { Items: wantedItems }, // TODO } = buyerSeat.getProposal(); // Check that the wanted items are still for sale. diff --git a/packages/zoe/src/zoeService/types.js b/packages/zoe/src/zoeService/types.js index 0fd3eed92c2..a9f50215fb3 100644 --- a/packages/zoe/src/zoeService/types.js +++ b/packages/zoe/src/zoeService/types.js @@ -131,7 +131,7 @@ * @typedef {Partial} Proposal * * @typedef {{give: AmountKeywordRecord, - * want: AmountKeywordRecord, + * want: AmountPatternKeywordRecord, * exit: ExitRule * }} ProposalRecord */ @@ -143,6 +143,13 @@ * { Asset: amountMath.make(5), Price: amountMath.make(9) } */ +/** + * @typedef {Record} AmountPatternKeywordRecord + * + * The keys are keywords, and the values are amount patterns. For example: + * { Price: amountMath.makeOpPattern(9, '<=') } + */ + /** * @typedef {Object} Waker * @property {() => void} wake diff --git a/packages/zoe/test/unitTests/contracts/test-coveredCall.js b/packages/zoe/test/unitTests/contracts/test-coveredCall.js index 036f1ddf6b3..03e440ea1f0 100644 --- a/packages/zoe/test/unitTests/contracts/test-coveredCall.js +++ b/packages/zoe/test/unitTests/contracts/test-coveredCall.js @@ -5,7 +5,7 @@ import test from 'ava'; import bundleSource from '@agoric/bundle-source'; import { E } from '@agoric/eventual-send'; -import { sameStructure } from '@agoric/same-structure'; +import { sameStructure, match, STAR_PATTERN } from '@agoric/same-structure'; import { makeLocalAmountMath } from '@agoric/ertp'; import buildManualTimer from '../../../tools/manualTimer'; @@ -337,7 +337,6 @@ test(`zoe - coveredCall - alice's deadline expires, cancelling alice and bob`, a // trick Dave? Can Dave describe what it is that he wants in the swap // offer description? test('zoe - coveredCall with swap for invitation', async t => { - t.plan(24); // Setup the environment const timer = buildManualTimer(console.log); const { moolaR, simoleanR, bucksR, moola, simoleans, bucks, zoe } = setup(); @@ -426,6 +425,27 @@ test('zoe - coveredCall with swap for invitation', async t => { t.is(optionDesc.expirationDate, 100); t.deepEqual(optionDesc.timeAuthority, timer); + const optionDescPattern = harden({ + handle: STAR_PATTERN, + instance: STAR_PATTERN, + installation: coveredCallInstallation, + description: 'exerciseOption', + underlyingAssets: { UnderlyingAsset: moola(3) }, + strikePrice: { StrikePrice: simoleans(7) }, + expirationDate: 100, + timeAuthority: timer, + }); + t.truthy(match(optionDescPattern, optionDesc)); + + const optionAmountPattern = invitationAmountMath.makePattern( + harden([optionDescPattern]), + ); + const split = invitationAmountMath.frugalSplit( + optionAmountPattern, + optionAmount, + ); + t.assert(split !== undefined); + // Let's imagine that Bob wants to create a swap to trade this // invitation for bucks. const swapIssuerKeywordRecord = harden({ @@ -491,6 +511,10 @@ test('zoe - coveredCall with swap for invitation', async t => { // Dave escrows his 1 buck with Zoe and forms his proposal const daveSwapProposal = harden({ + // TODO BUG The commented out line with optionAmountPattern is the + // one we want, but atomicSwap calls swap with the want pattern + // as the gains, which are assumed to be amounts, not amount patterns. + // want: { Asset: optionAmountPattern }, want: { Asset: optionAmount }, give: { Price: bucks(1) }, });