Skip to content

Commit

Permalink
8606 invite proposals (#10719)
Browse files Browse the repository at this point in the history
closes: #8606
closes: #8597

## Description

#8597 suggested adding a test proposal guards in Zoe. This adds that test, and also cleans up a redundant check in coveredCall. The added test exposed a mistake in the Zoe doc, so [1258](Agoric/documentation#1258) fixes that as well.

### Security Considerations

None

### Scaling Considerations

None.

### Documentation Considerations

Improved documentation.

### Testing Considerations

Added tests.

### Upgrade Considerations

These changes do not impact code on-chain. There's a correction to an example contract, an improvement in a TypeGuard, and a documentation update which will be separately release.
  • Loading branch information
mergify[bot] authored Jan 7, 2025
2 parents 2f3ca19 + 6cd9b9a commit 9c97014
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 9c97014

Please sign in to comment.