From 63ee139f2c0f5b0c4e79d1edefdc8645c6f8092f Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 8 Jun 2023 14:02:25 -0700 Subject: [PATCH 1/6] refactor: move atomicRearrange into Zcf This is the next step of #6577 (atomic reallocations for contracts). It adds native support in ZCF for atomicReallocate, which makes it possible to update contracts from the helper to use the ZCF-native approach, which is a prerequisite for removing the old hazardous staging-based approach. This PR only affects ZCF, though it updates types referenced from inter-protocol contracts. This is required in order to keep the mono-repo consistent, and has no run-time impact on the contracts, even if changes are pushed to some live contracts even before the ZOE change takes effect. The sample contracts in Zoe have also been updated to use the new API. These do not affect the interprotocol contracts. The only zoe contract referenced by inter-protocol contracts is scaledPriceAuthority, which is not changed here. The new code was tested with versions of the inter-protocol contracts updated to use the ZCF-native API. Those contracts were reverted to their old form before merging this PR since the contracts should be updated separately from Zoe. None of the contracts should be updated to use the new reallocation API until this PR is running on chain. They can be individually updated once this code is in place. --- .../inter-protocol/src/auction/auctioneer.js | 4 +- .../src/vaultFactory/liquidation.js | 2 +- .../inter-protocol/src/vaultFactory/types.js | 2 +- .../inter-protocol/src/vaultFactory/vault.js | 2 +- .../src/vaultFactory/vaultDirector.js | 2 +- .../src/vaultFactory/vaultManager.js | 2 +- .../vaultFactory/vault-contract-wrapper.js | 6 +- packages/zoe/src/contractFacet/reallocate.js | 104 +++++++ packages/zoe/src/contractFacet/types.js | 25 +- packages/zoe/src/contractFacet/zcfSeat.js | 102 ++++++- packages/zoe/src/contractFacet/zcfZygote.js | 1 + .../zoe/src/contractSupport/atomicTransfer.js | 13 +- .../zoe/src/contractSupport/zoeHelpers.js | 13 +- .../src/contracts/auction/firstPriceLogic.js | 4 +- .../src/contracts/auction/secondPriceLogic.js | 4 +- packages/zoe/src/contracts/autoswap.js | 10 +- packages/zoe/src/contracts/barterExchange.js | 5 +- .../contracts/callSpread/fundedCallSpread.js | 4 +- .../contracts/callSpread/pricedCallSpread.js | 4 +- packages/zoe/src/contracts/loan/borrow.js | 4 +- packages/zoe/src/contracts/loan/close.js | 8 +- packages/zoe/src/contracts/sellItems.js | 4 +- .../contracts/loan/test-liquidate.js | 4 +- .../unitTests/zcf/test-atomicRearrange.js | 268 ++++++++++++++++++ 24 files changed, 524 insertions(+), 73 deletions(-) create mode 100644 packages/zoe/src/contractFacet/reallocate.js create mode 100644 packages/zoe/test/unitTests/zcf/test-atomicRearrange.js diff --git a/packages/inter-protocol/src/auction/auctioneer.js b/packages/inter-protocol/src/auction/auctioneer.js index 8469ee52107..61df583c5d0 100644 --- a/packages/inter-protocol/src/auction/auctioneer.js +++ b/packages/inter-protocol/src/auction/auctioneer.js @@ -97,7 +97,7 @@ const distributeProportionalShares = ( const collShare = makeRatioFromAmounts(unsoldCollateral, totalCollDeposited); const currShare = makeRatioFromAmounts(proceeds, totalCollDeposited); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = []; let proceedsLeft = proceeds; let collateralLeft = unsoldCollateral; @@ -255,7 +255,7 @@ export const distributeProportionalSharesWithLimits = ( // collateral to reach their share. Then see what's left, and allocate it // among the remaining depositors. Escape to distributeProportionalShares if // anything doesn't work. - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = []; let proceedsLeft = proceeds; let collateralLeft = unsoldCollateral; diff --git a/packages/inter-protocol/src/vaultFactory/liquidation.js b/packages/inter-protocol/src/vaultFactory/liquidation.js index b2efde016c0..e3180fecb1c 100644 --- a/packages/inter-protocol/src/vaultFactory/liquidation.js +++ b/packages/inter-protocol/src/vaultFactory/liquidation.js @@ -267,7 +267,7 @@ export const getLiquidatableVaults = ( const { zcfSeat: liqSeat } = zcf.makeEmptySeatKit(); let totalDebt = AmountMath.makeEmpty(debtBrand); let totalCollateral = AmountMath.makeEmpty(collateralBrand); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = []; for (const vault of vaultsToLiquidate.values()) { diff --git a/packages/inter-protocol/src/vaultFactory/types.js b/packages/inter-protocol/src/vaultFactory/types.js index fe9ed2f90ce..7c189af1a0d 100644 --- a/packages/inter-protocol/src/vaultFactory/types.js +++ b/packages/inter-protocol/src/vaultFactory/types.js @@ -58,7 +58,7 @@ * @param {ZCFSeat} mintReceiver * @param {Amount<'nat'>} toMint * @param {Amount<'nat'>} fee - * @param {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} transfers + * @param {TransferPart[]} transfers * @returns {void} */ diff --git a/packages/inter-protocol/src/vaultFactory/vault.js b/packages/inter-protocol/src/vaultFactory/vault.js index 57f430a0c98..7e14f91c52d 100644 --- a/packages/inter-protocol/src/vaultFactory/vault.js +++ b/packages/inter-protocol/src/vaultFactory/vault.js @@ -545,7 +545,7 @@ export const prepareVault = (baggage, makeRecorderKit, zcf) => { const giveMintedTaken = AmountMath.subtract(fp.give.Minted, surplus); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = harden([ [clientSeat, vaultSeat, { Collateral: fp.give.Collateral }], [vaultSeat, clientSeat, { Collateral: fp.want.Collateral }], diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 0562e7ce153..cebc345d82c 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -196,7 +196,7 @@ const prepareVaultDirector = ( mintAndTransfer: (mintReceiver, toMint, fee, nonMintTransfers) => { const kept = AmountMath.subtract(toMint, fee); debtMint.mintGains(harden({ Minted: toMint }), mintSeat); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = [ ...nonMintTransfers, [mintSeat, rewardPoolSeat, { Minted: fee }], diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index ed3addfb156..1ce51665681 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -687,7 +687,7 @@ export const prepareVaultManagerKit = ( if (plan.transfersToVault.length > 0) { const transfers = plan.transfersToVault.map( ([vaultIndex, amounts]) => - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart} */ ([ + /** @type {TransferPart} */ ([ liqSeat, vaultsInPlan[vaultIndex].getVaultSeat(), amounts, diff --git a/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js index 542eb853c69..bf039f6ef1f 100644 --- a/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -1,14 +1,12 @@ /** @file DEPRECATED use the vault test driver instead */ import { AmountMath, makeIssuerKit } from '@agoric/ertp'; -import { assert } from '@agoric/assert'; import { makePublishKit, observeNotifier } from '@agoric/notifier'; import { makeFakeMarshaller, makeFakeStorage, } from '@agoric/notifier/tools/testSupports.js'; import { - atomicRearrange, prepareRecorderKit, unitAmount, } from '@agoric/zoe/src/contractSupport/index.js'; @@ -101,14 +99,14 @@ export async function start(zcf, privateArgs, baggage) { const mintAndTransfer = (mintReceiver, toMint, fee, nonMintTransfers) => { const kept = AmountMath.subtract(toMint, fee); stableMint.mintGains(harden({ Minted: toMint }), mintSeat); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = [ ...nonMintTransfers, [mintSeat, vaultFactorySeat, { Minted: fee }], [mintSeat, mintReceiver, { Minted: kept }], ]; try { - atomicRearrange(zcf, harden(transfers)); + zcf.atomicRearrange(harden(transfers)); } catch (e) { console.error('mintAndTransfer caught', e); stableMint.burnLosses(harden({ Minted: toMint }), mintSeat); diff --git a/packages/zoe/src/contractFacet/reallocate.js b/packages/zoe/src/contractFacet/reallocate.js new file mode 100644 index 00000000000..43f56d4a912 --- /dev/null +++ b/packages/zoe/src/contractFacet/reallocate.js @@ -0,0 +1,104 @@ +import { makeScalarMapStore } from '@agoric/vat-data'; + +import { assertRightsConserved } from './rightsConservation.js'; +import { addToAllocation, subtractFromAllocation } from './allocationMath.js'; + +const { Fail } = assert; + +/** @typedef {Array} TransactionList */ + +/** + * Convert from a list of transfer descriptions ([fromSeat, toSeat, fromAmount, + * toAmount], with many parts optional) to a list of resulting allocations for + * each of the seats mentioned. + * + * @param {Array} transfers + * @returns {[ZCFSeat,AmountKeywordRecord][]} + */ +export const makeAllocationMap = transfers => { + /** @type {MapStore} */ + const allocations = makeScalarMapStore(); + + const getAllocations = seat => { + if (allocations.has(seat)) { + return allocations.get(seat); + } + + /** @type {[TransactionList, TransactionList]} */ + const pair = [[], []]; + allocations.init(seat, pair); + return pair; + }; + + const updateAllocations = (seat, newAllocation) => { + allocations.set(seat, newAllocation); + }; + + const decrementAllocation = (seat, decrement) => { + const [incr, decr] = getAllocations(seat); + + const newDecr = [...decr, decrement]; + updateAllocations(seat, [incr, newDecr]); + }; + + const incrementAllocation = (seat, increment) => { + const [incr, decr] = getAllocations(seat); + + const newIncr = [...incr, increment]; + updateAllocations(seat, [newIncr, decr]); + }; + + for (const [ + fromSeat = undefined, + toSeat = undefined, + fromAmounts = undefined, + toAmounts = undefined, + ] of transfers) { + if (fromSeat) { + if (!fromAmounts) { + throw Fail`Transfer from ${fromSeat} must say how much`; + } + decrementAllocation(fromSeat, fromAmounts); + if (toSeat) { + // Conserved transfer between seats + if (toAmounts) { + // distinct amounts, so we check conservation. + assertRightsConserved( + Object.values(fromAmounts), + Object.values(toAmounts), + ); + incrementAllocation(toSeat, toAmounts); + } else { + // fromAmounts will be used for toAmounts as well + incrementAllocation(toSeat, fromAmounts); + } + } else { + // Transfer only from fromSeat + !toAmounts || + Fail`Transfer without toSeat cannot have toAmounts ${toAmounts}`; + } + } 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}`; + toAmounts || Fail`Transfer to ${toSeat} must say how much`; + incrementAllocation(toSeat, toAmounts); + } + } + + /** @type {[ZCFSeat,AmountKeywordRecord][]} */ + const resultingAllocations = []; + for (const seat of allocations.keys()) { + const [incrList, decrList] = getAllocations(seat); + let newAlloc = seat.getCurrentAllocation(); + for (const incr of incrList) { + newAlloc = addToAllocation(newAlloc, incr); + } + for (const decr of decrList) { + newAlloc = subtractFromAllocation(newAlloc, decr); + } + resultingAllocations.push([seat, newAlloc]); + } + return resultingAllocations; +}; diff --git a/packages/zoe/src/contractFacet/types.js b/packages/zoe/src/contractFacet/types.js index 843ff6988ff..00577ce9f56 100644 --- a/packages/zoe/src/contractFacet/types.js +++ b/packages/zoe/src/contractFacet/types.js @@ -26,6 +26,7 @@ * synchronously from within the contract, and usually is referred to * in code as zcf. * + * @property {(transfers: TransferPart[]) => void} atomicRearrange - atomically reallocate amounts among seats. * @property {Reallocate} reallocate - reallocate amounts among seats. * Deprecated: Use atomicRearrange instead. * @property {(keyword: Keyword) => void} assertUniqueKeyword - check @@ -59,6 +60,9 @@ */ /** + * @deprecated reallocate() will be supported until at least 2023/09/01. It may + * be removed without further warning any time after 2023/11/01. + * * @typedef {(seat1: ZCFSeat, seat2: ZCFSeat, ...seatRest: * Array) => void} Reallocate * @@ -83,6 +87,15 @@ * effect offer safety for seats whose allocations change. */ +/** + * @typedef {[ + * fromSeat?: ZCFSeat, + * toSeat?: ZCFSeat, + * fromAmounts?: AmountKeywordRecord, + * toAmounts?: AmountKeywordRecord + * ]} TransferPart + */ + /** * @callback SaveIssuer * @@ -184,6 +197,14 @@ * @returns {Amount} */ +/** + * @deprecated Use atomicRearrange instead + * + * @callback DeprecatedIncrementDecrementBy + * @param {AmountKeywordRecord} amountKeywordRecord + * @returns {AmountKeywordRecord} + */ + /** * @typedef {object} ZCFSeat * @property {(completion?: Completion) => void} exit @@ -198,9 +219,9 @@ * @property {() => boolean} hasStagedAllocation * Deprecated: Use atomicRearrange instead * @property {(newAllocation: Allocation) => boolean} isOfferSafe - * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} incrementBy + * @property {DeprecatedIncrementDecrementBy} incrementBy * Deprecated: Use atomicRearrange instead - * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} decrementBy + * @property {DeprecatedIncrementDecrementBy} decrementBy * Deprecated: Use atomicRearrange instead * @property {() => void} clear * Deprecated: Use atomicRearrange instead diff --git a/packages/zoe/src/contractFacet/zcfSeat.js b/packages/zoe/src/contractFacet/zcfSeat.js index a7caa360852..3e4c7d89709 100644 --- a/packages/zoe/src/contractFacet/zcfSeat.js +++ b/packages/zoe/src/contractFacet/zcfSeat.js @@ -1,10 +1,10 @@ import { makeScalarBigWeakMapStore, - provideDurableMapStore, - provideDurableWeakMapStore, + prepareExoClass, prepareExoClassKit, provide, - prepareExoClass, + provideDurableMapStore, + provideDurableWeakMapStore, } from '@agoric/vat-data'; import { E } from '@endo/eventual-send'; import { AmountMath } from '@agoric/ertp'; @@ -19,6 +19,8 @@ import { SeatDataShape, SeatShape, } from '../typeGuards.js'; +import { makeAllocationMap } from './reallocate.js'; +import { TransferPartShape } from '../contractSupport/atomicTransfer.js'; const { Fail } = assert; @@ -214,6 +216,10 @@ export const createSeatManager = ( return isOfferSafe(state.proposal, reallocation); }, + /** + * @deprecated switch to zcf.atomicRearrange() + * @param {AmountKeywordRecord} amountKeywordRecord + */ incrementBy(amountKeywordRecord) { const { self } = this; assertActive(self); @@ -227,6 +233,10 @@ export const createSeatManager = ( ); return amountKeywordRecord; }, + /** + * @deprecated switch to zcf.atomicRearrange() + * @param {AmountKeywordRecord} amountKeywordRecord + */ decrementBy(amountKeywordRecord) { const { self } = this; assertActive(self); @@ -265,6 +275,7 @@ export const createSeatManager = ( const ZcfSeatManagerIKit = harden({ seatManager: M.interface('ZcfSeatManager', { makeZCFSeat: M.call(SeatDataShape).returns(M.remotable('zcfSeat')), + atomicRearrange: M.call(M.arrayOf(TransferPartShape)).returns(), reallocate: M.call(M.remotable('zcfSeat'), M.remotable('zcfSeat')) .rest(M.arrayOf(M.remotable('zcfSeat'))) .returns(), @@ -289,6 +300,91 @@ export const createSeatManager = ( return zcfSeat; }, + /** + * Rearrange the allocations according to the transfer descriptions. + * This is a set of changes to allocations that must satisfy several + * constraints. If these constraints are all met, then the reallocation + * happens atomically. Otherwise, it does not happen at all. + * + * The conditions + * * All the mentioned seats are still live, + * * No outstanding stagings for any of the mentioned seats. Stagings + * have been deprecated in favor or atomicRearrange. To prevent + * confusion, for each reallocation, it can only be expressed in + * the old way or the new way, but not a mixture. + * * Offer safety + * * Overall conservation + * + * The overall transfer is expressed as an array of `TransferPart`. Each + * individual `TransferPart` is one of + * - A transfer from a `fromSeat` to a `toSeat`. Specify both toAmount + * and fromAmount to change keywords, otherwise only fromAmount is required. + * - A taking from a `fromSeat`'s allocation. See the `fromOnly` helper. + * - A giving into a `toSeat`'s allocation. See the `toOnly` helper. + * + * @param {TransferPart[]} transfers + */ + atomicRearrange(transfers) { + const newAllocations = makeAllocationMap(transfers); + + // ////// All Seats are active ///////////////////////////////// + newAllocations.forEach(([seat]) => { + assertActive(seat); + !seat.hasStagedAllocation() || + Fail`Cannot mix atomicRearrange with seat stagings: ${seat}`; + zcfSeatToSeatHandle.has(seat) || + Fail`The seat ${seat} was not recognized`; + }); + + // ////// Ensure that rights are conserved overall ///////////// + const flattenAllocations = allocations => + allocations.flatMap(Object.values); + const previousAmounts = flattenAllocations( + newAllocations.map(([seat]) => seat.getCurrentAllocation()), + ); + const newAmounts = flattenAllocations( + newAllocations.map(([_, allocation]) => allocation), + ); + assertRightsConserved(previousAmounts, newAmounts); + + // ////// Ensure that offer safety holds /////////////////////// + newAllocations.forEach(([seat, allocation]) => { + isOfferSafe(seat.getProposal(), allocation) || + Fail`Offer safety was violated by the proposed allocation: ${allocation}. Proposal was ${seat.getProposal()}`; + }); + + const seatHandleAllocations = newAllocations.map( + ([seat, allocation]) => { + const seatHandle = zcfSeatToSeatHandle.get(seat); + return { seatHandle, allocation }; + }, + ); + try { + // No side effects above. All conditions checked which could have + // caused us to reject this reallocation. Notice that the current + // allocations are captured in seatHandleAllocations, so there must + // be no awaits between that assignment and here. + // + // COMMIT POINT + // + // The effects must succeed atomically. The call to + // replaceAllocations() will be processed in the order of updates + // from zcf to zoe, its effects must occur immediately in zoe on + // reception, and must not fail. + // + // Commit the new allocations (currentAllocation is replaced + // for each of the seats) and inform Zoe of the new allocation. + + newAllocations.map(([seat, allocation]) => + activeZCFSeats.set(seat, allocation), + ); + + E(zoeInstanceAdmin).replaceAllocations(seatHandleAllocations); + } catch (err) { + shutdownWithFailure(err); + throw err; + } + }, reallocate(/** @type {ZCFSeat[]} */ ...seats) { seats.forEach(assertActive); seats.forEach(assertStagedAllocation); diff --git a/packages/zoe/src/contractFacet/zcfZygote.js b/packages/zoe/src/contractFacet/zcfZygote.js index 6bad81fd176..501de00db13 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -245,6 +245,7 @@ export const makeZCFZygote = async ( // accept raw functions. assert cannot be a valid passable! (It's a function // and has members.) const zcf = Remotable('Alleged: zcf', undefined, { + atomicRearrange: transfers => seatManager.atomicRearrange(transfers), reallocate: (...seats) => seatManager.reallocate(...seats), assertUniqueKeyword: kwd => getInstanceRecHolder().assertUniqueKeyword(kwd), saveIssuer: async (issuerP, keyword) => { diff --git a/packages/zoe/src/contractSupport/atomicTransfer.js b/packages/zoe/src/contractSupport/atomicTransfer.js index fa27fe8adc7..2af443d5fe8 100644 --- a/packages/zoe/src/contractSupport/atomicTransfer.js +++ b/packages/zoe/src/contractSupport/atomicTransfer.js @@ -9,15 +9,6 @@ export const TransferPartShape = M.splitArray( harden([M.opt(AmountKeywordRecordShape)]), ); -/** - * @typedef {[ - * fromSeat?: ZCFSeat, - * toSeat?: ZCFSeat, - * fromAmounts?: AmountKeywordRecord, - * toAmounts?: AmountKeywordRecord - * ]} TransferPart - */ - /** * Asks Zoe (via zcf) to rearrange the allocations among the seats * mentioned. This is a set of changes to allocations that must satisfy @@ -52,6 +43,8 @@ export const TransferPartShape = M.splitArray( * which will remain helpers. These helper are for convenience * in expressing atomic rearrangements clearly. * + * @deprecated use the zcf builtin instead + * * @param {ZCF} zcf * @param {TransferPart[]} transfers */ @@ -179,4 +172,4 @@ export const atomicTransfer = ( toSeat = undefined, fromAmounts = undefined, toAmounts = undefined, -) => atomicRearrange(zcf, harden([[fromSeat, toSeat, fromAmounts, toAmounts]])); +) => zcf.atomicRearrange(harden([[fromSeat, toSeat, fromAmounts, toAmounts]])); diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 19dfd1ac4fc..36f2cc9497a 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -4,12 +4,7 @@ import { makePromiseKit } from '@endo/promise-kit'; import { AssetKind } from '@agoric/ertp'; import { fromUniqueEntries } from '@agoric/internal'; import { satisfiesWant } from '../contractFacet/offerSafety.js'; -import { - atomicRearrange, - atomicTransfer, - fromOnly, - toOnly, -} from './atomicTransfer.js'; +import { atomicTransfer, fromOnly, toOnly } from './atomicTransfer.js'; export const defaultAcceptanceMsg = `The offer has been accepted. Once the contract has been completed, please check your payout`; @@ -57,8 +52,7 @@ export const satisfies = (zcf, seat, update) => { /** @type {Swap} */ export const swap = (zcf, leftSeat, rightSeat) => { try { - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [rightSeat, leftSeat, leftSeat.getProposal().want], [leftSeat, rightSeat, rightSeat.getProposal().want], @@ -78,8 +72,7 @@ export const swap = (zcf, leftSeat, rightSeat) => { /** @type {SwapExact} */ export const swapExact = (zcf, leftSeat, rightSeat) => { try { - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ fromOnly(rightSeat, rightSeat.getProposal().give), fromOnly(leftSeat, leftSeat.getProposal().give), diff --git a/packages/zoe/src/contracts/auction/firstPriceLogic.js b/packages/zoe/src/contracts/auction/firstPriceLogic.js index 294a3837ed4..d44239b37c6 100644 --- a/packages/zoe/src/contracts/auction/firstPriceLogic.js +++ b/packages/zoe/src/contracts/auction/firstPriceLogic.js @@ -1,5 +1,4 @@ import { AmountMath } from '@agoric/ertp'; -import { atomicRearrange } from '../../contractSupport/index.js'; /** * @param {ZCF} zcf @@ -42,8 +41,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { } // Everyone else gets a refund so their values remain the same. - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [highestBidSeat, sellSeat, { Bid: highestBid }, { Ask: highestBid }], [sellSeat, highestBidSeat, { Asset: assetAmount }], diff --git a/packages/zoe/src/contracts/auction/secondPriceLogic.js b/packages/zoe/src/contracts/auction/secondPriceLogic.js index 29d1faca8fc..7d08cbc1b20 100644 --- a/packages/zoe/src/contracts/auction/secondPriceLogic.js +++ b/packages/zoe/src/contracts/auction/secondPriceLogic.js @@ -1,5 +1,4 @@ import { AmountMath } from '@agoric/ertp'; -import { atomicRearrange } from '../../contractSupport/index.js'; /** * @param {ZCF} zcf @@ -48,8 +47,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { } // Everyone else gets a refund so their values remain the same. - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [ highestBidSeat, diff --git a/packages/zoe/src/contracts/autoswap.js b/packages/zoe/src/contracts/autoswap.js index 6d717ae153c..1b9f58f40a3 100644 --- a/packages/zoe/src/contracts/autoswap.js +++ b/packages/zoe/src/contracts/autoswap.js @@ -11,7 +11,6 @@ import { assertProposalShape, assertNatAssetKind, calcSecondaryRequired, - atomicRearrange, } from '../contractSupport/index.js'; /** @@ -88,8 +87,7 @@ const start = async zcf => { }; function consummate(tradeAmountIn, tradeAmountOut, swapSeat) { - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [ swapSeat, @@ -205,8 +203,7 @@ const start = async zcf => { Central: AmountMath.make(brands.Central, centralIn), Secondary: secondaryAmount, }; - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [seat, poolSeat, liquidityDeposited], [poolSeat, seat, { Liquidity: liquidityAmountOut }], @@ -308,8 +305,7 @@ const start = async zcf => { Secondary: newUserSecondaryAmount, }; - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [removeLiqSeat, poolSeat, { Liquidity: userAllocation.Liquidity }], [poolSeat, removeLiqSeat, liquidityRemoved], diff --git a/packages/zoe/src/contracts/barterExchange.js b/packages/zoe/src/contracts/barterExchange.js index 2ed71f86de1..5320359e212 100644 --- a/packages/zoe/src/contracts/barterExchange.js +++ b/packages/zoe/src/contracts/barterExchange.js @@ -1,7 +1,7 @@ import { Far } from '@endo/marshal'; import { makeLegacyMap } from '@agoric/store'; // Eventually will be importable from '@agoric/zoe-contract-support' -import { satisfies, atomicRearrange } from '../contractSupport/index.js'; +import { satisfies } from '../contractSupport/index.js'; /** * This Barter Exchange accepts offers to trade arbitrary goods for other @@ -63,8 +63,7 @@ const start = zcf => { const matchingTrade = findMatchingTrade(offerDetails, orders); if (matchingTrade) { // reallocate by giving each side what it wants - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [ offerDetails.seat, diff --git a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js index 28d965687b9..c15ba721f34 100644 --- a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js +++ b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js @@ -7,7 +7,6 @@ import { assertProposalShape, depositToSeat, assertNatAssetKind, - atomicRearrange, } from '../../contractSupport/index.js'; import { makePayoffHandler } from './payoffHandler.js'; import { Position } from './position.js'; @@ -112,8 +111,7 @@ const start = async zcf => { give: { Collateral: null }, want: { LongOption: null, ShortOption: null }, }); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [creatorSeat, collateralSeat, { Collateral: settlementAmount }], [ diff --git a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js index 10add08117a..9b06e16fd25 100644 --- a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js +++ b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js @@ -10,7 +10,6 @@ import { assertNatAssetKind, makeRatio, ceilMultiplyBy, - atomicRearrange, } from '../../contractSupport/index.js'; import { makePayoffHandler } from './payoffHandler.js'; import { Position } from './position.js'; @@ -132,8 +131,7 @@ const start = zcf => { 'wanted option not a match', ); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [collateralSeat, depositSeat, spreadAmount], [depositSeat, collateralSeat, { Collateral: newCollateral }], diff --git a/packages/zoe/src/contracts/loan/borrow.js b/packages/zoe/src/contracts/loan/borrow.js index a50285d6c37..f24a039ed26 100644 --- a/packages/zoe/src/contracts/loan/borrow.js +++ b/packages/zoe/src/contracts/loan/borrow.js @@ -9,7 +9,6 @@ import { getAmountOut, ceilMultiplyBy, getTimestamp, - atomicRearrange, } from '../../contractSupport/index.js'; import { scheduleLiquidation } from './scheduleLiquidation.js'; @@ -72,8 +71,7 @@ export const makeBorrowInvitation = (zcf, config) => { const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ // Transfer the wanted Loan amount to the borrower [lenderSeat, borrowerSeat, { Loan: loanWanted }], diff --git a/packages/zoe/src/contracts/loan/close.js b/packages/zoe/src/contracts/loan/close.js index 2351817758b..2d7060befb8 100644 --- a/packages/zoe/src/contracts/loan/close.js +++ b/packages/zoe/src/contracts/loan/close.js @@ -3,10 +3,7 @@ import './types.js'; import { Fail } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { - assertProposalShape, - atomicRearrange, -} from '../../contractSupport/index.js'; +import { assertProposalShape } from '../../contractSupport/index.js'; // The debt, the amount which must be repaid, is just the amount // loaned plus interest (aka stability fee). All debt must be repaid @@ -44,8 +41,7 @@ export const makeCloseLoanInvitation = (zcf, config) => { 'Collateral', collateralBrand, ); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [collateralSeat, repaySeat, { Collateral: collateralAmount }], [repaySeat, lenderSeat, { Loan: debt }], diff --git a/packages/zoe/src/contracts/sellItems.js b/packages/zoe/src/contracts/sellItems.js index 77b43ade259..7ec99d2da9b 100644 --- a/packages/zoe/src/contracts/sellItems.js +++ b/packages/zoe/src/contracts/sellItems.js @@ -11,7 +11,6 @@ import { defaultAcceptanceMsg, assertProposalShape, assertNatAssetKind, - atomicRearrange, } from '../contractSupport/index.js'; const { Fail } = assert; @@ -107,8 +106,7 @@ const start = zcf => { Fail`More money (${totalCost}) is required to buy these items`; // Reallocate. - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [buyerSeat, sellerSeat, { Money: providedMoney }], [sellerSeat, buyerSeat, { Items: wantedItems }], diff --git a/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js b/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js index d6f2e79c90d..4a321559beb 100644 --- a/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js +++ b/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js @@ -11,7 +11,6 @@ import { checkNoNewOffers, checkPayouts, } from './helpers.js'; -import { atomicRearrange } from '../../../../src/contractSupport/atomicTransfer.js'; test('test doLiquidation with mocked autoswap', async t => { const { zcf, collateralKit, loanKit } = await setupLoanUnitTest(); @@ -43,8 +42,7 @@ test('test doLiquidation with mocked autoswap', async t => { const swapHandler = swapSeat => { // swapSeat gains 20 loan tokens from fakePoolSeat, loses all collateral - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [swapSeat, fakePoolSeat, { In: collateral }, { Secondary: collateral }], [fakePoolSeat, swapSeat, { Central: price }, { Out: price }], diff --git a/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js b/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js new file mode 100644 index 00000000000..80de4f34aba --- /dev/null +++ b/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js @@ -0,0 +1,268 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { AmountMath } from '@agoric/ertp'; +import { + fromOnly, + toOnly, +} from '../../../src/contractSupport/atomicTransfer.js'; + +import { setupZCFTest } from './setupZcfTest.js'; +import { makeOffer } from '../makeOffer.js'; +import { setup } from '../setupBasicMints.js'; + +const zcfMintUtils = async (zcf, name) => { + const mint = await zcf.makeZCFMint(name); + const { brand } = mint.getIssuerRecord(); + + return { + make: v => AmountMath.make(brand, v), + makeEmpty: () => AmountMath.makeEmpty(brand), + brand: () => brand, + mintGains: (seat, v, t) => + mint.mintGains(harden({ [t]: AmountMath.make(brand, v) }), seat), + }; +}; + +const assertAllocations = (t, seat, expectedAllocations) => { + const allocations = seat.getCurrentAllocation(); + t.deepEqual(allocations, expectedAllocations, 'allocations did not match'); +}; + +test(`zcf.atomicRearrange all Legal Combinations`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + const { zcfSeat: carol } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [bob, carol, { B: bucks.make(30n) }], + [alice, carol, { M: moola.make(15n) }, { C: moola.make(15n) }], + fromOnly(bob, { B: bucks.make(12n) }), + toOnly(alice, { N: bucks.make(12n) }), + ]); + assertAllocations(t, alice, { M: moola.make(65n), N: bucks.make(12n) }); + assertAllocations(t, bob, { B: bucks.make(8n), M: moola.make(20n) }); + assertAllocations(t, carol, { B: bucks.make(30n), C: moola.make(15n) }); +}); + +test(`zcf.atomicRearrange missing to amount`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + const { zcfSeat: carol } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws(() => zcf.atomicRearrange([[undefined, carol, undefined]]), { + message: /must say how much/, + }); +}); + +test(`zcf.atomicRearrange too few argumemnts`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + // @ts-expect-error Testing + [bob, { B: bucks.make(30n) }], + ]), + { message: /Expected at least 3 arguments/ }, + ); +}); + +test(`zcf.atomicRearrange no seats`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [undefined, undefined, { B: bucks.make(30n) }], + ]), + { message: /Transfer must have at least one of fromSeat or toSeat/ }, + ); +}); + +test(`zcf.atomicRearrange no transfers`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + // @ts-expect-error Testing + [bob, alice, bob], + ]), + { message: /Must be a copyRecord/ }, + ); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [bob, alice, undefined, undefined], + ]), + { message: / must say how much/ }, + ); +}); + +test(`zcf.atomicRearrange no FromAmount`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [alice, bob, undefined, { M: moola.make(20n) }], + ]), + { message: /must say how much/ }, + ); +}); + +test(`zcf.atomicRearrange FromAmount w/o fromSeat`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [undefined, bob, { B: bucks.make(37n) }, { M: moola.make(20n) }], + ]), + { message: /Transfer without fromSeat cannot have fromAmounts/ }, + ); +}); + +test(`zcf.atomicRearrange toAmount w/o toSeat`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [alice, undefined, { B: bucks.make(37n) }, { M: moola.make(20n) }], + ]), + { message: /Transfer without toSeat cannot have toAmounts/ }, + ); +}); + +test(`zcf.atomicRearrange breaks conservation`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + toOnly(bob, { C: bucks.make(27n) }), + ]), + { message: /rights were not conserved for brand / }, + ); + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + fromOnly(bob, { B: bucks.make(10n) }), + ]), + { message: /rights were not conserved for brand / }, + ); +}); + +test(`zcf.atomicRearrange breaks offerSafety`, async t => { + const { moolaKit, moola, bucksKit, bucks } = setup(); + const { zoe, zcf } = await setupZCFTest({ + M: moolaKit.issuer, + B: bucksKit.issuer, + }); + + const { zcfSeat: alice } = await makeOffer( + zoe, + zcf, + harden({ give: { M: moola(30n) }, want: { B: bucks(200n) } }), + harden({ M: moolaKit.mint.mintPayment(moola(30n)) }), + ); + + const { zcfSeat: bob } = await makeOffer( + zoe, + zcf, + harden({ give: { B: bucks(20n) } }), + harden({ B: bucksKit.mint.mintPayment(bucks(20n)) }), + ); + + t.throws( + () => + zcf.atomicRearrange([ + [bob, alice, { B: bucks(20n) }], + [alice, bob, { M: moola(30n) }], + ]), + { message: /Offer safety was violated/ }, + ); +}); From ff54e41290676bc3626e1fdece8b00e22db90492 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 8 Jun 2023 15:38:17 -0700 Subject: [PATCH 2/6] test: exclude test-atomicRearrange from XS testing --- packages/zoe/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/zoe/package.json b/packages/zoe/package.json index d55e6b7851b..78627ab1ca0 100644 --- a/packages/zoe/package.json +++ b/packages/zoe/package.json @@ -104,6 +104,7 @@ "test/unitTests/zcf/test-reallocate-empty.js", "test/unitTests/zcf/test-zoeHelpersWZcf.js", "test/unitTests/zcf/test-reallocateForZCFMint.js", + "test/unitTests/zcf/test-atomicRearrange.js", "test/unitTests/zcf/test-zcf.js", "test/unitTests/zcf/test-allStagedSeatsUsed.js", "# ManualTimer.setWakeup: no function", From 03ac2cd563541ca9390857b56ab1a7773d41b33e Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 9 Jun 2023 09:51:50 -0700 Subject: [PATCH 3/6] chore: remove unhelpful type and default declaration --- packages/zoe/src/contractFacet/reallocate.js | 7 +------ packages/zoe/src/contractFacet/types.js | 12 ++---------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/zoe/src/contractFacet/reallocate.js b/packages/zoe/src/contractFacet/reallocate.js index 43f56d4a912..5eedaa9cf45 100644 --- a/packages/zoe/src/contractFacet/reallocate.js +++ b/packages/zoe/src/contractFacet/reallocate.js @@ -48,12 +48,7 @@ export const makeAllocationMap = transfers => { updateAllocations(seat, [newIncr, decr]); }; - for (const [ - fromSeat = undefined, - toSeat = undefined, - fromAmounts = undefined, - toAmounts = undefined, - ] of transfers) { + for (const [fromSeat, toSeat, fromAmounts, toAmounts] of transfers) { if (fromSeat) { if (!fromAmounts) { throw Fail`Transfer from ${fromSeat} must say how much`; diff --git a/packages/zoe/src/contractFacet/types.js b/packages/zoe/src/contractFacet/types.js index 00577ce9f56..aa6dee2f73d 100644 --- a/packages/zoe/src/contractFacet/types.js +++ b/packages/zoe/src/contractFacet/types.js @@ -197,14 +197,6 @@ * @returns {Amount} */ -/** - * @deprecated Use atomicRearrange instead - * - * @callback DeprecatedIncrementDecrementBy - * @param {AmountKeywordRecord} amountKeywordRecord - * @returns {AmountKeywordRecord} - */ - /** * @typedef {object} ZCFSeat * @property {(completion?: Completion) => void} exit @@ -219,9 +211,9 @@ * @property {() => boolean} hasStagedAllocation * Deprecated: Use atomicRearrange instead * @property {(newAllocation: Allocation) => boolean} isOfferSafe - * @property {DeprecatedIncrementDecrementBy} incrementBy + * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} incrementBy * Deprecated: Use atomicRearrange instead - * @property {DeprecatedIncrementDecrementBy} decrementBy + * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} decrementBy * Deprecated: Use atomicRearrange instead * @property {() => void} clear * Deprecated: Use atomicRearrange instead From f54a73b7db52b63d2c43608bf64fed70e5b1e99e Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 23 Jun 2023 12:50:33 -0700 Subject: [PATCH 4/6] chore: simplify remain atomicRearrange helper; use forEach over map --- packages/zoe/src/contractFacet/zcfSeat.js | 2 +- .../zoe/src/contractSupport/atomicTransfer.js | 82 +------------------ 2 files changed, 3 insertions(+), 81 deletions(-) diff --git a/packages/zoe/src/contractFacet/zcfSeat.js b/packages/zoe/src/contractFacet/zcfSeat.js index 3e4c7d89709..b9be8729f07 100644 --- a/packages/zoe/src/contractFacet/zcfSeat.js +++ b/packages/zoe/src/contractFacet/zcfSeat.js @@ -375,7 +375,7 @@ export const createSeatManager = ( // Commit the new allocations (currentAllocation is replaced // for each of the seats) and inform Zoe of the new allocation. - newAllocations.map(([seat, allocation]) => + newAllocations.forEach(([seat, allocation]) => activeZCFSeats.set(seat, allocation), ); diff --git a/packages/zoe/src/contractSupport/atomicTransfer.js b/packages/zoe/src/contractSupport/atomicTransfer.js index 2af443d5fe8..befda09311c 100644 --- a/packages/zoe/src/contractSupport/atomicTransfer.js +++ b/packages/zoe/src/contractSupport/atomicTransfer.js @@ -1,9 +1,6 @@ -import { mustMatch, M } from '@agoric/store'; -import { assertRightsConserved } from '../contractFacet/rightsConservation.js'; +import { M } from '@agoric/store'; import { AmountKeywordRecordShape, SeatShape } from '../typeGuards.js'; -const { Fail, quote: q } = assert; - export const TransferPartShape = M.splitArray( harden([M.opt(SeatShape), M.opt(SeatShape), M.opt(AmountKeywordRecordShape)]), harden([M.opt(AmountKeywordRecordShape)]), @@ -49,82 +46,7 @@ export const TransferPartShape = M.splitArray( * @param {TransferPart[]} transfers */ export const atomicRearrange = (zcf, transfers) => { - mustMatch(transfers, M.arrayOf(M.array()), 'transfers'); - const uniqueSeatSet = new Set(); - for (const [ - fromSeat = undefined, - toSeat = undefined, - fromAmounts = undefined, - toAmounts = undefined, - ] of transfers) { - if (fromSeat) { - if (!fromAmounts) { - throw Fail`Transfer from ${fromSeat} must say how much`; - } - uniqueSeatSet.add(fromSeat); - if (toSeat) { - // Conserved transfer between seats - if (toAmounts) { - // distinct amounts, so we check conservation. - assertRightsConserved( - Object.values(fromAmounts), - Object.values(toAmounts), - ); - } // else fromAmounts will be used as toAmounts - uniqueSeatSet.add(toSeat); - } else { - // Transfer only from fromSeat - !toAmounts || - Fail`Transfer without toSeat cannot have toAmounts ${toAmounts}`; - } - } 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}`; - toAmounts || Fail`Transfer to ${toSeat} must say how much`; - uniqueSeatSet.add(toSeat); - } - } - - const uniqueSeats = harden([...uniqueSeatSet.keys()]); - for (const seat of uniqueSeats) { - !seat.hasStagedAllocation() || - Fail`Cannot mix atomicRearrange with seat stagings: ${seat}`; - } - - // At this point the basic shape has been validated - - try { - for (const [ - fromSeat = undefined, - toSeat = undefined, - fromAmounts = undefined, - toAmounts = toSeat && fromAmounts, - ] of transfers) { - if (fromSeat && fromAmounts) { - // testing both just to satisfy the type checker - fromSeat.decrementBy(fromAmounts); - } - if (toSeat && toAmounts) { - // testing both just to satisfy the type checker - toSeat.incrementBy(toAmounts); - } - } - - // Perhaps deprecate this >= 2 restriction? - uniqueSeats.length >= 2 || - Fail`Can only commit a reallocation among at least 2 seats: ${q( - uniqueSeats.length, - )}`; - // Take it apart and put it back together to satisfy the type checker - const [seat0, seat1, ...restSeats] = uniqueSeats; - zcf.reallocate(seat0, seat1, ...restSeats); - } finally { - for (const seat of uniqueSeats) { - seat.clear(); - } - } + zcf.atomicRearrange(transfers); }; /** From b9c3ff2fe0da101249e71600bbbcd41044c48ebb Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 23 Jun 2023 16:30:42 -0700 Subject: [PATCH 5/6] chore: local review clean-ups --- packages/zoe/src/contractFacet/reallocate.js | 13 +++------- packages/zoe/src/contractFacet/types.js | 3 +-- packages/zoe/src/contractFacet/zcfSeat.js | 26 +++++++++++--------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/zoe/src/contractFacet/reallocate.js b/packages/zoe/src/contractFacet/reallocate.js index 5eedaa9cf45..f09b7f031c4 100644 --- a/packages/zoe/src/contractFacet/reallocate.js +++ b/packages/zoe/src/contractFacet/reallocate.js @@ -13,7 +13,7 @@ const { Fail } = assert; * each of the seats mentioned. * * @param {Array} transfers - * @returns {[ZCFSeat,AmountKeywordRecord][]} + * @returns {[ZCFSeat, AmountKeywordRecord][]} */ export const makeAllocationMap = transfers => { /** @type {MapStore} */ @@ -30,22 +30,18 @@ export const makeAllocationMap = transfers => { return pair; }; - const updateAllocations = (seat, newAllocation) => { - allocations.set(seat, newAllocation); - }; - const decrementAllocation = (seat, decrement) => { const [incr, decr] = getAllocations(seat); const newDecr = [...decr, decrement]; - updateAllocations(seat, [incr, newDecr]); + allocations.set(seat, [incr, newDecr]); }; const incrementAllocation = (seat, increment) => { const [incr, decr] = getAllocations(seat); const newIncr = [...incr, increment]; - updateAllocations(seat, [newIncr, decr]); + allocations.set(seat, [newIncr, decr]); }; for (const [fromSeat, toSeat, fromAmounts, toAmounts] of transfers) { @@ -84,8 +80,7 @@ export const makeAllocationMap = transfers => { /** @type {[ZCFSeat,AmountKeywordRecord][]} */ const resultingAllocations = []; - for (const seat of allocations.keys()) { - const [incrList, decrList] = getAllocations(seat); + for (const [seat, [incrList, decrList]] of allocations.entries()) { let newAlloc = seat.getCurrentAllocation(); for (const incr of incrList) { newAlloc = addToAllocation(newAlloc, incr); diff --git a/packages/zoe/src/contractFacet/types.js b/packages/zoe/src/contractFacet/types.js index aa6dee2f73d..7999bb3650c 100644 --- a/packages/zoe/src/contractFacet/types.js +++ b/packages/zoe/src/contractFacet/types.js @@ -60,8 +60,7 @@ */ /** - * @deprecated reallocate() will be supported until at least 2023/09/01. It may - * be removed without further warning any time after 2023/11/01. + * @deprecated reallocate(). Use zcf.atomicRearrange() instead * * @typedef {(seat1: ZCFSeat, seat2: ZCFSeat, ...seatRest: * Array) => void} Reallocate diff --git a/packages/zoe/src/contractFacet/zcfSeat.js b/packages/zoe/src/contractFacet/zcfSeat.js index b9be8729f07..955e13848a0 100644 --- a/packages/zoe/src/contractFacet/zcfSeat.js +++ b/packages/zoe/src/contractFacet/zcfSeat.js @@ -328,35 +328,37 @@ export const createSeatManager = ( const newAllocations = makeAllocationMap(transfers); // ////// All Seats are active ///////////////////////////////// - newAllocations.forEach(([seat]) => { + for (const [seat] of newAllocations) { assertActive(seat); !seat.hasStagedAllocation() || Fail`Cannot mix atomicRearrange with seat stagings: ${seat}`; zcfSeatToSeatHandle.has(seat) || Fail`The seat ${seat} was not recognized`; - }); + } // ////// Ensure that rights are conserved overall ///////////// - const flattenAllocations = allocations => + + // convert array of keywordAmountRecords to 1-level array of Amounts + const flattenAmounts = allocations => allocations.flatMap(Object.values); - const previousAmounts = flattenAllocations( + const previousAmounts = flattenAmounts( newAllocations.map(([seat]) => seat.getCurrentAllocation()), ); - const newAmounts = flattenAllocations( + const newAmounts = flattenAmounts( newAllocations.map(([_, allocation]) => allocation), ); assertRightsConserved(previousAmounts, newAmounts); // ////// Ensure that offer safety holds /////////////////////// - newAllocations.forEach(([seat, allocation]) => { + for (const [seat, allocation] of newAllocations) { isOfferSafe(seat.getProposal(), allocation) || Fail`Offer safety was violated by the proposed allocation: ${allocation}. Proposal was ${seat.getProposal()}`; - }); + } const seatHandleAllocations = newAllocations.map( ([seat, allocation]) => { const seatHandle = zcfSeatToSeatHandle.get(seat); - return { seatHandle, allocation }; + return { allocation, seatHandle }; }, ); try { @@ -369,15 +371,15 @@ export const createSeatManager = ( // // The effects must succeed atomically. The call to // replaceAllocations() will be processed in the order of updates - // from zcf to zoe, its effects must occur immediately in zoe on + // from ZCF to Zoe. Its effects must occur immediately in Zoe on // reception, and must not fail. // // Commit the new allocations (currentAllocation is replaced // for each of the seats) and inform Zoe of the new allocation. - newAllocations.forEach(([seat, allocation]) => - activeZCFSeats.set(seat, allocation), - ); + for (const [seat, allocation] of newAllocations) { + activeZCFSeats.set(seat, allocation); + } E(zoeInstanceAdmin).replaceAllocations(seatHandleAllocations); } catch (err) { From 8de85007348982a157d6fe4c67683a9d81a72b17 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 21 Jul 2023 15:16:46 -0700 Subject: [PATCH 6/6] doc: add void label on remote call to replaceAllocatios() & document --- packages/zoe/src/contractFacet/zcfSeat.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/zoe/src/contractFacet/zcfSeat.js b/packages/zoe/src/contractFacet/zcfSeat.js index 955e13848a0..006ea2840cb 100644 --- a/packages/zoe/src/contractFacet/zcfSeat.js +++ b/packages/zoe/src/contractFacet/zcfSeat.js @@ -381,7 +381,10 @@ export const createSeatManager = ( activeZCFSeats.set(seat, allocation); } - E(zoeInstanceAdmin).replaceAllocations(seatHandleAllocations); + // we don't wait for the results here. As described in + // docs/zoe-zcf.md, The initial allocation to a seat originates with + // Zoe, but *all subsequent updates come from ZCF to Zoe*. + void E(zoeInstanceAdmin).replaceAllocations(seatHandleAllocations); } catch (err) { shutdownWithFailure(err); throw err;