Skip to content
Draft
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
9 changes: 6 additions & 3 deletions packages/zoe/exported.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
AmountKeywordRecord as _AmountKeywordRecord,
ContractMeta as _ContractMeta,
Handle as _Handle,
Installation as _Installation,
Instance as _Instance,
Invitation as _Invitation,
InvitationAmount as _InvitationAmount,
IssuerKeywordRecord as _IssuerKeywordRecord,
Expand All @@ -26,6 +24,11 @@ import {
ZoeService as _ZoeService,
} from './src/types-index.js';

import {
Installation as _Installation,
Instance as _Instance,
} from './src/zoeService/utils.js';

declare global {
// @ts-ignore TS2666: Exports and export assignments are not permitted in module augmentations.
export {
Expand All @@ -35,7 +38,7 @@ declare global {
_FeeIssuerConfig as FeeIssuerConfig,
_Handle as Handle,
_Installation as Installation,
_Instance as Instance,
// _Instance as Instance,
_Invitation as Invitation,
_InvitationAmount as InvitationAmount,
_IssuerKeywordRecord as IssuerKeywordRecord,
Expand Down
26 changes: 12 additions & 14 deletions packages/zoe/src/cleanProposal.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { assert, q, Fail } from '@endo/errors';
import { assertRecord } from '@endo/marshal';
import { assertKey, M, mustMatch, isKey } from '@endo/patterns';
import { AmountMath, getAssetKind } from '@agoric/ertp';
import { objectMap } from '@agoric/internal';
import { assertRecord } from '@endo/marshal';
import { assertKey, assertPattern, mustMatch, isKey } from '@agoric/store';
import { FullProposalShape } from './typeGuards.js';

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

export const coerceAmountPatternKeywordRecord = (
allegedAmountKeywordRecord,
export const coerceAmountBoundKeywordRecord = (
allegedAmountBoundKeywordRecord,
getAssetKindByBrand,
) => {
cleanKeywords(allegedAmountKeywordRecord);
cleanKeywords(allegedAmountBoundKeywordRecord);
// FIXME objectMap should constrain the mapping function by the record's type
return objectMap(allegedAmountKeywordRecord, amount => {
return objectMap(allegedAmountBoundKeywordRecord, amount => {
const { brand, value } = amount;
// Check that each value can be coerced using the AmountMath
// indicated by brand. `AmountMath.coerce` throws if coercion fails.
if (isKey(amount)) {
const brandAssetKind = getAssetKindByBrand(amount.brand);
const brandAssetKind = getAssetKindByBrand(brand);
const assetKind = getAssetKind(amount);
// TODO: replace this assertion with a check of the assetKind
// property on the brand, when that exists.
assetKind === brandAssetKind ||
Fail`The amount ${amount} did not have the assetKind of the brand ${brandAssetKind}`;
return AmountMath.coerce(amount.brand, amount);
return AmountMath.coerce(brand, amount);
} else {
assertPattern(amount);
mustMatch(value, M.kind('match:containerHas'));
return amount;
}
});
Expand All @@ -90,7 +91,7 @@ export const coerceAmountKeywordRecord = (
allegedAmountKeywordRecord,
getAssetKindByBrand,
) => {
const result = coerceAmountPatternKeywordRecord(
const result = coerceAmountBoundKeywordRecord(
allegedAmountKeywordRecord,
getAssetKindByBrand,
);
Expand Down Expand Up @@ -153,10 +154,7 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => {
ownKeys(rest).length === 0 ||
Fail`${proposal} - Must only have want:, give:, exit: properties: ${rest}`;

const cleanedWant = coerceAmountPatternKeywordRecord(
want,
getAssetKindByBrand,
);
const cleanedWant = coerceAmountBoundKeywordRecord(want, getAssetKindByBrand);
const cleanedGive = coerceAmountKeywordRecord(give, getAssetKindByBrand);

const cleanedProposal = harden({
Expand Down
10 changes: 7 additions & 3 deletions packages/zoe/src/contractFacet/offerSafety.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AmountMath } from '@agoric/ertp';

/**
* @import { AmountKeywordRecord, AmountBoundKeywordRecord } from '../zoeService/types.js'
*/

/**
* Helper to perform satisfiesWant and satisfiesGive. Is
* allocationAmount greater than or equal to requiredAmount for every
Expand All @@ -9,19 +13,19 @@ import { AmountMath } from '@agoric/ertp';
* isOfferSafe will still be boolean. When we have Multiples, satisfiesWant and
* satisfiesGive will tell how many times the offer was matched.
*
* @param {AmountKeywordRecord} giveOrWant
* @param {AmountBoundKeywordRecord} giveOrWant
* @param {AmountKeywordRecord} allocation
* @returns {0|1}
*/
const satisfiesInternal = (giveOrWant = {}, allocation) => {
const isGTEByKeyword = ([keyword, requiredAmount]) => {
const isGTEByKeyword = ([keyword, requiredAmountBound]) => {
// If there is no allocation for a keyword, we know the giveOrWant
// is not satisfied without checking further.
if (allocation[keyword] === undefined) {
return 0;
}
const allocationAmount = allocation[keyword];
return AmountMath.isGTE(allocationAmount, requiredAmount) ? 1 : 0;
return AmountMath.isGTE(allocationAmount, requiredAmountBound) ? 1 : 0;
};
return Object.entries(giveOrWant).every(isGTEByKeyword) ? 1 : 0;
};
Expand Down
15 changes: 10 additions & 5 deletions packages/zoe/src/contractFacet/reallocate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { makeScalarMapStore } from '@agoric/vat-data';

import { assertRightsConserved } from './rightsConservation.js';
import { addToAllocation, subtractFromAllocation } from './allocationMath.js';
import { mustBeKey } from '../contractSupport/zoeHelpers.js';

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

for (const [fromSeat, toSeat, fromAmounts, toAmounts] of transfers) {
for (const [fromSeat, toSeat, fromAmountBounds, toAmounts] of transfers) {
if (fromSeat) {
if (!fromAmounts) {
if (!fromAmountBounds) {
throw Fail`Transfer from ${fromSeat} must say how much`;
}
const fromAmounts = mustBeKey(
fromAmountBounds,
'TODO: atomicRearange does not yet support AmountBounds',
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

"atomicRearange" should be "atomicRearrange" (missing 'r').

Suggested change
'TODO: atomicRearange does not yet support AmountBounds',
'TODO: atomicRearrange does not yet support AmountBounds',

Copilot uses AI. Check for mistakes.
);
decrementAllocation(fromSeat, fromAmounts);
if (toSeat) {
// Conserved transfer between seats
Expand All @@ -74,8 +79,8 @@ export const makeAllocationMap = transfers => {
} else {
toSeat || Fail`Transfer must have at least one of fromSeat or toSeat`;
// Transfer only to toSeat
!fromAmounts ||
Fail`Transfer without fromSeat cannot have fromAmounts ${fromAmounts}`;
!fromAmountBounds ||
Fail`Transfer without fromSeat cannot have fromAmountBounds ${fromAmountBounds}`;
toAmounts || Fail`Transfer to ${toSeat} must say how much`;
incrementAllocation(toSeat, toAmounts);
}
Expand All @@ -93,5 +98,5 @@ export const makeAllocationMap = transfers => {
}
resultingAllocations.push([seat, newAlloc]);
}
return resultingAllocations;
return harden(resultingAllocations);
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

The change from returning resultingAllocations to harden(resultingAllocations) suggests the original array wasn't hardened. This should be verified against the function's return type expectations and existing callers to ensure compatibility.

Copilot uses AI. Check for mistakes.
};
6 changes: 3 additions & 3 deletions packages/zoe/src/contractFacet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ import type { Passable } from '@endo/pass-style';
import type { Key, Pattern } from '@endo/patterns';
import type {
AmountKeywordRecord,
AmountBoundKeywordRecord,
ExitRule,
FeeMintAccess,
Instance,
InvitationDetails,
Keyword,
ProposalRecord,
StandardTerms,
UserSeat,
ZoeService,
} from '../types-index.js';
import type { ContractStartFunction } from '../zoeService/utils.js';
import type { ContractStartFunction, Instance } from '../zoeService/utils.js';

/**
* Any passable non-thenable. Often an explanatory string.
Expand Down Expand Up @@ -121,7 +121,7 @@ export type ZCF<CT = Record<string, unknown>> = {
export type TransferPart = [
fromSeat?: ZCFSeat,
toSeat?: ZCFSeat,
fromAmounts?: AmountKeywordRecord,
fromAmounts?: AmountBoundKeywordRecord,
toAmounts?: AmountKeywordRecord,
];

Expand Down
20 changes: 14 additions & 6 deletions packages/zoe/src/contractSupport/atomicTransfer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { M } from '@agoric/store';
import { AmountKeywordRecordShape, SeatShape } from '../typeGuards.js';
import {
AmountKeywordRecordShape,
AmountBoundKeywordRecordShape,
SeatShape,
} from '../typeGuards.js';

/**
* @import {TransferPart, ZCF, ZCFSeat} from '@agoric/zoe';
* @import {TransferPart, ZCF, ZCFSeat, AmountBoundKeywordRecord} from '@agoric/zoe';
*/

export const TransferPartShape = M.splitArray(
harden([M.opt(SeatShape), M.opt(SeatShape), M.opt(AmountKeywordRecordShape)]),
harden([
M.opt(SeatShape),
M.opt(SeatShape),
M.opt(AmountBoundKeywordRecordShape),
]),
harden([M.opt(AmountKeywordRecordShape)]),
);

Expand Down Expand Up @@ -62,11 +70,11 @@ export const atomicRearrange = (zcf, transfers) => {
* `fromOnly` are non-optional, as otherwise it doesn't make much sense.
*
* @param {ZCFSeat} fromSeat
* @param {AmountKeywordRecord} fromAmounts
* @param {AmountBoundKeywordRecord} fromAmountBounds
* @returns {TransferPart}
*/
export const fromOnly = (fromSeat, fromAmounts) =>
harden([fromSeat, undefined, fromAmounts]);
export const fromOnly = (fromSeat, fromAmountBounds) =>
harden([fromSeat, undefined, fromAmountBounds]);

/**
* Sometimes a TransferPart in an atomicRearrange only expresses what amounts
Expand Down
40 changes: 33 additions & 7 deletions packages/zoe/src/contractSupport/zoeHelpers.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Fail } from '@endo/errors';
import { Fail, b } from '@endo/errors';
import { E } from '@endo/eventual-send';
import { makePromiseKit } from '@endo/promise-kit';
import { mustMatch, keyEQ } from '@agoric/store';
import { mustMatch, keyEQ, isKey, isPattern } from '@endo/patterns';
import { AssetKind } from '@agoric/ertp';
import { fromUniqueEntries } from '@agoric/internal';
import { satisfiesWant } from '../contractFacet/offerSafety.js';
import { atomicTransfer, fromOnly, toOnly } from './atomicTransfer.js';

/**
* @import {Pattern} from '@endo/patterns';
* @import {ContractMeta, Invitation, Proposal, ZCF, ZCFSeat} from '@agoric/zoe';
* @import {Invitation, Proposal, ZCF, ZCFSeat, AmountBoundKeywordRecord} from '@agoric/zoe';
*/

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

/**
* @param {AmountBoundKeywordRecord} want
* @param {string} complaint
* @returns {AmountKeywordRecord}
*/
export const mustBeKey = (want, complaint) => {
if (isKey(want)) {
return want;
}
if (isPattern(want)) {
throw Fail`${b(complaint)}: ${want}`;
}
throw Fail`Must be key: ${want}`;
Comment on lines +77 to +88
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

[nitpick] The function parameter is named want but the function is generic and could be used for other purposes. Consider renaming to value or input for better clarity.

Suggested change
* @param {AmountBoundKeywordRecord} want
* @param {string} complaint
* @returns {AmountKeywordRecord}
*/
export const mustBeKey = (want, complaint) => {
if (isKey(want)) {
return want;
}
if (isPattern(want)) {
throw Fail`${b(complaint)}: ${want}`;
}
throw Fail`Must be key: ${want}`;
* @param {AmountBoundKeywordRecord} value
* @param {string} complaint
* @returns {AmountKeywordRecord}
*/
export const mustBeKey = (value, complaint) => {
if (isKey(value)) {
return value;
}
if (isPattern(value)) {
throw Fail`${b(complaint)}: ${value}`;
}
throw Fail`Must be key: ${value}`;

Copilot uses AI. Check for mistakes.
};
harden(mustBeKey);

/** @type {SwapExact} */
export const swapExact = (zcf, leftSeat, rightSeat) => {
try {
const { give: rightGive, want: rightWantBound } = rightSeat.getProposal();
const { give: leftGive, want: leftWantBound } = leftSeat.getProposal();
const rightWant = mustBeKey(
rightWantBound,
'TODO: swapExact does not yet support want patterns',
);
const leftWant = mustBeKey(
leftWantBound,
'TODO: swapExact does not yet support want patterns',
);
zcf.atomicRearrange(
harden([
fromOnly(rightSeat, rightSeat.getProposal().give),
fromOnly(leftSeat, leftSeat.getProposal().give),
fromOnly(rightSeat, rightGive),
fromOnly(leftSeat, leftGive),

toOnly(leftSeat, leftSeat.getProposal().want),
toOnly(rightSeat, rightSeat.getProposal().want),
toOnly(leftSeat, leftWant),
toOnly(rightSeat, rightWant),
]),
);
} catch (err) {
Expand Down
3 changes: 1 addition & 2 deletions packages/zoe/src/contracts/auction/firstPriceLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => {
want: { Ask: minBid },
} = sellSeat.getProposal();

/** @type {Brand<'nat'>} */
const bidBrand = minBid.brand;
const bidBrand = /** @type {Brand<'nat'>} */ (minBid.brand);
const emptyBid = AmountMath.makeEmpty(bidBrand);

let highestBid = emptyBid;
Expand Down
3 changes: 1 addition & 2 deletions packages/zoe/src/contracts/auction/secondPriceLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => {
want: { Ask: minBid },
} = sellSeat.getProposal();

/** @type {Brand<'nat'>} */
const bidBrand = minBid.brand;
const bidBrand = /** @type {Brand<'nat'>} */ (minBid.brand);
const emptyBid = AmountMath.makeEmpty(bidBrand);

let highestBid = emptyBid;
Expand Down
14 changes: 12 additions & 2 deletions packages/zoe/src/contracts/loan/borrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, Fail } from '@endo/errors';
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';
import { makePromiseKit } from '@endo/promise-kit';
import { M, mustMatch } from '@endo/patterns';
import { AmountMath } from '@agoric/ertp';

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

/**
* @import {NatAmount} from '@agoric/ertp';
*/

/** @type {MakeBorrowInvitation} */
export const makeBorrowInvitation = (zcf, config) => {
const {
Expand All @@ -28,7 +33,9 @@ export const makeBorrowInvitation = (zcf, config) => {
} = config;

// We can only lend what the lender has already escrowed.
const maxLoan = lenderSeat.getAmountAllocated('Loan');
const maxLoan = /** @type {NatAmount} */ (
lenderSeat.getAmountAllocated('Loan')
);

/** @type {OfferHandler} */
const borrow = async borrowerSeat => {
Expand All @@ -43,7 +50,10 @@ export const makeBorrowInvitation = (zcf, config) => {
borrowerSeat.getProposal().give.Collateral.brand
),
);
const loanWanted = borrowerSeat.getProposal().want.Loan;
const loanWanted = /** @type {NatAmount} */ (
borrowerSeat.getProposal().want.Loan
);
mustMatch(loanWanted.value, M.nat());
const loanBrand = zcf.getTerms().brands.Loan;

// The value of the collateral in the Loan brand
Expand Down
7 changes: 4 additions & 3 deletions packages/zoe/src/typeGuards.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @jessie-check

import {
AmountBoundShape,
AmountShape,
AssetKindShape,
BrandShape,
Expand Down Expand Up @@ -31,9 +32,9 @@ export const InstallationShape = M.remotable('Installation');
export const SeatShape = M.remotable('Seat');

export const AmountKeywordRecordShape = M.recordOf(KeywordShape, AmountShape);
export const AmountPatternKeywordRecordShape = M.recordOf(
export const AmountBoundKeywordRecordShape = M.recordOf(
KeywordShape,
M.pattern(),
AmountBoundShape,
);
export const PaymentPKeywordRecordShape = M.recordOf(
KeywordShape,
Expand Down Expand Up @@ -80,7 +81,7 @@ export const TimerShape = makeHandleShape('timer');
* @see {ProposalRecord} type
*/
export const FullProposalShape = {
want: AmountPatternKeywordRecordShape,
want: AmountBoundKeywordRecordShape,
give: AmountKeywordRecordShape,
// To accept only one, we could use M.or rather than M.splitRecord,
// but the error messages would have been worse. Rather,
Expand Down
Loading
Loading