diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 584d50233739..52950aeb7187 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -337,7 +337,7 @@ test.serial('errors', async t => { }), { error: undefined, - numWantsSatisfied: 1, + numWantsSatisfied: Infinity, }, ); await eventLoopIteration(); diff --git a/packages/vats/test/bootstrapTests/test-vaults-integration.js b/packages/vats/test/bootstrapTests/test-vaults-integration.js index 7e2bbd08ad96..0109b47168a4 100644 --- a/packages/vats/test/bootstrapTests/test-vaults-integration.js +++ b/packages/vats/test/bootstrapTests/test-vaults-integration.js @@ -135,7 +135,7 @@ test('adjust balances', async t => { updated: 'offerStatus', status: { id: 'adjust', - numWantsSatisfied: 1, + numWantsSatisfied: Infinity, }, }); }); @@ -193,7 +193,8 @@ test('close vault', async t => { updated: 'offerStatus', status: { id: 'close-insufficient', - numWantsSatisfied: 1, // trivially true because proposal `want` was empty. + // XXX there were no wants. Zoe treats as Infinitely satisfied + numWantsSatisfied: Infinity, error: `Error: ${message}`, }, }); @@ -215,6 +216,7 @@ test('close vault', async t => { result: 'your vault is closed, thank you for your business', // funds are returned payouts: likePayouts(giveCollateral, 0), + numWantsSatisfied: Infinity, }, }); }); @@ -280,7 +282,7 @@ test('exit bid', async t => { status: { id: 'bid', result: 'Your bid has been accepted', // it was accepted before being exited - numWantsSatisfied: 1, // trivially 1 because there were no "wants" in the proposal + numWantsSatisfied: Infinity, // trivially, because there were no "wants" in the proposal payouts: { // got back the give Bid: { value: 100000n }, @@ -309,7 +311,9 @@ test('propose change to auction governance param', async t => { }); await eventLoopIteration(); - t.like(wd.getLatestUpdateRecord(), { status: { numWantsSatisfied: 1 } }); + t.like(wd.getLatestUpdateRecord(), { + status: { numWantsSatisfied: Infinity }, + }); const auctioneer = agoricNamesRemotes.instance.auctioneer; const timerBrand = agoricNamesRemotes.brand.timer; @@ -341,7 +345,9 @@ test('propose change to auction governance param', async t => { }); await eventLoopIteration(); - t.like(wd.getLatestUpdateRecord(), { status: { numWantsSatisfied: 1 } }); + t.like(wd.getLatestUpdateRecord(), { + status: { numWantsSatisfied: Infinity }, + }); const { fromCapData } = makeMarshal(undefined, slotToBoardRemote); const key = `published.committees.Economic_Committee.latestQuestion`; diff --git a/packages/wallet/api/test/test-lib-wallet.js b/packages/wallet/api/test/test-lib-wallet.js index 7bf3569a0ce3..3886983cf586 100644 --- a/packages/wallet/api/test/test-lib-wallet.js +++ b/packages/wallet/api/test/test-lib-wallet.js @@ -1338,6 +1338,7 @@ test('addOffer invitationQuery', async t => { value: 1n, }, }, + multiples: 1n, exit: { onDemand: null, }, @@ -1459,6 +1460,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 19064df6b3f6..3e15c233531a 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, mustMatch, 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,36 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => { const cleanedProposal = harden({ want: cleanedWant, give: cleanedGive, + multiples, exit, }); mustMatch(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; + if (typeof value !== 'bigint') { + throw Fail`multiples > 1 not yet implemented for non-fungibles: ${multiples} * ${amount}`; + } + assert(value >= 1n); + 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..869817c5b55a 100644 --- a/packages/zoe/src/contractFacet/offerSafety.js +++ b/packages/zoe/src/contractFacet/offerSafety.js @@ -1,34 +1,51 @@ import { AmountMath } from '@agoric/ertp'; +import { natSafeMath } from '../contractSupport/safeMath.js'; + +const { Fail } = assert; +const { entries } = Object; /** - * Helper to perform satisfiesWant and satisfiesGive. Is - * allocationAmount greater than or equal to requiredAmount for every - * keyword of giveOrWant? - * - * To prepare for multiples, satisfiesWant and satisfiesGive return 0 or 1. - * isOfferSafe will still be boolean. When we have Multiples, satisfiesWant and - * satisfiesGive will tell how many times the offer was matched. + * Helper to perform numWantsSatisfied and numGivesSatisfied. How many times + * does the `allocation` satisfy the `giveOrWant`? * * @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,14 +56,17 @@ 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 * amounts must be greater than or equal to what was originally - * offered (proposal.give). + * offered (proposal.give * proposal.multiples). * * @param {ProposalRecord} proposal - the rules that accompanied the * escrow of payments that dictate what the user expected to get back @@ -57,9 +77,13 @@ 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 numGivesSatisfied = (proposal, allocation) => +// numSatisfied(proposal.give, allocation); +// harden(numGivesSatisfied); /** * `isOfferSafe` checks offer safety for a single offer. @@ -78,13 +102,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 2287395c33fd..6884de88fc55 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -3,8 +3,8 @@ 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 { 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`; @@ -33,20 +33,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} */ @@ -160,6 +165,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 a5264eb2e190..d8aa45703a19 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -71,6 +71,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 ccb894cae6da..5f66f70212de 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 f34d578e1719..cda80b4324a4 100644 --- a/packages/zoe/src/zoeService/offer/offer.js +++ b/packages/zoe/src/zoeService/offer/offer.js @@ -48,7 +48,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 { mustMatch(proposal, proposalShape, `${q(description)} proposal`); } diff --git a/packages/zoe/src/zoeService/types.js b/packages/zoe/src/zoeService/types.js index 34897fd32e34..18de08ced10a 100644 --- a/packages/zoe/src/zoeService/types.js +++ b/packages/zoe/src/zoeService/types.js @@ -210,11 +210,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. @@ -227,6 +228,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 2462440d97ec..dffd5d3ebd56 100644 --- a/packages/zoe/src/zoeService/zoeSeat.js +++ b/packages/zoe/src/zoeService/zoeSeat.js @@ -4,7 +4,8 @@ import { M, prepareExoClassKit } 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 { @@ -319,8 +320,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 6a8f7b8d4ec3..2db9f5d43dda 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-offerTo.js +++ b/packages/zoe/test/unitTests/contractSupport/test-offerTo.js @@ -104,7 +104,7 @@ test(`offerTo - basic usage`, async t => { want: { TokenL: moolaIssuer.getBrand().getAmountShape(), }, - // multiples: 1n, + multiples: 1n, exit: { onDemand: null, }, @@ -220,7 +220,10 @@ 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/, + // TODO golden error message list property names in sorted order, + // which should work again after https://github.com/endojs/endo/pull/1678 + // + '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 639006832210..be852994e846 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js @@ -574,7 +574,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 2ac334bac699..c2131d86c114 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -29,6 +29,7 @@ test('cleanProposal test', t => { { give: { Asset: simoleans(1n) }, want: { Price: moola(3n) }, + multiples: 1n, exit: { onDemand: null }, }, ); @@ -38,6 +39,7 @@ test('cleanProposal - all empty', t => { proposeGood(t, {}, 'nat', { give: harden({}), want: harden({}), + multiples: 1n, exit: { onDemand: null }, }); @@ -52,6 +54,7 @@ test('cleanProposal - all empty', t => { { give: harden({}), want: harden({}), + multiples: 1n, exit: { waived: null }, }, ); @@ -72,6 +75,7 @@ test('cleanProposal - repeated brands', t => { { want: { Asset2: simoleans(1n) }, give: { Price2: moola(3n) }, + multiples: 1n, exit: { afterDeadline: { timer, deadline: 100n } }, }, ); @@ -108,6 +112,7 @@ test('cleanProposal - want patterns', t => { { want: { Asset2: M.any() }, give: { Price2: moola(3n) }, + multiples: 1n, exit: { afterDeadline: { timer, deadline: 100n } }, }, ); @@ -187,9 +192,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 3499a577f440..757bcb24f428 100644 --- a/packages/zoe/test/unitTests/test-offerSafety.js +++ b/packages/zoe/test/unitTests/test-offerSafety.js @@ -2,7 +2,7 @@ import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; import { isOfferSafe, - satisfiesWant, + numWantsSatisfied, } from '../../src/contractFacet/offerSafety.js'; import { setup } from './setupBasicMints.js'; @@ -29,12 +29,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 @@ -43,12 +58,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 @@ -57,12 +73,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 @@ -71,12 +88,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 @@ -85,12 +103,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 @@ -99,12 +118,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 @@ -113,12 +133,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 @@ -127,12 +148,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 @@ -141,19 +163,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 e4f77c559463..e00361ed3a99 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -719,6 +719,7 @@ test(`zcfSeat.getProposal from zcf.makeEmptySeatKit`, async t => { }, give: {}, want: {}, + multiples: 1n, }); }); @@ -889,6 +890,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 e412a2513a68..3db4cc73d84e 100644 --- a/packages/zoe/test/unitTests/zoe/test-escrowStorage.js +++ b/packages/zoe/test/unitTests/zoe/test-escrowStorage.js @@ -53,6 +53,7 @@ test('provideEscrowStorage', async t => { GameTicket: gameTicketAmount, Money: stableAmount, }, + multiples: 1n, exit: { onDemand: null, }, @@ -157,6 +158,7 @@ test('payments without matching give keywords', async t => { GameTicket: gameTicketAmount, Money: stableAmount, }, + multiples: 1n, exit: { onDemand: null, }, @@ -192,6 +194,7 @@ test(`give keywords without matching payments`, async t => { GameTicket: gameTicketAmount, Money: stableAmount, }, + multiples: 1n, exit: { onDemand: null, },