Skip to content

Commit

Permalink
Merge pull request #49 from Agoric/dc-contract-docs2
Browse files Browse the repository at this point in the history
fix: document contract; refactor for clarity; use defensive patterns
  • Loading branch information
dckc authored Jan 24, 2024
2 parents 7927d21 + d58a631 commit db4e168
Showing 1 changed file with 109 additions and 38 deletions.
147 changes: 109 additions & 38 deletions contract/src/offer-up.contract.js
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);

0 comments on commit db4e168

Please sign in to comment.