Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8606 invite proposals #10719

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does getProposalShapeForInvitation return values other than a ProposalShape?

Is ProposalShape wrong?

I notice that getProposalShapeForInvitation typedef returns Pattern | undefined but some places then require that it be narrower than Pattern:

const proposalShape =
offerDataAccess.getProposalShapeForInvitation(invitationHandle);
if (proposalShape !== undefined) {
mustMatch(proposal, proposalShape, `${q(description)} proposal`);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different meta-levels: proposalShape is the type for something that is shaped like a proposal. E(zoe).offer(invitation, proposal) takes a proposal, and its guard requires that it match proposalShape.

  const proposal = harden({
    give: { B: moola(5n) },
    exit: { onDemand: null },
  });
  E(zoe).offer(invitation, proposal, { B: fiveMoola });

The object returned by getProposalShapeForInvitation(invitationHandle) describes a proposal. It's a pattern.

  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);

It's arguable whether getProposalShapeForInvitation should return a ProposalShapePattern, but I don't think it's possible to customize M.pattern().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining. I agree Pattern is the right type but I request that getProposalShapeForInvitation get a jsdoc explaining its return type. A good time also to inline GetProposalShapeForInvitation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

),
});

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, {});
});
Loading