Skip to content

Commit

Permalink
test: add tests for patterns in makeInvitation()
Browse files Browse the repository at this point in the history
correct one typeGuard

improve jsdoc

Also noticed that the types documented for
getProposalShapeForInvitation were incorrect. fixed that with
Agoric/documentation#1258
  • Loading branch information
Chris-Hibbert committed Jan 7, 2025
1 parent 103e0bc commit bff77ff
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 17 deletions.
7 changes: 1 addition & 6 deletions packages/zoe/src/contracts/coveredCall.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/zoe/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
),
});

Expand Down
8 changes: 1 addition & 7 deletions packages/zoe/src/zoeService/internal-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,6 @@
* @returns {Promise<BundleCap>}
*/

/**
* @callback GetProposalShapeForInvitation
* @param {InvitationHandle} invitationHandle
* @returns {Pattern | undefined}
*/

/**
* @typedef ZoeStorageManager
* @property {MakeZoeInstanceStorageManager} makeZoeInstanceStorageManager
Expand All @@ -138,7 +132,7 @@
* @property {GetInstallationForInstance} getInstallationForInstance
* @property {GetInstanceAdmin} getInstanceAdmin
* @property {UnwrapInstallation} unwrapInstallation
* @property {GetProposalShapeForInvitation} getProposalShapeForInvitation
* @property {(invitationHandle: InvitationHandle) => Pattern | undefined} getProposalShapeForInvitation
*/

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/zoe/src/zoeService/types-ambient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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().
*/

/**
Expand Down
48 changes: 47 additions & 1 deletion packages/zoe/test/unitTests/contracts/coveredCall.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
);
});
143 changes: 143 additions & 0 deletions packages/zoe/test/unitTests/zcf/offer-proposalShape.test.js
Original file line number Diff line number Diff line change
@@ -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, {});
});

0 comments on commit bff77ff

Please sign in to comment.