diff --git a/packages/wallet/api/src/findOrMakeInvitation.js b/packages/wallet/api/src/findOrMakeInvitation.js index 9128dbee54e..62e704c64a3 100644 --- a/packages/wallet/api/src/findOrMakeInvitation.js +++ b/packages/wallet/api/src/findOrMakeInvitation.js @@ -10,7 +10,7 @@ const assertFirstCapASCII = str => { const firstCapASCII = /^[A-Z][a-zA-Z0-9_$]*$/; assert( firstCapASCII.test(str), - X`The string ${q(str)} must be ascii and must start with a capital letter.`, + X`The string ${q(str)} must be an ascii identifier starting with upper case.`, ); assert( str !== 'NaN' && str !== 'Infinity', diff --git a/packages/zoe/src/cleanProposal.js b/packages/zoe/src/cleanProposal.js index 2fe9d893534..cfe9f02bbdb 100644 --- a/packages/zoe/src/cleanProposal.js +++ b/packages/zoe/src/cleanProposal.js @@ -1,21 +1,20 @@ // @ts-check import { assert, details as X, q } from '@agoric/assert'; -import { mustBeComparable } from '@agoric/same-structure'; import { isNat } from '@agoric/nat'; import { AmountMath, getAssetKind } from '@agoric/ertp'; import { assertRecord } from '@agoric/marshal'; +import { assertPattern } from '@agoric/store'; import { isOnDemandExitRule, isWaivedExitRule, isAfterDeadlineExitRule, } from './typeGuards.js'; +import { arrayToObj, assertSubset } from './objArrayConversion.js'; import '../exported.js'; import './internal-types.js'; -import { arrayToObj, assertSubset } from './objArrayConversion.js'; - const firstCapASCII = /^[A-Z][a-zA-Z0-9_$]*$/; // We adopt simple requirements on keywords so that they do not accidentally @@ -33,7 +32,7 @@ export const assertKeywordName = keyword => { firstCapASCII.test(keyword), X`keyword ${q( keyword, - )} must be ascii and must start with a capital letter.`, + )} must be an ascii identifier starting with upper case.`, ); assert( keyword !== 'NaN' && keyword !== 'Infinity', @@ -48,6 +47,7 @@ const assertKeysAllowed = (allowedKeys, record) => { const keys = Object.getOwnPropertyNames(record); assertSubset(allowedKeys, keys); // assert that there are no symbol properties. + // TODO unreachable: already rejected as unpassable assert( Object.getOwnPropertySymbols(record).length === 0, X`no symbol properties allowed`, @@ -95,6 +95,7 @@ export const cleanKeywords = keywordRecord => { const keywords = Object.getOwnPropertyNames(keywordRecord); // Insist that there are no symbol properties. + // TODO unreachable: already rejected as unpassable assert( Object.getOwnPropertySymbols(keywordRecord).length === 0, X`no symbol properties allowed`, @@ -177,7 +178,7 @@ const rootKeysAllowed = harden(['want', 'give', 'exit']); * @returns {ProposalRecord} */ export const cleanProposal = (proposal, getAssetKindByBrand) => { - mustBeComparable(proposal); + assertPattern(proposal); assertKeysAllowed(rootKeysAllowed, proposal); // We fill in the default values if the keys are undefined. diff --git a/packages/zoe/src/contractSupport/index.js b/packages/zoe/src/contractSupport/index.js index 0aa7d4b3d3c..4b8b96019ff 100644 --- a/packages/zoe/src/contractSupport/index.js +++ b/packages/zoe/src/contractSupport/index.js @@ -26,6 +26,7 @@ export * from './statistics.js'; export { defaultAcceptanceMsg, swap, + fitProposalPattern, assertProposalShape, assertIssuerKeywords, satisfies, diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 679b51ecbe6..11c8c3ce1df 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -5,7 +5,7 @@ import { assert, details as X } from '@agoric/assert'; import { sameStructure } from '@agoric/same-structure'; import { E } from '@agoric/eventual-send'; import { makePromiseKit } from '@agoric/promise-kit'; - +import { fit } from '@agoric/store'; import { AssetKind } from '@agoric/ertp'; import { satisfiesWant } from '../contractFacet/offerSafety.js'; @@ -100,6 +100,18 @@ export const swapExact = (zcf, leftSeat, rightSeat) => { * @property {Partial>} [exit] */ +/** + * Check the seat's proposal against `proposalPattern`. + * If the client submits an offer which does not match + * these expectations, the seat will be exited (and payments refunded). + * + * @param {ZCFSeat} seat + * @param {Pattern} proposalPattern + */ +export const fitProposalPattern = (seat, proposalPattern) => + // TODO remove this harden, obligating our caller to harden. + fit(seat.getProposal(), harden(proposalPattern)); + /** * Check the seat's proposal against an `expected` record that says * what shape of proposal is acceptable. diff --git a/packages/zoe/src/contracts/coveredCall.js b/packages/zoe/src/contracts/coveredCall.js index 1b92d935f34..f52bb14506d 100644 --- a/packages/zoe/src/contracts/coveredCall.js +++ b/packages/zoe/src/contracts/coveredCall.js @@ -1,10 +1,11 @@ // @ts-check import { assert } from '@agoric/assert'; +import { M, fit } from '@agoric/store'; import '../../exported.js'; // Eventually will be importable from '@agoric/zoe-contract-support' -import { assertProposalShape, swapExact } from '../contractSupport/index.js'; +import { swapExact } from '../contractSupport/index.js'; import { isAfterDeadlineExitRule } from '../typeGuards.js'; /** @@ -27,8 +28,10 @@ import { isAfterDeadlineExitRule } from '../typeGuards.js'; * different brands can be escrowed under different keywords. The * proposal must have an exit record with the key "afterDeadline": * { - * give: { ... }, want: { ... }, exit: {afterDeadline: { deadline: - * time, timer: myTimer } + * give: { ... }, + * want: { ... }, + * exit: { + * afterDeadline: { deadline: time, timer: myTimer } * }, * } * @@ -70,7 +73,7 @@ const start = zcf => { /** @type {OfferHandler} */ const makeOption = sellSeat => { - assertProposalShape(sellSeat, { exit: { afterDeadline: null } }); + fit(sellSeat.getProposal(), M.split({ exit: { afterDeadline: M.any() } })); const sellSeatExitRule = sellSeat.getProposal().exit; assert( isAfterDeadlineExitRule(sellSeatExitRule), diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index f0bc8bb25d9..333080eddef 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -1,9 +1,38 @@ // @ts-check -/** - * @param {ExitRule} exit - * @returns {exit is OnDemandExitRule} - */ +import { AmountPattern } from '@agoric/ertp'; +import { M } from '@agoric/store'; + +export const ExitOnDemandPattern = harden({ onDemand: null }); + +export const ExitWaivedPattern = harden({ waived: null }); + +export const ExitAfterDeadlinePattern = harden({ + afterDeadline: { timer: M.remotable(), deadline: M.nat() }, +}); + +export const ExitRulePattern = M.or( + ExitOnDemandPattern, + ExitWaivedPattern, + ExitAfterDeadlinePattern, +); + +export const KeywordRecordPatternOf = valuePatt => + M.recordOf(M.string(), valuePatt); + +export const AmountKeywordRecordPattern = KeywordRecordPatternOf(AmountPattern); + +export const PatternKeywordRecordPattern = KeywordRecordPatternOf(M.pattern()); + +export const ProposalPattern = M.partial( + { + want: M.or(undefined, PatternKeywordRecordPattern), + give: M.or(undefined, AmountKeywordRecordPattern), + exit: M.or(undefined, ExitRulePattern), + }, + {}, +); + export const isOnDemandExitRule = exit => { const [exitKey] = Object.getOwnPropertyNames(exit); return exitKey === 'onDemand'; diff --git a/packages/zoe/test/unitTests/contracts/test-coveredCall-want-pattern.js b/packages/zoe/test/unitTests/contracts/test-coveredCall-want-pattern.js new file mode 100644 index 00000000000..17f41eb2962 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/test-coveredCall-want-pattern.js @@ -0,0 +1,309 @@ +// @ts-check +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import path from 'path'; + +import bundleSource from '@agoric/bundle-source'; +import { E } from '@agoric/eventual-send'; +import { M, fit } from '@agoric/store'; +import { AmountMath, AssetKind } from '@agoric/ertp'; +import { sameStructure } from '@agoric/same-structure'; + +import buildManualTimer from '../../../tools/manualTimer.js'; +import { setup } from '../setupBasicMints.js'; + +const filename = new URL(import.meta.url).pathname; +const dirname = path.dirname(filename); + +const coveredCallRoot = `${dirname}/../../../src/contracts/coveredCall.js`; +const atomicSwapRoot = `${dirname}/../../../src/contracts/atomicSwap.js`; + +// Alice makes a covered call and escrows. She shares the invitation to +// Bob. Bob tries to sell the invitation to Dave through a swap. Can Bob +// trick Dave? Can Dave describe what it is that he wants in the swap +// offer pattern? +test('zoe - coveredCall with swap for invitation', async t => { + t.plan(24); + // Setup the environment + const timer = buildManualTimer(console.log); + const { moolaR, simoleanR, bucksR, moola, simoleans, bucks, zoe } = setup(); + // Pack the contract. + const coveredCallBundle = await bundleSource(coveredCallRoot); + + const coveredCallInstallation = await E(zoe).install(coveredCallBundle); + const atomicSwapBundle = await bundleSource(atomicSwapRoot); + + const swapInstallationId = await E(zoe).install(atomicSwapBundle); + + // Setup Alice + // Alice starts with 3 moola + const aliceMoolaPayment = moolaR.mint.mintPayment(moola(3n)); + const aliceMoolaPurse = moolaR.issuer.makeEmptyPurse(); + const aliceSimoleanPurse = simoleanR.issuer.makeEmptyPurse(); + + // Setup Bob + // Bob starts with nothing + const bobMoolaPurse = moolaR.issuer.makeEmptyPurse(); + const bobSimoleanPurse = simoleanR.issuer.makeEmptyPurse(); + const bobBucksPurse = bucksR.issuer.makeEmptyPurse(); + + // Setup Dave + // Dave starts with 1 buck + const daveSimoleanPayment = simoleanR.mint.mintPayment(simoleans(7n)); + const daveBucksPayment = bucksR.mint.mintPayment(bucks(1n)); + const daveMoolaPurse = moolaR.issuer.makeEmptyPurse(); + const daveSimoleanPurse = simoleanR.issuer.makeEmptyPurse(); + const daveBucksPurse = bucksR.issuer.makeEmptyPurse(); + + // Alice creates a coveredCall instance of moola for simoleans + const issuerKeywordRecord = harden({ + UnderlyingAsset: moolaR.issuer, + StrikePrice: simoleanR.issuer, + }); + const { creatorInvitation: aliceInvitation } = await E(zoe).startInstance( + coveredCallInstallation, + issuerKeywordRecord, + ); + + // Alice escrows with Zoe. She specifies her proposal, + // which includes the amounts she gives and wants as well as the exit + // conditions. In this case, she choses an exit condition of after + // the deadline of "100" according to a particular timer. This is + // meant to be something far in the future, and will not be + // reached in this test. + + const aliceProposal = harden({ + give: { UnderlyingAsset: moola(3n) }, + want: { StrikePrice: simoleans(7n) }, + exit: { + afterDeadline: { + deadline: 100n, // we will not reach this + timer, + }, + }, + }); + const alicePayments = { UnderlyingAsset: aliceMoolaPayment }; + // Alice makes an option. + const aliceSeat = await E(zoe).offer( + aliceInvitation, + aliceProposal, + alicePayments, + ); + + const optionP = E(aliceSeat).getOfferResult(); + + // Imagine that Alice sends the invitation to Bob (not done here since + // this test doesn't actually have separate vats/parties) + + // Bob inspects the invitation payment and checks its information against the + // questions that he has about whether it is worth being a counter + // party in the covered call: Did the covered call use the + // expected covered call installation (code)? Does it use the issuers + // that he expects (moola and simoleans)? + const invitationIssuer = await E(zoe).getInvitationIssuer(); + const invitationBrand = await E(invitationIssuer).getBrand(); + const bobExclOption = await E(invitationIssuer).claim(optionP); + const optionAmount = await E(invitationIssuer).getAmountOf(bobExclOption); + const optionDesc = optionAmount.value[0]; + t.is(optionDesc.installation, coveredCallInstallation); + t.is(optionDesc.description, 'exerciseOption'); + t.deepEqual(optionDesc.underlyingAssets, { UnderlyingAsset: moola(3n) }); + t.deepEqual(optionDesc.strikePrice, { StrikePrice: simoleans(7n) }); + t.is(optionDesc.expirationDate, 100n); + t.deepEqual(optionDesc.timeAuthority, timer); + + // Let's imagine that Bob wants to create a swap to trade this + // invitation for bucks. + const swapIssuerKeywordRecord = harden({ + Asset: invitationIssuer, + Price: bucksR.issuer, + }); + const { creatorInvitation: bobSwapInvitation } = await E(zoe).startInstance( + swapInstallationId, + swapIssuerKeywordRecord, + ); + + // Bob wants to swap an invitation with the same amount as his + // current invitation from Alice. He wants 1 buck in return. + const bobProposalSwap = harden({ + give: { Asset: await E(invitationIssuer).getAmountOf(bobExclOption) }, + want: { Price: bucks(1n) }, + }); + + const bobPayments = harden({ Asset: bobExclOption }); + + // Bob escrows his option in the swap + // Bob makes an offer to the swap with his "higher order" invitation + const bobSwapSeat = await E(zoe).offer( + bobSwapInvitation, + bobProposalSwap, + bobPayments, + ); + + const daveSwapInvitationP = E(bobSwapSeat).getOfferResult(); + + // Bob passes the swap invitation to Dave and tells him the + // optionAmounts (basically, the description of the option) + + const { + value: [{ instance: swapInstance, installation: daveSwapInstallId }], + } = await E(invitationIssuer).getAmountOf(daveSwapInvitationP); + + const daveSwapIssuers = await E(zoe).getIssuers(swapInstance); + + // Dave is looking to buy the option to trade his 7 simoleans for + // 3 moola, and is willing to pay 1 buck for the option. He + // checks that this instance matches what he wants + + // Did this swap use the correct swap installation? Yes + t.is(daveSwapInstallId, swapInstallationId); + + // Is this swap for the correct issuers and has no other terms? Yes + t.truthy( + sameStructure( + daveSwapIssuers, + harden({ + Asset: invitationIssuer, + Price: bucksR.issuer, + }), + ), + ); + + const optionAmountPattern1 = harden({ + brand: M.any(), + value: [ + { + handle: M.any(), + instance: M.any(), + installation: coveredCallInstallation, + description: 'exerciseOption', + underlyingAssets: { UnderlyingAsset: M.gte(moola(2n)) }, + strikePrice: { StrikePrice: M.lte(simoleans(8n)) }, + timeAuthority: timer, + expirationDate: M.and(M.gte(50n), M.lte(300n)), + fee: undefined, + zoeTimeAuthority: undefined, + expiry: undefined, + }, + ], + }); + + fit(optionAmount, optionAmountPattern1); + + const optionAmountPattern2 = harden({ + brand: M.remotable(), + value: [ + M.split({ + installation: coveredCallInstallation, + description: 'exerciseOption', + underlyingAssets: { UnderlyingAsset: M.gte(moola(2n)) }, + strikePrice: { StrikePrice: M.lte(simoleans(8n)) }, + timeAuthority: timer, + expirationDate: M.and(M.gte(50n), M.lte(300n)), + }), + ], + }); + + fit(optionAmount, optionAmountPattern2); + + // What's actually up to be bought? Is it the kind of invitation that + // Dave wants? What's the price for that invitation? Is it acceptable + // to Dave? Bob can tell Dave this out of band, and if he lies, + // Dave's offer will be rejected and he will get a refund. Dave + // knows this to be true because he knows the swap. + + // Dave escrows his 1 buck with Zoe and forms his proposal + const daveSwapProposal = harden({ + want: { Asset: optionAmount }, + give: { Price: bucks(1n) }, + }); + + const daveSwapPayments = harden({ Price: daveBucksPayment }); + const daveSwapSeat = await E(zoe).offer( + daveSwapInvitationP, + daveSwapProposal, + daveSwapPayments, + ); + + t.is( + await daveSwapSeat.getOfferResult(), + 'The offer has been accepted. Once the contract has been completed, please check your payout', + ); + + const daveOption = await daveSwapSeat.getPayout('Asset'); + const daveBucksPayout = await daveSwapSeat.getPayout('Price'); + + // Dave exercises his option by making an offer to the covered + // call. First, he escrows with Zoe. + + const daveCoveredCallProposal = harden({ + want: { UnderlyingAsset: moola(3n) }, + give: { StrikePrice: simoleans(7n) }, + }); + const daveCoveredCallPayments = harden({ + StrikePrice: daveSimoleanPayment, + }); + const daveCoveredCallSeat = await E(zoe).offer( + daveOption, + daveCoveredCallProposal, + daveCoveredCallPayments, + ); + + t.is( + await E(daveCoveredCallSeat).getOfferResult(), + `The option was exercised. Please collect the assets in your payout.`, + ); + + // Dave should get 3 moola, Bob should get 1 buck, and Alice + // get 7 simoleans + const daveMoolaPayout = await daveCoveredCallSeat.getPayout( + 'UnderlyingAsset', + ); + const daveSimoleanPayout = await daveCoveredCallSeat.getPayout('StrikePrice'); + const aliceMoolaPayout = await aliceSeat.getPayout('UnderlyingAsset'); + const aliceSimoleanPayout = await aliceSeat.getPayout('StrikePrice'); + const bobInvitationPayout = await bobSwapSeat.getPayout('Asset'); + const bobBucksPayout = await bobSwapSeat.getPayout('Price'); + + t.deepEqual(await moolaR.issuer.getAmountOf(daveMoolaPayout), moola(3n)); + t.deepEqual( + await simoleanR.issuer.getAmountOf(daveSimoleanPayout), + simoleans(0n), + ); + + t.deepEqual(await moolaR.issuer.getAmountOf(aliceMoolaPayout), moola(0n)); + t.deepEqual( + await simoleanR.issuer.getAmountOf(aliceSimoleanPayout), + simoleans(7n), + ); + + t.deepEqual( + await E(invitationIssuer).getAmountOf(bobInvitationPayout), + AmountMath.makeEmpty(invitationBrand, AssetKind.SET), + ); + t.deepEqual(await bucksR.issuer.getAmountOf(bobBucksPayout), bucks(1n)); + + // Alice deposits her payouts + await aliceMoolaPurse.deposit(aliceMoolaPayout); + await aliceSimoleanPurse.deposit(aliceSimoleanPayout); + + // Bob deposits his payouts + await bobBucksPurse.deposit(bobBucksPayout); + + // Dave deposits his payouts + await daveMoolaPurse.deposit(daveMoolaPayout); + await daveSimoleanPurse.deposit(daveSimoleanPayout); + await daveBucksPurse.deposit(daveBucksPayout); + + t.is(aliceMoolaPurse.getCurrentAmount().value, 0n); + t.is(aliceSimoleanPurse.getCurrentAmount().value, 7n); + + t.is(bobMoolaPurse.getCurrentAmount().value, 0n); + t.is(bobSimoleanPurse.getCurrentAmount().value, 0n); + t.is(bobBucksPurse.getCurrentAmount().value, 1n); + + t.is(daveMoolaPurse.getCurrentAmount().value, 3n); + t.is(daveSimoleanPurse.getCurrentAmount().value, 0n); + t.is(daveBucksPurse.getCurrentAmount().value, 0n); +}); diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 80da963c6ed..44d96e2d998 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -60,9 +60,7 @@ test('cleanProposal - repeated brands', t => { }); const expected = harden({ - want: { - Asset2: simoleans(1n), - }, + want: { Asset2: simoleans(1n) }, give: { Price2: moola(3n) }, exit: { afterDeadline: { timer, deadline: 100n } }, }); @@ -92,3 +90,68 @@ test('cleanProposal - wrong assetKind', t => { message: /The amount .* did not have the assetKind of the brand .*/, }); }); + +test('cleanProposal - other wrong stuff', t => { + const { moola, simoleans } = setup(); + const timer = buildManualTimer(console.log); + + const proposeBad = (proposal, assetKind, message) => + t.throws(() => cleanProposal(harden(proposal), () => assetKind), { + message, + }); + + proposeBad( + { want: { lowercase: simoleans(1n) } }, + 'nat', + /keyword "lowercase" must be an ascii identifier starting with upper case./, + ); + proposeBad( + { give: { lowercase: simoleans(1n) } }, + 'nat', + /keyword "lowercase" must be an ascii identifier starting with upper case./, + ); + proposeBad( + { want: { 'Not Ident': simoleans(1n) } }, + 'nat', + /keyword "Not Ident" must be an ascii identifier starting with upper case./, + ); + proposeBad( + { what: { 'Not Ident': simoleans(1n) } }, + 'nat', + /key "what" was not one of the expected keys \["want","give","exit"\]/, + ); + proposeBad( + { [Symbol.for('what')]: { 'Not Ident': simoleans(1n) } }, + 'nat', + /cannot serialize Remotables with non-methods like "Symbol\(what\)" in {}/, + ); + proposeBad( + { want: { [Symbol.for('S')]: simoleans(1n) } }, + 'nat', + /cannot serialize Remotables with non-methods like "Symbol\(S\)" in {}/, + ); + proposeBad( + { exit: { afterDeadline: { timer, deadline: 3 } } }, + 'nat', + /deadline must be a Nat BigInt/, + ); + proposeBad( + { exit: { afterDeadline: { timer, deadline: -3n } } }, + 'nat', + /deadline must be a Nat BigInt/, + ); + proposeBad({ exit: {} }, 'nat', /exit {} should only have one key/); + proposeBad( + { exit: { onDemand: null, waived: null } }, + 'nat', + /exit {"onDemand":null,"waived":null} should only have one key/, + ); + proposeBad( + { + want: { Asset: simoleans(1n) }, + give: { Asset: moola(3n) }, + }, + 'nat', + /a keyword cannot be in both 'want' and 'give'/, + ); +}); diff --git a/packages/zoe/test/unitTests/test-zoe.js b/packages/zoe/test/unitTests/test-zoe.js index 1d3fddaeb9c..02e681f2a9b 100644 --- a/packages/zoe/test/unitTests/test-zoe.js +++ b/packages/zoe/test/unitTests/test-zoe.js @@ -140,8 +140,8 @@ test(`E(zoe).startInstance - terms, issuerKeywordRecord switched`, async t => { // disclosure bug is fixed. See // https://github.com/endojs/endo/pull/640 // - // /keyword "something" must be ascii and must start with a capital letter./ - /keyword .* must be ascii and must start with a capital letter./, + // /keyword "something" must be an ascii identifier starting with upper case./ + /keyword .* must be an ascii identifier starting with upper case./, }, ); }); diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index f501c788990..00a74fcbf88 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -151,8 +151,8 @@ test(`zcf.assertUniqueKeyword`, async t => { // disclosure bug is fixed. See // https://github.com/endojs/endo/pull/640 // - // 'keyword "a" must be ascii and must start with a capital letter.', - /keyword .* must be ascii and must start with a capital letter./, + // 'keyword "a" must be an ascii identifier starting with upper case.', + /keyword .* must be an ascii identifier starting with upper case./, }); t.throws(() => zcf.assertUniqueKeyword('3'), { message: @@ -160,8 +160,8 @@ test(`zcf.assertUniqueKeyword`, async t => { // disclosure bug is fixed. See // https://github.com/endojs/endo/pull/640 // - // 'keyword "3" must be ascii and must start with a capital letter.', - /keyword .* must be ascii and must start with a capital letter./, + // 'keyword "3" must be an ascii identifier starting with upper case.', + /keyword .* must be an ascii identifier starting with upper case./, }); zcf.assertUniqueKeyword('MyKeyword'); }); @@ -234,8 +234,8 @@ test(`zcf.saveIssuer - bad keyword`, async t => { // disclosure bug is fixed. See // https://github.com/endojs/endo/pull/640 // - // `keyword "bad keyword" must be ascii and must start with a capital letter.`, - /keyword .* must be ascii and must start with a capital letter./, + // `keyword "bad keyword" must be an ascii identifier starting with upper case.`, + /keyword .* must be an ascii identifier starting with upper case./, }, ); }); @@ -392,8 +392,8 @@ test(`zcf.makeZCFMint - bad keyword`, async t => { // disclosure bug is fixed. See // https://github.com/endojs/endo/pull/640 // - // 'keyword "a" must be ascii and must start with a capital letter.', - /keyword .* must be ascii and must start with a capital letter./, + // 'keyword "a" must be an ascii identifier starting with upper case.', + /keyword .* must be an ascii identifier starting with upper case./, }); }); diff --git a/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js b/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js index 4c633b01a64..c8b3e7fa041 100644 --- a/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zoeHelpersWZcf.js @@ -2,6 +2,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { M, fit } from '@agoric/store'; import { AssetKind, makeIssuerKit } from '@agoric/ertp'; import { setup } from '../setupBasicMints.js'; import { @@ -291,6 +292,43 @@ test(`zoeHelper with zcf - assertIssuerKeywords`, async t => { t.notThrows(() => assertIssuerKeywords(zcf, ['A', 'B'])); }); +test(`zoeHelper with zcf - fit proposal patterns`, async t => { + const { moola, simoleans } = setup(); + + const proposal = harden({ + want: { A: moola(20n) }, + give: { B: simoleans(3n) }, + }); + + // @ts-ignore invalid arguments for testing + t.throws(() => fit(proposal, harden([])), { + message: /.* - Must be equivalent to: \[\]/, + }); + t.throws( + () => fit(proposal, M.split({ want: { C: M.any() } })), + { + message: /Must have same property names as record pattern: {"C":"\[match:any\]"}/, + }, + 'empty keywordRecord does not match', + ); + t.notThrows(() => fit(proposal, M.split({ want: { A: M.any() } }))); + t.notThrows(() => fit(proposal, M.split({ give: { B: M.any() } }))); + t.throws( + () => fit(proposal, M.split({ give: { c: M.any() } })), + { + message: /Must have same property names as record pattern: {"c":"\[match:any\]"}/, + }, + 'wrong key in keywordRecord does not match', + ); + t.throws( + () => fit(proposal, M.split({ exit: { onDemaind: M.any() } })), + { + message: /Must have same property names as record pattern: {"exit":{"onDemaind":"\[match:any\]"}}/, + }, + 'missing exit rule', + ); +}); + test(`zoeHelper with zcf - assertProposalShape`, async t => { const { moolaIssuer, @@ -564,6 +602,19 @@ test(`zoeHelper w/zcf - swapExact w/extra payments`, async t => { ); }); +test(`zcf/zoeHelper - fit proposal pattern w/bad Expected`, async t => { + const { moola, simoleans } = setup(); + + const proposal = harden({ + want: { A: moola(20n) }, + give: { B: simoleans(3n) }, + }); + + t.throws(() => fit(proposal, M.split({ give: { B: moola(3n) } })), { + message: /.* - Must be equivalent to: .*/, + }); +}); + test(`zcf/zoeHelper - assertProposalShape w/bad Expected`, async t => { const { moolaIssuer, @@ -582,7 +633,6 @@ test(`zcf/zoeHelper - assertProposalShape w/bad Expected`, async t => { { B: simoleanMint.mintPayment(simoleans(3n)) }, ); - // @ts-ignore invalid arguments for testing t.throws(() => assertProposalShape(zcfSeat, { give: { B: moola(3n) } }), { message: /The value of the expected record must be null but was .*/, });