From 6cd9b9a52e53b96f81d7c9942da5b724f5135a0b Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 17 Dec 2024 13:07:57 -0800 Subject: [PATCH] test: add tests for patterns in makeInvitation() correct one typeGuard improve jsdoc Also noticed that the types documented for getProposalShapeForInvitation were incorrect. fixed that with https://github.com/Agoric/documentation/pull/1258 --- packages/zoe/src/contracts/coveredCall.js | 7 +- packages/zoe/src/typeGuards.js | 2 +- packages/zoe/src/zoeService/internal-types.js | 8 +- packages/zoe/src/zoeService/types-ambient.js | 6 +- .../unitTests/contracts/coveredCall.test.js | 48 +++++- .../unitTests/zcf/offer-proposalShape.test.js | 143 ++++++++++++++++++ 6 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 packages/zoe/test/unitTests/zcf/offer-proposalShape.test.js diff --git a/packages/zoe/src/contracts/coveredCall.js b/packages/zoe/src/contracts/coveredCall.js index 1f441aebb57..f70f4c7a27a 100644 --- a/packages/zoe/src/contracts/coveredCall.js +++ b/packages/zoe/src/contracts/coveredCall.js @@ -1,5 +1,5 @@ import { Fail, q } from '@endo/errors'; -import { M, mustMatch } from '@agoric/store'; + // Eventually will be importable from '@agoric/zoe-contract-support' import { swapExact } from '../contractSupport/index.js'; import { isAfterDeadlineExitRule } from '../typeGuards.js'; @@ -69,11 +69,6 @@ const start = zcf => { /** @type {OfferHandler} */ const makeOption = sellSeat => { - mustMatch( - sellSeat.getProposal(), - M.splitRecord({ exit: { afterDeadline: M.any() } }), - 'exit afterDeadline', - ); const sellSeatExitRule = sellSeat.getProposal().exit; if (!isAfterDeadlineExitRule(sellSeatExitRule)) { throw Fail`the seller must have an afterDeadline exitRule, but instead had ${q( diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index 0c1d7a02ddb..d1324aee24f 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -367,7 +367,7 @@ export const ZoeServiceI = M.interface('ZoeService', { }), getInvitationDetails: M.call(M.eref(InvitationShape)).returns(M.any()), getProposalShapeForInvitation: M.call(InvitationHandleShape).returns( - M.opt(ProposalShape), + M.opt(M.pattern()), ), }); diff --git a/packages/zoe/src/zoeService/internal-types.js b/packages/zoe/src/zoeService/internal-types.js index eb7badb4ce5..21536e5aa2c 100644 --- a/packages/zoe/src/zoeService/internal-types.js +++ b/packages/zoe/src/zoeService/internal-types.js @@ -114,12 +114,6 @@ * @returns {Promise} */ -/** - * @callback GetProposalShapeForInvitation - * @param {InvitationHandle} invitationHandle - * @returns {Pattern | undefined} - */ - /** * @typedef ZoeStorageManager * @property {MakeZoeInstanceStorageManager} makeZoeInstanceStorageManager @@ -138,7 +132,7 @@ * @property {GetInstallationForInstance} getInstallationForInstance * @property {GetInstanceAdmin} getInstanceAdmin * @property {UnwrapInstallation} unwrapInstallation - * @property {GetProposalShapeForInvitation} getProposalShapeForInvitation + * @property {(invitationHandle: InvitationHandle) => Pattern | undefined} getProposalShapeForInvitation */ /** diff --git a/packages/zoe/src/zoeService/types-ambient.js b/packages/zoe/src/zoeService/types-ambient.js index bfb3dc71ba2..9576ffc0ce4 100644 --- a/packages/zoe/src/zoeService/types-ambient.js +++ b/packages/zoe/src/zoeService/types-ambient.js @@ -39,12 +39,14 @@ * @property {GetInstance} getInstance * @property {GetInstallation} getInstallation * @property {GetInvitationDetails} getInvitationDetails - * Return an object with the instance, installation, description, invitation - * handle, and any custom properties specific to the contract. + * Return an object with the instance, installation, description, invitation + * handle, and any custom properties specific to the contract. * @property {GetFeeIssuer} getFeeIssuer * @property {GetConfiguration} getConfiguration * @property {GetBundleIDFromInstallation} getBundleIDFromInstallation * @property {(invitationHandle: InvitationHandle) => Pattern | undefined} getProposalShapeForInvitation + * Return the pattern (if any) associated with the invitationHandle that a + * proposal is required to match to be accepted by zoe.offer(). */ /** diff --git a/packages/zoe/test/unitTests/contracts/coveredCall.test.js b/packages/zoe/test/unitTests/contracts/coveredCall.test.js index 6a6d6f788bc..a718a212e6d 100644 --- a/packages/zoe/test/unitTests/contracts/coveredCall.test.js +++ b/packages/zoe/test/unitTests/contracts/coveredCall.test.js @@ -4,7 +4,7 @@ import path from 'path'; import bundleSource from '@endo/bundle-source'; import { E } from '@endo/eventual-send'; -import { Far } from '@endo/marshal'; +import { deeplyFulfilled, Far } from '@endo/marshal'; import { AmountMath, AssetKind } from '@agoric/ertp'; import { claim } from '@agoric/ertp/src/legacy-payment-helpers.js'; import { keyEQ } from '@agoric/store'; @@ -1072,3 +1072,49 @@ test('zoe - coveredCall non-fungible', async t => { t.deepEqual(bobCcPurse.getCurrentAmount().value, ['GrowlTiger']); t.deepEqual(bobRpgPurse.getCurrentAmount().value, []); }); + +test('zoe - coveredCall - bad proposal shape', async t => { + const { moolaKit, simoleanKit, moola, zoe, vatAdminState } = setup(); + + // Bundle and install the contract. + const bundle = await bundleSource(coveredCallRoot); + vatAdminState.installBundle('b1-coveredcall', bundle); + const coveredCallInstallation = + await E(zoe).installBundleID('b1-coveredcall'); + + // Start an instance. + const issuerKeywordRecord = harden({ + UnderlyingAsset: moolaKit.issuer, + StrikePrice: simoleanKit.issuer, + }); + const { creatorInvitation } = await E(zoe).startInstance( + coveredCallInstallation, + issuerKeywordRecord, + ); + + // Make an unacceptable proposal. + const badProposal = harden({ + give: { UnderlyingAsset: moola(3n) }, + exit: { waived: null }, + }); + const payments = harden({ + UnderlyingAsset: moolaKit.mint.mintPayment(moola(3n)), + }); + const badSeat = await E(zoe).offer(creatorInvitation, badProposal, payments); + await t.throwsAsync( + () => E(badSeat).getOfferResult(), + { + message: + /the seller must have an afterDeadline exitRule, but instead had {"waived":null}/, + }, + 'A bad proposal shape must be rejected', + ); + + // The payment must be returned. + const payouts = await deeplyFulfilled(E(badSeat).getPayouts()); + t.deepEqual(payouts, payments); + t.deepEqual( + await moolaKit.issuer.getAmountOf(payouts.UnderlyingAsset), + moola(3n), + ); +}); diff --git a/packages/zoe/test/unitTests/zcf/offer-proposalShape.test.js b/packages/zoe/test/unitTests/zcf/offer-proposalShape.test.js new file mode 100644 index 00000000000..ed4ea98cd4a --- /dev/null +++ b/packages/zoe/test/unitTests/zcf/offer-proposalShape.test.js @@ -0,0 +1,143 @@ +import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; + +import path from 'path'; + +import { E } from '@endo/eventual-send'; +import bundleSource from '@endo/bundle-source'; + +import { M } from '@endo/patterns'; +import { AmountShape } from '@agoric/ertp'; +import { makeZoeForTest } from '../../../tools/setup-zoe.js'; +import { setup } from '../setupBasicMints.js'; +import { makeFakeVatAdmin } from '../../../tools/fakeVatAdmin.js'; + +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const contractRoot = `${dirname}/zcfTesterContract.js`; + +test(`ProposalShapes mismatch`, async t => { + const { moolaIssuer, simoleanIssuer, moola, moolaMint } = setup(); + let testJig; + const setJig = jig => { + testJig = jig; + }; + const { admin: fakeVatAdminSvc, vatAdminState } = makeFakeVatAdmin(setJig); + /** @type {ZoeService} */ + const zoe = makeZoeForTest(fakeVatAdminSvc); + + // pack the contract + const bundle = await bundleSource(contractRoot); + // install the contract + vatAdminState.installBundle('b1-zcftester', bundle); + const installation = await E(zoe).installBundleID('b1-zcftester'); + + // Alice creates an instance + const issuerKeywordRecord = harden({ + Pixels: moolaIssuer, + Money: simoleanIssuer, + }); + + await E(zoe).startInstance(installation, issuerKeywordRecord); + + // The contract uses the testJig so the contractFacet + // is available here for testing purposes + /** @type {ZCF} */ + // @ts-expect-error cast + const zcf = testJig.zcf; + + const boring = () => { + return 'ok'; + }; + + const proposalShape = M.splitRecord({ + give: { B: AmountShape }, + exit: { deadline: M.any() }, + }); + const invitation = await zcf.makeInvitation( + boring, + 'seat1', + {}, + proposalShape, + ); + const { handle } = await E(zoe).getInvitationDetails(invitation); + const shape = await E(zoe).getProposalShapeForInvitation(handle); + t.deepEqual(shape, proposalShape); + + const proposal = harden({ + give: { B: moola(5n) }, + exit: { onDemand: null }, + }); + + const fiveMoola = moolaMint.mintPayment(moola(5n)); + await t.throwsAsync( + () => + E(zoe).offer(invitation, proposal, { + B: fiveMoola, + }), + { + message: + '"seat1" proposal: exit: {"onDemand":null} - Must have missing properties ["deadline"]', + }, + ); + t.falsy(vatAdminState.getHasExited()); + // The moola was not deposited. + t.true(await E(moolaIssuer).isLive(fiveMoola)); +}); + +test(`ProposalShapes matched`, async t => { + const { moolaIssuer, simoleanIssuer } = setup(); + let testJig; + const setJig = jig => { + testJig = jig; + }; + const { admin: fakeVatAdminSvc, vatAdminState } = makeFakeVatAdmin(setJig); + /** @type {ZoeService} */ + const zoe = makeZoeForTest(fakeVatAdminSvc); + + // pack the contract + const bundle = await bundleSource(contractRoot); + // install the contract + vatAdminState.installBundle('b1-zcftester', bundle); + const installation = await E(zoe).installBundleID('b1-zcftester'); + + // Alice creates an instance + const issuerKeywordRecord = harden({ + Pixels: moolaIssuer, + Money: simoleanIssuer, + }); + + await E(zoe).startInstance(installation, issuerKeywordRecord); + + // The contract uses the testJig so the contractFacet + // is available here for testing purposes + /** @type {ZCF} */ + // @ts-expect-error cast + const zcf = testJig.zcf; + + const boring = () => { + return 'ok'; + }; + + const proposalShape = M.splitRecord({ exit: { onDemand: null } }); + const invitation = await zcf.makeInvitation( + boring, + 'seat', + {}, + proposalShape, + ); + const { handle } = await E(zoe).getInvitationDetails(invitation); + const shape = await E(zoe).getProposalShapeForInvitation(handle); + t.deepEqual(shape, proposalShape); + + // onDemand is the default + const seat = await E(zoe).offer(invitation); + + const result = await E(seat).getOfferResult(); + t.is(result, 'ok', `userSeat1 offer result`); + + t.falsy(await E(seat).hasExited()); + await E(seat).tryExit(); + t.true(await E(seat).hasExited()); + const payouts = await E(seat).getPayouts(); + t.deepEqual(payouts, {}); +});