@agoric/zoe
Version:
Zoe: the Smart Contract Framework for Offer Enforcement
149 lines (131 loc) • 4.42 kB
JavaScript
import { makeNotifierKit } from '@agoric/notifier';
import { Far } from '@endo/marshal';
import {
swap,
satisfies,
assertProposalShape,
assertIssuerKeywords,
} from '../contractSupport/zoeHelpers.js';
/**
* @import {ContractMeta, Invitation, OfferHandler, ZCF, ZCFSeat} from '@agoric/zoe';
*/
/**
* SimpleExchange is an exchange with a simple matching algorithm, which allows
* an unlimited number of parties to create new orders or accept existing
* orders. The notifier allows callers to find the current list of orders.
* https://agoric.com/documentation/zoe/guide/contracts/simple-exchange.html
*
* The SimpleExchange uses Asset and Price as its keywords. The contract treats
* the two keywords symmetrically. New offers can be created and existing offers
* can be accepted in either direction.
*
* { give: { 'Asset', simoleans(5n) }, want: { 'Price', quatloos(3) } }
* { give: { 'Price', quatloos(8) }, want: { 'Asset', simoleans(3n) } }
*
* The Asset is treated as an exact amount to be exchanged, while the
* Price is a limit that may be improved on. This simple exchange does
* not partially fill orders.
*
* The publicFacet is returned from the contract.
*
* @param {ZCF} zcf
*/
const start = zcf => {
let sellSeats = [];
let buySeats = [];
const { notifier, updater } = makeNotifierKit(getBookOrders());
assertIssuerKeywords(zcf, harden(['Asset', 'Price']));
function dropExit(p) {
return {
want: p.want,
give: p.give,
};
}
function flattenOrders(seats) {
const activeSeats = seats.filter(s => !s.hasExited());
return activeSeats.map(seat => dropExit(seat.getProposal()));
}
function getBookOrders() {
return {
buys: flattenOrders(buySeats),
sells: flattenOrders(sellSeats),
};
}
// Tell the notifier that there has been a change to the book orders
function bookOrdersChanged() {
updater.updateState(getBookOrders());
}
// If there's an existing offer that this offer is a match for, make the trade
// and return the seat for the matched offer. If not, return undefined, so
// the caller can know to add the new offer to the book.
function swapIfCanTrade(offers, seat) {
for (const offer of offers) {
const satisfiedBy = (xSeat, ySeat) =>
satisfies(zcf, xSeat, ySeat.getCurrentAllocation());
if (satisfiedBy(offer, seat) && satisfiedBy(seat, offer)) {
swap(zcf, seat, offer);
// return handle to remove
return offer;
}
}
return undefined;
}
// try to swap offerHandle with one of the counterOffers. If it works, remove
// the matching offer and return the remaining counterOffers. If there's no
// matching offer, add the offerHandle to the coOffers, and return the
// unmodified counterOfffers
function swapIfCanTradeAndUpdateBook(counterOffers, coOffers, seat) {
const offer = swapIfCanTrade(counterOffers, seat);
if (offer) {
// remove the matched offer.
counterOffers = counterOffers.filter(value => value !== offer);
} else {
// Save the order in the book
coOffers.push(seat);
}
bookOrdersChanged();
return counterOffers;
}
const sell = seat => {
assertProposalShape(seat, {
give: { Asset: null },
want: { Price: null },
});
buySeats = swapIfCanTradeAndUpdateBook(buySeats, sellSeats, seat);
return 'Order Added';
};
const buy = seat => {
assertProposalShape(seat, {
give: { Price: null },
want: { Asset: null },
});
sellSeats = swapIfCanTradeAndUpdateBook(sellSeats, buySeats, seat);
return 'Order Added';
};
/** @type {OfferHandler} */
const exchangeOfferHandler = seat => {
// Buy Order
if (seat.getProposal().want.Asset) {
return buy(seat);
}
// Sell Order
if (seat.getProposal().give.Asset) {
return sell(seat);
}
// Eject because the offer must be invalid
throw seat.fail(
Error(`The proposal did not match either a buy or sell order.`),
);
};
const makeExchangeInvitation = () =>
zcf.makeInvitation(exchangeOfferHandler, 'exchange');
const publicFacet = Far('SimpleExchangePublicFacet', {
makeInvitation: makeExchangeInvitation,
getNotifier: () => notifier,
});
// set the initial state of the notifier
bookOrdersChanged();
return harden({ publicFacet });
};
harden(start);
export { start };