diff --git a/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js b/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js index c37339a6f3d8..cad1c87c02e3 100644 --- a/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js +++ b/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js @@ -270,7 +270,12 @@ const helperBehavior = { const want = proposal.want.Debt || emptyDebt; const giveDebtOnly = matches( proposal, - harden({ give: { [KW.Debt]: M.record() }, want: {}, exit: M.any() }), + harden({ + give: { [KW.Debt]: M.record() }, + want: {}, + multiples: 1n, + exit: M.any(), + }), ); // Calculate the fee, the amount to mint and the resulting debt. We'll diff --git a/packages/wallet/api/test/test-lib-wallet.js b/packages/wallet/api/test/test-lib-wallet.js index c75b4eb12fd8..48d2751f565a 100644 --- a/packages/wallet/api/test/test-lib-wallet.js +++ b/packages/wallet/api/test/test-lib-wallet.js @@ -1356,6 +1356,7 @@ test('lib-wallet can give attestations in offers', async t => { give: { Attestation: AmountMath.make(attestationBrand, 30n), }, + multiples: 1n, want: {}, }, { @@ -1365,6 +1366,7 @@ test('lib-wallet can give attestations in offers', async t => { give: { Attestation: AmountMath.make(attestationBrand, 30n), }, + multiples: 1n, want: {}, }, ]); @@ -1428,6 +1430,7 @@ test('lib-wallet can want attestations in offers', async t => { want: { Attestation: AmountMath.make(attestationBrand, 65n), }, + multiples: 1n, give: {}, }, ]); @@ -1444,6 +1447,7 @@ test('lib-wallet can want attestations in offers', async t => { give: { Attestation: AmountMath.make(attestationBrand, 65n), }, + multiples: 1n, want: {}, }, ]); @@ -1551,6 +1555,7 @@ test('addOffer invitationQuery', async t => { value: 1n, }, }, + multiples: 1n, exit: { onDemand: null, }, @@ -1672,6 +1677,7 @@ test('addOffer offer.invitation', async t => { value: 1n, }, }, + multiples: 1n, exit: { onDemand: null, }, diff --git a/packages/zoe/src/cleanProposal.js b/packages/zoe/src/cleanProposal.js index a2bf67906204..bd0111c2ed12 100644 --- a/packages/zoe/src/cleanProposal.js +++ b/packages/zoe/src/cleanProposal.js @@ -4,9 +4,11 @@ import { assertRecord } from '@endo/marshal'; import { assertKey, assertPattern, fit, isKey } from '@agoric/store'; import { FullProposalShape } from './typeGuards.js'; import { arrayToObj } from './objArrayConversion.js'; +import { natSafeMath } from './contractSupport/safeMath.js'; import './internal-types.js'; +const { values } = Object; const { ownKeys } = Reflect; export const MAX_KEYWORD_LENGTH = 100; @@ -140,6 +142,7 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => { const { want = harden({}), give = harden({}), + multiples = 1n, exit = harden({ onDemand: null }), ...rest } = proposal; @@ -155,10 +158,35 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => { const cleanedProposal = harden({ want: cleanedWant, give: cleanedGive, + multiples, exit, }); fit(cleanedProposal, FullProposalShape, 'proposal'); + if (multiples > 1n) { + for (const amount of values(cleanedGive)) { + typeof amount.value === 'bigint' || + Fail`multiples > 1 not yet implemented for non-fungibles: ${multiples} * ${amount}`; + } + } assertExit(exit); assertKeywordNotInBoth(cleanedWant, cleanedGive); return cleanedProposal; }; + +/** + * + * @param {Amount} amount + * @param {bigint} multiples + * @returns {Amount} + */ +export const scaleAmount = (amount, multiples) => { + if (multiples === 1n) { + return amount; + } + const { brand, value } = amount; + assert(value >= 1n); + typeof value === 'bigint' || + Fail`multiples > 1 not yet implemented for non-fungibles: ${multiples} * ${amount}`; + return harden({ brand, value: natSafeMath.multiply(value, multiples) }); +}; +harden(scaleAmount); diff --git a/packages/zoe/src/contractFacet/offerSafety.js b/packages/zoe/src/contractFacet/offerSafety.js index 6171b00287d7..6d90808710f1 100644 --- a/packages/zoe/src/contractFacet/offerSafety.js +++ b/packages/zoe/src/contractFacet/offerSafety.js @@ -1,34 +1,55 @@ import { AmountMath } from '@agoric/ertp'; +import { natSafeMath } from '../contractSupport/safeMath.js'; + +const { details: X } = assert; +const { entries } = Object; /** - * Helper to perform satisfiesWant and satisfiesGive. Is - * allocationAmount greater than or equal to requiredAmount for every - * keyword of giveOrWant? + * Helper to perform numWantsSatisfied and satisfiesGive. How many times + * does the `allocation` satisfy the `giveOrWant`? * - * To prepare for multiples, satisfiesWant and satisfiesGive return 0 or 1. - * isOfferSafe will still be boolean. When we have Multiples, satisfiesWant and + * To prepare for multiples, numWantsSatisfied and satisfiesGive return 0 or 1. + * isOfferSafe will still be boolean. When we have Multiples, numWantsSatisfied and * satisfiesGive will tell how many times the offer was matched. * * @param {AmountKeywordRecord} giveOrWant * @param {AmountKeywordRecord} allocation - * @returns {0|1} + * @returns {number} If the giveOrWant is empty, then any allocation satisfies + * it an `Infinity` number of times. */ -const satisfiesInternal = (giveOrWant = {}, allocation) => { - const isGTEByKeyword = ([keyword, requiredAmount]) => { - // If there is no allocation for a keyword, we know the giveOrWant - // is not satisfied without checking further. +const numSatisfied = (giveOrWant = {}, allocation) => { + let multiples = Infinity; + for (const [keyword, requiredAmount] of entries(giveOrWant)) { if (allocation[keyword] === undefined) { return 0; } const allocationAmount = allocation[keyword]; - return AmountMath.isGTE(allocationAmount, requiredAmount) ? 1 : 0; - }; - return Object.entries(giveOrWant).every(isGTEByKeyword) ? 1 : 0; + if (!AmountMath.isGTE(allocationAmount, requiredAmount)) { + return 0; + } + if (typeof requiredAmount.value !== 'bigint') { + multiples = 1; + } else if (requiredAmount.value > 0n) { + assert.typeof(allocationAmount.value, 'bigint'); + const howMany = natSafeMath.floorDivide( + allocationAmount.value, + requiredAmount.value, + ); + if (multiples > howMany) { + howMany <= Number.MAX_SAFE_INTEGER || + Fail`numSatisfied ${howMany} out of safe integer range`; + multiples = Number(howMany); + } + } + } + return multiples; }; /** * For this allocation to satisfy what the user wanted, their * allocated amounts must be greater than or equal to proposal.want. + * Even if multiples > 1n, this succeeds if it satisfies just one + * unit of want. * * @param {ProposalRecord} proposal - the rules that accompanied the * escrow of payments that dictate what the user expected to get back @@ -39,9 +60,12 @@ const satisfiesInternal = (giveOrWant = {}, allocation) => { * @param {AmountKeywordRecord} allocation - a record with keywords * as keys and amounts as values. These amounts are the reallocation * to be given to a user. + * @returns {number} If the want is empty, then any allocation satisfies + * it an `Infinity` number of times. */ -const satisfiesWant = (proposal, allocation) => - satisfiesInternal(proposal.want, allocation); +export const numWantsSatisfied = (proposal, allocation) => + numSatisfied(proposal.want, allocation); +harden(numWantsSatisfied); /** * For this allocation to count as a full refund, the allocated @@ -57,9 +81,12 @@ const satisfiesWant = (proposal, allocation) => * @param {AmountKeywordRecord} allocation - a record with keywords * as keys and amounts as values. These amounts are the reallocation * to be given to a user. + * @returns {number} If the give is empty, then any allocation satisfies + * it an `Infinity` number of times. */ -const satisfiesGive = (proposal, allocation) => - satisfiesInternal(proposal.give, allocation); +// Commented out because not currently used +// const satisfiesGive = (proposal, allocation) => +// satisfiesInternal(proposal.give, allocation); /** * `isOfferSafe` checks offer safety for a single offer. @@ -78,13 +105,10 @@ const satisfiesGive = (proposal, allocation) => * as keys and amounts as values. These amounts are the reallocation * to be given to a user. */ -function isOfferSafe(proposal, allocation) { - return ( - satisfiesGive(proposal, allocation) > 0 || - satisfiesWant(proposal, allocation) > 0 - ); -} - +export const isOfferSafe = (proposal, allocation) => { + const { give, want, multiples } = proposal; + const howMany = + numSatisfied(give, allocation) + numSatisfied(want, allocation); + return howMany >= multiples; +}; harden(isOfferSafe); -harden(satisfiesWant); -export { isOfferSafe, satisfiesWant }; diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 872c7e4070f0..7dcc2d6aa48b 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -3,13 +3,13 @@ import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; import { AssetKind } from '@agoric/ertp'; import { fromUniqueEntries } from '@agoric/internal'; -import { satisfiesWant } from '../contractFacet/offerSafety.js'; import { atomicRearrange, atomicTransfer, fromOnly, toOnly, } from './atomicTransfer.js'; +import { numWantsSatisfied } from '../contractFacet/offerSafety.js'; export const defaultAcceptanceMsg = `The offer has been accepted. Once the contract has been completed, please check your payout`; @@ -38,20 +38,25 @@ export const assertIssuerKeywords = (zcf, expected) => { * check; whether the allocation constitutes a refund is not * checked. The update is merged with currentAllocation * (update's values prevailing if the keywords are the same) - * to produce the newAllocation. The return value is 0 for - * false and 1 for true. When multiples are introduced, any - * positive return value will mean true. + * to produce the newAllocation. The return value indicates the + * number of times the want was satisfied. + * + * There are some calls to `satisfies` dating from when it returned a + * boolean rather than a number. Manual inspection verifies that these + * are only sensitive to whether the result is truthy or falsy. + * Since `0` is falsy and any positive number (including `Infinity`) + * is truthy, all these callers still operate correctly. * * @param {ZCF} zcf * @param {ZcfSeatPartial} seat * @param {AmountKeywordRecord} update - * @returns {0|1} + * @returns {number} */ export const satisfies = (zcf, seat, update) => { const currentAllocation = seat.getCurrentAllocation(); const newAllocation = { ...currentAllocation, ...update }; const proposal = seat.getProposal(); - return satisfiesWant(proposal, newAllocation); + return numWantsSatisfied(proposal, newAllocation); }; /** @type {Swap} */ @@ -167,6 +172,14 @@ export const assertProposalShape = (seat, expected) => { assertKeys(actual.give, expected.give); assertKeys(actual.want, expected.want); assertKeys(actual.exit, expected.exit); + if ('multiples' in expected) { + // Not sure what to do with the value of expected.multiples. Probably + // nothing until we convert all this to use proper patterns + } else { + // multiples other than 1n need to be opted into + actual.multiples === 1n || + Fail`Only 1n multiples expected: ${actual.multiples}`; + } }; /* Given a brand, assert that brand is AssetKind.NAT. */ diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index f50788ba7d8c..45311866f105 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -69,6 +69,7 @@ export const TimerShape = makeHandleShape('timer'); export const FullProposalShape = harden({ want: AmountPatternKeywordRecordShape, give: AmountKeywordRecordShape, + multiples: M.bigint(), // To accept only one, we could use M.or rather than M.splitRecord, // but the error messages would have been worse. Rather, // cleanProposal's assertExit checks that there's exactly one. diff --git a/packages/zoe/src/zoeService/escrowStorage.js b/packages/zoe/src/zoeService/escrowStorage.js index 51f1cb98c3b2..e3404050978d 100644 --- a/packages/zoe/src/zoeService/escrowStorage.js +++ b/packages/zoe/src/zoeService/escrowStorage.js @@ -7,7 +7,7 @@ import { provideDurableWeakMapStore } from '@agoric/vat-data'; import './types.js'; import './internal-types.js'; -import { cleanKeywords } from '../cleanProposal.js'; +import { cleanKeywords, scaleAmount } from '../cleanProposal.js'; import { arrayToObj } from '../objArrayConversion.js'; /** @@ -74,7 +74,7 @@ export const provideEscrowStorage = baggage => { /** @type {DepositPayments} */ const depositPayments = async (proposal, payments) => { - const { give, want } = proposal; + const { give, want, multiples } = proposal; const giveKeywords = Object.keys(give); const wantKeywords = Object.keys(want); const paymentKeywords = cleanKeywords(payments); @@ -108,7 +108,8 @@ export const provideEscrowStorage = baggage => { )} keyword in proposal.give did not have an associated payment in the paymentKeywordRecord, which had keywords: ${q( paymentKeywords, )}`; - return doDepositPayment(payments[keyword], give[keyword]); + const giveAmount = scaleAmount(give[keyword], multiples); + return doDepositPayment(payments[keyword], giveAmount); }), ); diff --git a/packages/zoe/src/zoeService/offer/offer.js b/packages/zoe/src/zoeService/offer/offer.js index 25a64935c5dc..8244e58f2f99 100644 --- a/packages/zoe/src/zoeService/offer/offer.js +++ b/packages/zoe/src/zoeService/offer/offer.js @@ -46,7 +46,12 @@ export const makeOfferMethod = offerDataAccess => { const proposal = cleanProposal(uncleanProposal, getAssetKindByBrand); const proposalShape = offerDataAccess.getProposalShapeForInvitation(invitationHandle); - if (proposalShape !== undefined) { + if (proposalShape === undefined) { + // For the contract to opt into accepting a multiples value other than + // `1n`, it must provide `makeInvitation` with a proposalShape. + proposal.multiples === 1n || + Fail`Contract not willing to accept multiples for this invitation: ${proposal}`; + } else { fit(proposal, proposalShape, `${q(description)} proposal`); } diff --git a/packages/zoe/src/zoeService/types.js b/packages/zoe/src/zoeService/types.js index 3a0a0b804f73..ff8c441b352a 100644 --- a/packages/zoe/src/zoeService/types.js +++ b/packages/zoe/src/zoeService/types.js @@ -207,11 +207,12 @@ * interact with the contract. * @property {() => Promise} hasExited * Returns true if the seat has exited, false if it is still active. - * @property {() => Promise<0|1>} numWantsSatisfied returns 1 if the proposal's - * want clause was satisfied by the final allocation, otherwise 0. This is - * numeric to support a planned enhancement called "multiples" which will allow - * the return value to be any non-negative number. The promise will resolve - * after the seat has exited. + * @property {() => Promise} numWantsSatisfied + * Returns the number of times that the proposal's `want` clause was satisfied + * by the final allocation. If the `want` was not satisfied then it was + * satisfied `0` times. If the want was satisfied, then it was satisfied + * `>= 1` times. The promise will resolve after the seat has exited. + * * @property {() => Promise} getFinalAllocation * return a promise for the final allocation. The promise will resolve after the * seat has exited. @@ -225,6 +226,7 @@ * * @typedef {{give: AmountKeywordRecord, * want: AmountKeywordRecord, + * multiples: bigint, * exit: ExitRule * }} ProposalRecord */ diff --git a/packages/zoe/src/zoeService/zoeSeat.js b/packages/zoe/src/zoeService/zoeSeat.js index 31feb402743e..9435d1c266c3 100644 --- a/packages/zoe/src/zoeService/zoeSeat.js +++ b/packages/zoe/src/zoeService/zoeSeat.js @@ -4,7 +4,8 @@ import { M, vivifyFarClassKit } from '@agoric/vat-data'; import { deeplyFulfilled } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; -import { satisfiesWant } from '../contractFacet/offerSafety.js'; +import { numWantsSatisfied } from '../contractFacet/offerSafety.js'; + import '../types.js'; import '../internal-types.js'; import { @@ -272,8 +273,8 @@ export const makeZoeSeatAdminFactory = baggage => { const { state } = this; return E.when( state.subscriber.subscribeAfter(), - () => satisfiesWant(state.proposal, state.currentAllocation), - () => satisfiesWant(state.proposal, state.currentAllocation), + () => numWantsSatisfied(state.proposal, state.currentAllocation), + () => numWantsSatisfied(state.proposal, state.currentAllocation), ); }, getExitSubscriber() { diff --git a/packages/zoe/test/unitTests/contractSupport/test-offerTo.js b/packages/zoe/test/unitTests/contractSupport/test-offerTo.js index 5ed081fcad8d..2f892fe4efc7 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-offerTo.js +++ b/packages/zoe/test/unitTests/contractSupport/test-offerTo.js @@ -105,7 +105,7 @@ test(`offerTo - basic usage`, async t => { want: { TokenL: moolaIssuer.getBrand().getAmountShape(), }, - // multiples: 1n, + multiples: 1n, exit: { onDemand: null, }, @@ -221,7 +221,7 @@ test(`offerTo - violates offer safety of fromSeat`, async t => { ), { message: - /Offer safety was violated by the proposed allocation: {"Token[JK]":{"brand":"\[Alleged: .* brand]","value":"\[0n]"},"Token[KJ]":{"brand":"\[Alleged: .* brand]","value":"\[0n]"}}. Proposal was/, + 'Offer safety was violated by the proposed allocation: {"TokenJ":{"brand":"[Alleged: moola brand]","value":"[0n]"},"TokenK":{"brand":"[Alleged: bucks brand]","value":"[0n]"}}. Proposal was {"exit":{"onDemand":null},"give":{"TokenK":{"brand":"[Alleged: bucks brand]","value":"[5n]"}},"multiples":"[1n]","want":{"TokenJ":{"brand":"[Alleged: moola brand]","value":"[3n]"}}}', }, ); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js index e0b8a668d75d..7cb9143df891 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js @@ -577,7 +577,7 @@ test('oracle continuing invitation', async t => { const invPrice = await E(invitationMakers).PushPrice('1234'); const invPriceResult = await E(zoe).offer(invPrice); - t.deepEqual(await E(invPriceResult).numWantsSatisfied(), 1); + t.deepEqual(await E(invPriceResult).numWantsSatisfied(), Infinity); await E(oracleTimer).tick(); await E(oracleTimer).tick(); diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 86c8a7778aee..50f3ccc4325a 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -30,6 +30,7 @@ test('cleanProposal test', t => { { give: { Asset: simoleans(1n) }, want: { Price: moola(3n) }, + multiples: 1n, exit: { onDemand: null }, }, ); @@ -39,6 +40,7 @@ test('cleanProposal - all empty', t => { proposeGood(t, {}, 'nat', { give: harden({}), want: harden({}), + multiples: 1n, exit: { onDemand: null }, }); @@ -53,6 +55,7 @@ test('cleanProposal - all empty', t => { { give: harden({}), want: harden({}), + multiples: 1n, exit: { waived: null }, }, ); @@ -73,6 +76,7 @@ test('cleanProposal - repeated brands', t => { { want: { Asset2: simoleans(1n) }, give: { Price2: moola(3n) }, + multiples: 1n, exit: { afterDeadline: { timer, deadline: 100n } }, }, ); @@ -109,6 +113,7 @@ test('cleanProposal - want patterns', t => { { want: { Asset2: M.any() }, give: { Price2: moola(3n) }, + multiples: 1n, exit: { afterDeadline: { timer, deadline: 100n } }, }, ); @@ -188,9 +193,10 @@ test('cleanProposal - other wrong stuff', t => { /keyword "Not Ident" must be an ascii identifier starting with upper case./, ); proposeGood(t, { give: { ['A'.repeat(100)]: simoleans(1n) } }, 'nat', { - exit: { onDemand: null }, - give: { ['A'.repeat(100)]: simoleans(1n) }, want: {}, + give: { ['A'.repeat(100)]: simoleans(1n) }, + multiples: 1n, + exit: { onDemand: null }, }); proposeBad( t, diff --git a/packages/zoe/test/unitTests/test-offerSafety-multiples.js b/packages/zoe/test/unitTests/test-offerSafety-multiples.js new file mode 100644 index 000000000000..ada1f9d7abb4 --- /dev/null +++ b/packages/zoe/test/unitTests/test-offerSafety-multiples.js @@ -0,0 +1,193 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { isOfferSafe } from '../../src/contractFacet/offerSafety.js'; +import { setup } from './setupBasicMints.js'; + +// Potential outcomes: +// 1. Users can get what they wanted, get back what they gave, both, or +// neither +// 2. Users can either get more than, less than, or equal to what they +// wanted or gave + +// possible combinations to test: +// more than want, more than give -> isOfferSafe() = true +// more than want, less than give -> true +// more than want, equal to give -> true +// less than want, more than give -> true +// less than want, less than give -> false +// less than want, equal to give -> true +// equal to want, more than give -> true +// equal to want, less than give -> true +// equal to want, equal to give -> true + +// more than want, more than give -> isOfferSafe() = true +test('isOfferSafe - more than want, more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: { A: moola(8n) }, + want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(50n), B: simoleans(35n), C: bucks(40n) }), + ), + ); +}); + +// more than want, less than give -> true +test('isOfferSafe - more than want, less than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: { A: moola(8n) }, + want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(1n), B: simoleans(35n), C: bucks(40n) }), + ), + ); +}); + +// more than want, equal to give -> true +test('isOfferSafe - more than want, equal to give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { A: moola(8n) }, + give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(45n), B: simoleans(30n), C: bucks(35n) }), + ), + ); +}); + +// less than want, more than give -> true +test('isOfferSafe - less than want, more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { A: moola(8n) }, + give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(7n), B: simoleans(45n), C: bucks(95n) }), + ), + ); +}); + +// less than want, less than give -> false +test('isOfferSafe - less than want, less than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { A: moola(8n) }, + give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.falsy( + isOfferSafe( + proposal, + harden({ A: moola(7n), B: simoleans(5n), C: bucks(6n) }), + ), + ); +}); + +// less than want, equal to give -> true +test('isOfferSafe - less than want, equal to give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(5n), B: simoleans(5n), C: bucks(35n) }), + ), + ); +}); + +// equal to want, more than give -> true +test('isOfferSafe - equal to want, more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(10n), B: simoleans(30n), C: bucks(40n) }), + ), + ); +}); + +// equal to want, less than give -> true +test('isOfferSafe - equal to want, less than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(0n), B: simoleans(30n), C: bucks(0n) }), + ), + ); +}); + +// equal to want, equal to give -> true +test('isOfferSafe - equal to want, equal to give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(5n), B: simoleans(30n), C: bucks(35n) }), + ), + ); +}); + +test('isOfferSafe - empty proposal', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: {}, + want: {}, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(1n), B: simoleans(6n), C: bucks(7n) }), + ), + ); +}); diff --git a/packages/zoe/test/unitTests/test-offerSafety.js b/packages/zoe/test/unitTests/test-offerSafety.js index 26c085417e66..918d1d0c2b5f 100644 --- a/packages/zoe/test/unitTests/test-offerSafety.js +++ b/packages/zoe/test/unitTests/test-offerSafety.js @@ -3,7 +3,7 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { isOfferSafe, - satisfiesWant, + numWantsSatisfied, } from '../../src/contractFacet/offerSafety.js'; import { setup } from './setupBasicMints.js'; @@ -30,12 +30,27 @@ test('isOfferSafe - more than want, more than give', t => { const proposal = harden({ give: { A: moola(8n) }, want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(10n), B: simoleans(7n), C: bucks(8n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); +}); + +test('isOfferSafe - much more than want, much more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: { A: moola(8n) }, + want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, + exit: { waived: null }, + }); + const amounts = harden({ A: moola(100n), B: simoleans(70n), C: bucks(80n) }); + + t.truthy(isOfferSafe(proposal, amounts)); + t.is(numWantsSatisfied(proposal, amounts), 11); }); // more than want, less than give -> true @@ -44,12 +59,13 @@ test('isOfferSafe - more than want, less than give', t => { const proposal = harden({ give: { A: moola(8n) }, want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(1n), B: simoleans(7n), C: bucks(8n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // more than want, equal to give -> true @@ -58,12 +74,13 @@ test('isOfferSafe - more than want, equal to give', t => { const proposal = harden({ want: { A: moola(8n) }, give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(9n), B: simoleans(6n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // less than want, more than give -> true @@ -72,12 +89,13 @@ test('isOfferSafe - less than want, more than give', t => { const proposal = harden({ want: { A: moola(8n) }, give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(7n), B: simoleans(9n), C: bucks(19n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 0); + t.is(numWantsSatisfied(proposal, amounts), 0); }); // less than want, less than give -> false @@ -86,12 +104,13 @@ test('isOfferSafe - less than want, less than give', t => { const proposal = harden({ want: { A: moola(8n) }, give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(7n), B: simoleans(5n), C: bucks(6n) }); t.falsy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 0); + t.is(numWantsSatisfied(proposal, amounts), 0); }); // less than want, equal to give -> true @@ -100,12 +119,13 @@ test('isOfferSafe - less than want, equal to give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(1n), B: simoleans(5n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 0); + t.is(numWantsSatisfied(proposal, amounts), 0); }); // equal to want, more than give -> true @@ -114,12 +134,13 @@ test('isOfferSafe - equal to want, more than give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(2n), B: simoleans(6n), C: bucks(8n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // equal to want, less than give -> true @@ -128,12 +149,13 @@ test('isOfferSafe - equal to want, less than give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(0n), B: simoleans(6n), C: bucks(0n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // equal to want, equal to give -> true @@ -142,19 +164,25 @@ test('isOfferSafe - equal to want, equal to give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(1n), B: simoleans(6n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); test('isOfferSafe - empty proposal', t => { const { moola, simoleans, bucks } = setup(); - const proposal = harden({ give: {}, want: {}, exit: { waived: null } }); + const proposal = harden({ + give: {}, + want: {}, + multiples: 1n, + exit: { waived: null }, + }); const amounts = harden({ A: moola(1n), B: simoleans(6n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), Infinity); }); diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index d4db403862a6..a58fcff67680 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -713,6 +713,7 @@ test(`zcfSeat.getProposal from zcf.makeEmptySeatKit`, async t => { }, give: {}, want: {}, + multiples: 1n, }); }); @@ -884,6 +885,7 @@ test(`userSeat.getProposal from zcf.makeEmptySeatKit`, async t => { }, give: {}, want: {}, + multiples: 1n, }); }); diff --git a/packages/zoe/test/unitTests/zoe/test-escrowStorage.js b/packages/zoe/test/unitTests/zoe/test-escrowStorage.js index 38470921070d..ee7ccfd39851 100644 --- a/packages/zoe/test/unitTests/zoe/test-escrowStorage.js +++ b/packages/zoe/test/unitTests/zoe/test-escrowStorage.js @@ -54,6 +54,7 @@ test('provideEscrowStorage', async t => { GameTicket: gameTicketAmount, Money: currencyAmount, }, + multiples: 1n, exit: { onDemand: null, }, @@ -158,6 +159,7 @@ test('payments without matching give keywords', async t => { GameTicket: gameTicketAmount, Money: currencyAmount, }, + multiples: 1n, exit: { onDemand: null, }, @@ -193,6 +195,7 @@ test(`give keywords without matching payments`, async t => { GameTicket: gameTicketAmount, Money: currencyAmount, }, + multiples: 1n, exit: { onDemand: null, },