Skip to content

Commit 9b970c3

Browse files
committed
feat(zoe): zoe part of minimal want patterns using M.containerHas(el,n)
1 parent b01e796 commit 9b970c3

22 files changed

+203
-114
lines changed

packages/zoe/exported.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
AmountKeywordRecord as _AmountKeywordRecord,
1111
ContractMeta as _ContractMeta,
1212
Handle as _Handle,
13-
Installation as _Installation,
14-
Instance as _Instance,
1513
Invitation as _Invitation,
1614
InvitationAmount as _InvitationAmount,
1715
IssuerKeywordRecord as _IssuerKeywordRecord,
@@ -26,6 +24,11 @@ import {
2624
ZoeService as _ZoeService,
2725
} from './src/types-index.js';
2826

27+
import {
28+
Installation as _Installation,
29+
Instance as _Instance,
30+
} from './src/zoeService/utils.js';
31+
2932
declare global {
3033
// @ts-ignore TS2666: Exports and export assignments are not permitted in module augmentations.
3134
export {
@@ -35,7 +38,7 @@ declare global {
3538
_FeeIssuerConfig as FeeIssuerConfig,
3639
_Handle as Handle,
3740
_Installation as Installation,
38-
_Instance as Instance,
41+
// _Instance as Instance,
3942
_Invitation as Invitation,
4043
_InvitationAmount as InvitationAmount,
4144
_IssuerKeywordRecord as IssuerKeywordRecord,

packages/zoe/src/cleanProposal.js

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { assert, q, Fail } from '@endo/errors';
2+
import { assertRecord } from '@endo/marshal';
3+
import { assertKey, M, mustMatch, isKey } from '@endo/patterns';
24
import { AmountMath, getAssetKind } from '@agoric/ertp';
35
import { objectMap } from '@agoric/internal';
4-
import { assertRecord } from '@endo/marshal';
5-
import { assertKey, assertPattern, mustMatch, isKey } from '@agoric/store';
66
import { FullProposalShape } from './typeGuards.js';
77

88
const { ownKeys } = Reflect;
@@ -56,25 +56,26 @@ export const cleanKeywords = uncleanKeywordRecord => {
5656
return /** @type {string[]} */ (keywords);
5757
};
5858

59-
export const coerceAmountPatternKeywordRecord = (
60-
allegedAmountKeywordRecord,
59+
export const coerceAmountBoundKeywordRecord = (
60+
allegedAmountBoundKeywordRecord,
6161
getAssetKindByBrand,
6262
) => {
63-
cleanKeywords(allegedAmountKeywordRecord);
63+
cleanKeywords(allegedAmountBoundKeywordRecord);
6464
// FIXME objectMap should constrain the mapping function by the record's type
65-
return objectMap(allegedAmountKeywordRecord, amount => {
65+
return objectMap(allegedAmountBoundKeywordRecord, amount => {
66+
const { brand, value } = amount;
6667
// Check that each value can be coerced using the AmountMath
6768
// indicated by brand. `AmountMath.coerce` throws if coercion fails.
6869
if (isKey(amount)) {
69-
const brandAssetKind = getAssetKindByBrand(amount.brand);
70+
const brandAssetKind = getAssetKindByBrand(brand);
7071
const assetKind = getAssetKind(amount);
7172
// TODO: replace this assertion with a check of the assetKind
7273
// property on the brand, when that exists.
7374
assetKind === brandAssetKind ||
7475
Fail`The amount ${amount} did not have the assetKind of the brand ${brandAssetKind}`;
75-
return AmountMath.coerce(amount.brand, amount);
76+
return AmountMath.coerce(brand, amount);
7677
} else {
77-
assertPattern(amount);
78+
mustMatch(value, M.kind('match:containerHas'));
7879
return amount;
7980
}
8081
});
@@ -90,7 +91,7 @@ export const coerceAmountKeywordRecord = (
9091
allegedAmountKeywordRecord,
9192
getAssetKindByBrand,
9293
) => {
93-
const result = coerceAmountPatternKeywordRecord(
94+
const result = coerceAmountBoundKeywordRecord(
9495
allegedAmountKeywordRecord,
9596
getAssetKindByBrand,
9697
);
@@ -153,10 +154,7 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => {
153154
ownKeys(rest).length === 0 ||
154155
Fail`${proposal} - Must only have want:, give:, exit: properties: ${rest}`;
155156

156-
const cleanedWant = coerceAmountPatternKeywordRecord(
157-
want,
158-
getAssetKindByBrand,
159-
);
157+
const cleanedWant = coerceAmountBoundKeywordRecord(want, getAssetKindByBrand);
160158
const cleanedGive = coerceAmountKeywordRecord(give, getAssetKindByBrand);
161159

162160
const cleanedProposal = harden({

packages/zoe/src/contractFacet/offerSafety.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { AmountMath } from '@agoric/ertp';
22

3+
/**
4+
* @import { AmountKeywordRecord, AmountBoundKeywordRecord } from '../zoeService/types.js'
5+
*/
6+
37
/**
48
* Helper to perform satisfiesWant and satisfiesGive. Is
59
* allocationAmount greater than or equal to requiredAmount for every
@@ -9,19 +13,19 @@ import { AmountMath } from '@agoric/ertp';
913
* isOfferSafe will still be boolean. When we have Multiples, satisfiesWant and
1014
* satisfiesGive will tell how many times the offer was matched.
1115
*
12-
* @param {AmountKeywordRecord} giveOrWant
16+
* @param {AmountBoundKeywordRecord} giveOrWant
1317
* @param {AmountKeywordRecord} allocation
1418
* @returns {0|1}
1519
*/
1620
const satisfiesInternal = (giveOrWant = {}, allocation) => {
17-
const isGTEByKeyword = ([keyword, requiredAmount]) => {
21+
const isGTEByKeyword = ([keyword, requiredAmountBound]) => {
1822
// If there is no allocation for a keyword, we know the giveOrWant
1923
// is not satisfied without checking further.
2024
if (allocation[keyword] === undefined) {
2125
return 0;
2226
}
2327
const allocationAmount = allocation[keyword];
24-
return AmountMath.isGTE(allocationAmount, requiredAmount) ? 1 : 0;
28+
return AmountMath.isGTE(allocationAmount, requiredAmountBound) ? 1 : 0;
2529
};
2630
return Object.entries(giveOrWant).every(isGTEByKeyword) ? 1 : 0;
2731
};

packages/zoe/src/contractFacet/reallocate.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { makeScalarMapStore } from '@agoric/vat-data';
33

44
import { assertRightsConserved } from './rightsConservation.js';
55
import { addToAllocation, subtractFromAllocation } from './allocationMath.js';
6+
import { mustBeKey } from '../contractSupport/zoeHelpers.js';
67

78
/**
89
* @import {MapStore} from '@agoric/swingset-liveslots';
@@ -47,11 +48,15 @@ export const makeAllocationMap = transfers => {
4748
allocations.set(seat, [newIncr, decr]);
4849
};
4950

50-
for (const [fromSeat, toSeat, fromAmounts, toAmounts] of transfers) {
51+
for (const [fromSeat, toSeat, fromAmountBounds, toAmounts] of transfers) {
5152
if (fromSeat) {
52-
if (!fromAmounts) {
53+
if (!fromAmountBounds) {
5354
throw Fail`Transfer from ${fromSeat} must say how much`;
5455
}
56+
const fromAmounts = mustBeKey(
57+
fromAmountBounds,
58+
'TODO: atomicRearange does not yet support AmountBounds',
59+
);
5560
decrementAllocation(fromSeat, fromAmounts);
5661
if (toSeat) {
5762
// Conserved transfer between seats
@@ -74,8 +79,8 @@ export const makeAllocationMap = transfers => {
7479
} else {
7580
toSeat || Fail`Transfer must have at least one of fromSeat or toSeat`;
7681
// Transfer only to toSeat
77-
!fromAmounts ||
78-
Fail`Transfer without fromSeat cannot have fromAmounts ${fromAmounts}`;
82+
!fromAmountBounds ||
83+
Fail`Transfer without fromSeat cannot have fromAmountBounds ${fromAmountBounds}`;
7984
toAmounts || Fail`Transfer to ${toSeat} must say how much`;
8085
incrementAllocation(toSeat, toAmounts);
8186
}
@@ -93,5 +98,5 @@ export const makeAllocationMap = transfers => {
9398
}
9499
resultingAllocations.push([seat, newAlloc]);
95100
}
96-
return resultingAllocations;
101+
return harden(resultingAllocations);
97102
};

packages/zoe/src/contractFacet/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ import type { Passable } from '@endo/pass-style';
1414
import type { Key, Pattern } from '@endo/patterns';
1515
import type {
1616
AmountKeywordRecord,
17+
AmountBoundKeywordRecord,
1718
ExitRule,
1819
FeeMintAccess,
19-
Instance,
2020
InvitationDetails,
2121
Keyword,
2222
ProposalRecord,
2323
StandardTerms,
2424
UserSeat,
2525
ZoeService,
2626
} from '../types-index.js';
27-
import type { ContractStartFunction } from '../zoeService/utils.js';
27+
import type { ContractStartFunction, Instance } from '../zoeService/utils.js';
2828

2929
/**
3030
* Any passable non-thenable. Often an explanatory string.
@@ -121,7 +121,7 @@ export type ZCF<CT = Record<string, unknown>> = {
121121
export type TransferPart = [
122122
fromSeat?: ZCFSeat,
123123
toSeat?: ZCFSeat,
124-
fromAmounts?: AmountKeywordRecord,
124+
fromAmounts?: AmountBoundKeywordRecord,
125125
toAmounts?: AmountKeywordRecord,
126126
];
127127

packages/zoe/src/contractSupport/atomicTransfer.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { M } from '@agoric/store';
2-
import { AmountKeywordRecordShape, SeatShape } from '../typeGuards.js';
2+
import {
3+
AmountKeywordRecordShape,
4+
AmountBoundKeywordRecordShape,
5+
SeatShape,
6+
} from '../typeGuards.js';
37

48
/**
5-
* @import {TransferPart, ZCF, ZCFSeat} from '@agoric/zoe';
9+
* @import {TransferPart, ZCF, ZCFSeat, AmountBoundKeywordRecord} from '@agoric/zoe';
610
*/
711

812
export const TransferPartShape = M.splitArray(
9-
harden([M.opt(SeatShape), M.opt(SeatShape), M.opt(AmountKeywordRecordShape)]),
13+
harden([
14+
M.opt(SeatShape),
15+
M.opt(SeatShape),
16+
M.opt(AmountBoundKeywordRecordShape),
17+
]),
1018
harden([M.opt(AmountKeywordRecordShape)]),
1119
);
1220

@@ -62,11 +70,11 @@ export const atomicRearrange = (zcf, transfers) => {
6270
* `fromOnly` are non-optional, as otherwise it doesn't make much sense.
6371
*
6472
* @param {ZCFSeat} fromSeat
65-
* @param {AmountKeywordRecord} fromAmounts
73+
* @param {AmountBoundKeywordRecord} fromAmountBounds
6674
* @returns {TransferPart}
6775
*/
68-
export const fromOnly = (fromSeat, fromAmounts) =>
69-
harden([fromSeat, undefined, fromAmounts]);
76+
export const fromOnly = (fromSeat, fromAmountBounds) =>
77+
harden([fromSeat, undefined, fromAmountBounds]);
7078

7179
/**
7280
* Sometimes a TransferPart in an atomicRearrange only expresses what amounts

packages/zoe/src/contractSupport/zoeHelpers.js

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { Fail } from '@endo/errors';
1+
import { Fail, b } from '@endo/errors';
22
import { E } from '@endo/eventual-send';
33
import { makePromiseKit } from '@endo/promise-kit';
4-
import { mustMatch, keyEQ } from '@agoric/store';
4+
import { mustMatch, keyEQ, isKey, isPattern } from '@endo/patterns';
55
import { AssetKind } from '@agoric/ertp';
66
import { fromUniqueEntries } from '@agoric/internal';
77
import { satisfiesWant } from '../contractFacet/offerSafety.js';
88
import { atomicTransfer, fromOnly, toOnly } from './atomicTransfer.js';
99

1010
/**
1111
* @import {Pattern} from '@endo/patterns';
12-
* @import {ContractMeta, Invitation, Proposal, ZCF, ZCFSeat} from '@agoric/zoe';
12+
* @import {Invitation, Proposal, ZCF, ZCFSeat, AmountBoundKeywordRecord} from '@agoric/zoe';
1313
*/
1414

1515
export const defaultAcceptanceMsg = `The offer has been accepted. Once the contract has been completed, please check your payout`;
@@ -73,16 +73,42 @@ export const swap = (zcf, leftSeat, rightSeat) => {
7373
return defaultAcceptanceMsg;
7474
};
7575

76+
/**
77+
* @param {AmountBoundKeywordRecord} want
78+
* @param {string} complaint
79+
* @returns {AmountKeywordRecord}
80+
*/
81+
export const mustBeKey = (want, complaint) => {
82+
if (isKey(want)) {
83+
return want;
84+
}
85+
if (isPattern(want)) {
86+
throw Fail`${b(complaint)}: ${want}`;
87+
}
88+
throw Fail`Must be key: ${want}`;
89+
};
90+
harden(mustBeKey);
91+
7692
/** @type {SwapExact} */
7793
export const swapExact = (zcf, leftSeat, rightSeat) => {
7894
try {
95+
const { give: rightGive, want: rightWantBound } = rightSeat.getProposal();
96+
const { give: leftGive, want: leftWantBound } = leftSeat.getProposal();
97+
const rightWant = mustBeKey(
98+
rightWantBound,
99+
'TODO: swapExact does not yet support want patterns',
100+
);
101+
const leftWant = mustBeKey(
102+
leftWantBound,
103+
'TODO: swapExact does not yet support want patterns',
104+
);
79105
zcf.atomicRearrange(
80106
harden([
81-
fromOnly(rightSeat, rightSeat.getProposal().give),
82-
fromOnly(leftSeat, leftSeat.getProposal().give),
107+
fromOnly(rightSeat, rightGive),
108+
fromOnly(leftSeat, leftGive),
83109

84-
toOnly(leftSeat, leftSeat.getProposal().want),
85-
toOnly(rightSeat, rightSeat.getProposal().want),
110+
toOnly(leftSeat, leftWant),
111+
toOnly(rightSeat, rightWant),
86112
]),
87113
);
88114
} catch (err) {

packages/zoe/src/contracts/auction/firstPriceLogic.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => {
1111
want: { Ask: minBid },
1212
} = sellSeat.getProposal();
1313

14-
/** @type {Brand<'nat'>} */
15-
const bidBrand = minBid.brand;
14+
const bidBrand = /** @type {Brand<'nat'>} */ (minBid.brand);
1615
const emptyBid = AmountMath.makeEmpty(bidBrand);
1716

1817
let highestBid = emptyBid;

packages/zoe/src/contracts/auction/secondPriceLogic.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => {
1111
want: { Ask: minBid },
1212
} = sellSeat.getProposal();
1313

14-
/** @type {Brand<'nat'>} */
15-
const bidBrand = minBid.brand;
14+
const bidBrand = /** @type {Brand<'nat'>} */ (minBid.brand);
1615
const emptyBid = AmountMath.makeEmpty(bidBrand);
1716

1817
let highestBid = emptyBid;

packages/zoe/src/contracts/loan/borrow.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, Fail } from '@endo/errors';
22
import { E } from '@endo/eventual-send';
33
import { Far } from '@endo/marshal';
44
import { makePromiseKit } from '@endo/promise-kit';
5+
import { M, mustMatch } from '@endo/patterns';
56
import { AmountMath } from '@agoric/ertp';
67

78
import {
@@ -16,6 +17,10 @@ import { calculateInterest, makeDebtCalculator } from './updateDebt.js';
1617
import { makeCloseLoanInvitation } from './close.js';
1718
import { makeAddCollateralInvitation } from './addCollateral.js';
1819

20+
/**
21+
* @import {NatAmount} from '@agoric/ertp';
22+
*/
23+
1924
/** @type {MakeBorrowInvitation} */
2025
export const makeBorrowInvitation = (zcf, config) => {
2126
const {
@@ -28,7 +33,9 @@ export const makeBorrowInvitation = (zcf, config) => {
2833
} = config;
2934

3035
// We can only lend what the lender has already escrowed.
31-
const maxLoan = lenderSeat.getAmountAllocated('Loan');
36+
const maxLoan = /** @type {NatAmount} */ (
37+
lenderSeat.getAmountAllocated('Loan')
38+
);
3239

3340
/** @type {OfferHandler} */
3441
const borrow = async borrowerSeat => {
@@ -43,7 +50,10 @@ export const makeBorrowInvitation = (zcf, config) => {
4350
borrowerSeat.getProposal().give.Collateral.brand
4451
),
4552
);
46-
const loanWanted = borrowerSeat.getProposal().want.Loan;
53+
const loanWanted = /** @type {NatAmount} */ (
54+
borrowerSeat.getProposal().want.Loan
55+
);
56+
mustMatch(loanWanted.value, M.nat());
4757
const loanBrand = zcf.getTerms().brands.Loan;
4858

4959
// The value of the collateral in the Loan brand

0 commit comments

Comments
 (0)