-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #49 from Agoric/dc-contract-docs2
fix: document contract; refactor for clarity; use defensive patterns
- Loading branch information
Showing
1 changed file
with
109 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,141 @@ | ||
/** @file Contract to mint and sell Item NFTs. */ | ||
/** | ||
* @file Contract to mint and sell a few Item NFTs at a time. | ||
* | ||
* We declare variables (including functions) before using them, | ||
* so you may want to skip ahead and come back to some details. | ||
* @see {start} for the main contract entrypoint | ||
* | ||
* As is typical in Zoe contracts, the flow is: | ||
* 1. contract does internal setup and returns public / creator facets. | ||
* 2. client uses a public facet method -- {@link makeTradeInvitation} in this case -- | ||
* to make an invitation. | ||
* 3. client makes an offer using the invitation, along with | ||
* a proposal (with give and want) and payments. Zoe escrows the payments, and then | ||
* 4. Zoe invokes the offer handler specified in step 2 -- here {@link tradeHandler}. | ||
* | ||
* @see {@link https://docs.agoric.com/guides/zoe/|Zoe Overview} for a walk-thru of this contract | ||
* @see {@link https://docs.agoric.com/guides/js-programming/hardened-js.html|Hardened JavaScript} | ||
* for background on `harden` and `assert`. | ||
*/ | ||
// @ts-check | ||
|
||
import { Far } from '@endo/far'; | ||
import { M, getCopyBagEntries } from '@endo/patterns'; | ||
import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; | ||
import { AssetKind } from '@agoric/ertp/src/amountMath.js'; | ||
import { AmountShape } from '@agoric/ertp/src/typeGuards.js'; | ||
// Use the deprecated atomicRearrange API | ||
// for compatibility with mainnet1B. | ||
import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; | ||
import '@agoric/zoe/exported.js'; | ||
|
||
import { makeTracer } from './debug.js'; | ||
|
||
const { Fail, quote: q } = assert; | ||
|
||
const trace = makeTracer('OfferUp', true); | ||
// #region bag utilities | ||
/** @type { (xs: bigint[]) => bigint } */ | ||
const sum = xs => xs.reduce((acc, x) => acc + x, 0n); | ||
|
||
/** | ||
* @param {import('@endo/patterns').CopyBag} bag | ||
* @returns {bigint[]} | ||
*/ | ||
const bagCounts = bag => { | ||
const entries = getCopyBagEntries(bag); | ||
return entries.map(([_k, ct]) => ct); | ||
}; | ||
// #endregion | ||
|
||
/** | ||
* In addition to the standard `issuers` and `brands` terms, | ||
* this contract is parameterized by terms for price and, | ||
* optionally, a maximum number of items sold for that price (default: 3). | ||
* | ||
* @typedef {{ | ||
* tradePrice: Amount; | ||
* maxItems?: bigint; | ||
* }} OfferUpTerms | ||
*/ | ||
|
||
/** @param {Amount<'copyBag'>} amt */ | ||
const bagValueSize = amt => { | ||
/** @type {[unknown, bigint][]} */ | ||
const entries = getCopyBagEntries(amt.value); // XXX getCopyBagEntries returns any??? | ||
const total = entries.reduce((acc, [_, qty]) => acc + qty, 0n); | ||
return total; | ||
export const meta = { | ||
customTermsShape: M.splitRecord( | ||
{ tradePrice: AmountShape }, | ||
{ maxItems: M.bigint() }, | ||
), | ||
}; | ||
// compatibility with an earlier contract metadata API | ||
export const customTermsShape = meta.customTermsShape; | ||
|
||
/** | ||
* @param {ZCF<{tradePrice: Amount}>} zcf | ||
* Start a contract that | ||
* - creates a new non-fungible asset type for Items, and | ||
* - handles offers to buy up to `maxItems` items at a time. | ||
* | ||
* @param {ZCF<OfferUpTerms>} zcf | ||
*/ | ||
export const start = async zcf => { | ||
const { tradePrice } = zcf.getTerms(); | ||
const { tradePrice, maxItems = 3n } = zcf.getTerms(); | ||
|
||
const { zcfSeat: proceeds } = zcf.makeEmptySeatKit(); | ||
const mint = await zcf.makeZCFMint('Item', AssetKind.COPY_BAG); | ||
/** | ||
* a new ERTP mint for items, accessed thru the Zoe Contract Facet. | ||
* Note: `makeZCFMint` makes the associated brand and issuer available | ||
* in the contract's terms. | ||
* | ||
* AssetKind.COPY_BAG can express non-fungible (or rather: semi-fungible) | ||
* amounts such as: 3 potions and 1 map. | ||
*/ | ||
const itemMint = await zcf.makeZCFMint('Item', AssetKind.COPY_BAG); | ||
const { brand: itemBrand } = itemMint.getIssuerRecord(); | ||
|
||
const tradeShape = harden({ | ||
give: { Price: AmountShape }, | ||
want: { Items: AmountShape }, | ||
/** | ||
* a pattern to constrain proposals given to {@link tradeHandler} | ||
* | ||
* The `Price` amount must be >= `tradePrice` term. | ||
* The `Items` amount must use the `Item` brand and a bag value. | ||
*/ | ||
const proposalShape = harden({ | ||
give: { Price: M.gte(tradePrice) }, | ||
want: { Items: { brand: itemBrand, value: M.bag() } }, | ||
exit: M.any(), | ||
}); | ||
|
||
/** @param {ZCFSeat} buyerSeat */ | ||
const tradeHandler = buyerSeat => { | ||
const { give, want } = buyerSeat.getProposal(); | ||
trace('trade', 'give', give, 'want', want.Items.value); | ||
/** a seat for allocating proceeds of sales */ | ||
const proceeds = zcf.makeEmptySeatKit().zcfSeat; | ||
|
||
AmountMath.isGTE(give.Price, tradePrice) || | ||
Fail`${q(give.Price)} below required ${q(tradePrice)}}`; | ||
/** @type {OfferHandler} */ | ||
const tradeHandler = buyerSeat => { | ||
// give and want are guaranteed by Zoe to match proposalShape | ||
const { want } = buyerSeat.getProposal(); | ||
|
||
bagValueSize(want.Items) <= 3n || Fail`only 3 items allowed in a trade`; | ||
sum(bagCounts(want.Items.value)) <= maxItems || | ||
Fail`max ${q(maxItems)} items allowed: ${q(want.Items)}`; | ||
|
||
// We use the deprecated stage/reallocate API | ||
// so that we can test this with the version of zoe on mainnet1B. | ||
// using atomicRearrange bloated the contract from ~1MB to ~3BM | ||
buyerSeat.decrementBy(proceeds.incrementBy(give)); | ||
const tmp = mint.mintGains(want); | ||
buyerSeat.incrementBy(tmp.decrementBy(want)); | ||
zcf.reallocate(buyerSeat, tmp, proceeds); | ||
const newItems = itemMint.mintGains(want); | ||
atomicRearrange( | ||
zcf, | ||
harden([ | ||
// price from buyer to proceeds | ||
[buyerSeat, proceeds, { Price: tradePrice }], | ||
// new items to buyer | ||
[newItems, buyerSeat, want], | ||
]), | ||
); | ||
|
||
buyerSeat.exit(true); | ||
newItems.exit(); | ||
return 'trade complete'; | ||
}; | ||
|
||
const publicFacet = Far('API', { | ||
makeTradeInvitation: () => | ||
zcf.makeInvitation(tradeHandler, 'trade', undefined, tradeShape), | ||
}); | ||
/** | ||
* Make an invitation to trade for items. | ||
* | ||
* Proposal Keywords used in offers using these invitations: | ||
* - give: `Price` | ||
* - want: `Items` | ||
*/ | ||
const makeTradeInvitation = () => | ||
zcf.makeInvitation(tradeHandler, 'buy items', undefined, proposalShape); | ||
|
||
// Mark the publicFacet Far, i.e. reachable from outside the contract | ||
const publicFacet = Far('Items Public Facet', { | ||
makeTradeInvitation, | ||
}); | ||
return harden({ publicFacet }); | ||
}; | ||
harden(start); |