diff --git a/main/guides/zoe/assets/trade-offer-safety-1.mmd b/main/guides/zoe/assets/trade-offer-safety-1.mmd index f0eb07c8b..d5af83605 100644 --- a/main/guides/zoe/assets/trade-offer-safety-1.mmd +++ b/main/guides/zoe/assets/trade-offer-safety-1.mmd @@ -10,9 +10,9 @@ sequenceDiagram end box skyblue Contract Instance - participant C as gameAssetContract + participant C as offer-up.contract end A-)Zoe: getPublicFacet(instance) A-)+Zoe: getTerms(instance) - Zoe--)-A: { issuers, brands, joinPrice } + Zoe--)-A: { issuers, brands, tradePrice } diff --git a/main/guides/zoe/assets/trade-offer-safety-1.svg b/main/guides/zoe/assets/trade-offer-safety-1.svg index 7b70d2a53..e08405e5c 100644 --- a/main/guides/zoe/assets/trade-offer-safety-1.svg +++ b/main/guides/zoe/assets/trade-offer-safety-1.svg @@ -1 +1 @@ -Contract InstancegameAssetContractZoegameAssetContractZoeAlicegetPublicFacet(instance)1getTerms(instance)2{ issuers, brands, joinPrice }3Alice \ No newline at end of file +Contract Instanceoffer-up.contractZoeoffer-up.contractZoeAlicegetPublicFacet(instance)1getTerms(instance)2{ issuers, brands, tradePrice }3Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-2.mmd b/main/guides/zoe/assets/trade-offer-safety-2.mmd index 51afaeff5..3c95ecf26 100644 --- a/main/guides/zoe/assets/trade-offer-safety-2.mmd +++ b/main/guides/zoe/assets/trade-offer-safety-2.mmd @@ -10,9 +10,9 @@ sequenceDiagram end box skyblue Contract - participant C as gameAssetContract + participant C as offer-up.contract end - A-)C: makeJoinInvitation() + A-)C: makeTradeInvitation() A-)Zoe: offer(toJoin, proposal, { Price }) - A-)Zoe: E(seat).getPayout('Places') + A-)Zoe: E(seat).getPayout('Items') diff --git a/main/guides/zoe/assets/trade-offer-safety-2.svg b/main/guides/zoe/assets/trade-offer-safety-2.svg index 4ab44c82f..4edeba19e 100644 --- a/main/guides/zoe/assets/trade-offer-safety-2.svg +++ b/main/guides/zoe/assets/trade-offer-safety-2.svg @@ -1 +1 @@ -ContractgameAssetContractZoegameAssetContractZoeAlicemakeJoinInvitation()1offer(toJoin, proposal, { Price })2E(seat).getPayout('Places')3Alice \ No newline at end of file +Contractoffer-up.contractZoeoffer-up.contractZoeAlicemakeTradeInvitation()1offer(toJoin, proposal, { Price })2E(seat).getPayout('Items')3Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-3.mmd b/main/guides/zoe/assets/trade-offer-safety-3.mmd index 25eb5b26a..5b215ef3f 100644 --- a/main/guides/zoe/assets/trade-offer-safety-3.mmd +++ b/main/guides/zoe/assets/trade-offer-safety-3.mmd @@ -10,12 +10,12 @@ sequenceDiagram end box skyblue Contract - participant C as gameAssetContract + participant C as offer-up.contract end - A-)C: makeJoinInvitation() + A-)C: makeTradeInvitation() activate C - C--)Zoe: makeInvitation(joinHandler, ...) + C--)Zoe: makeInvitation(tradeHandler, ...) deactivate C activate Zoe Zoe--)-C: invitation @@ -26,4 +26,4 @@ sequenceDiagram Zoe--)Zoe: escrow Price pmt - Zoe--)-C: joinHandler(gameSeat) + Zoe--)-C: tradeHandler(buyerSeat) diff --git a/main/guides/zoe/assets/trade-offer-safety-3.svg b/main/guides/zoe/assets/trade-offer-safety-3.svg index 8323004f0..6a6ef2f24 100644 --- a/main/guides/zoe/assets/trade-offer-safety-3.svg +++ b/main/guides/zoe/assets/trade-offer-safety-3.svg @@ -1 +1 @@ -ContractgameAssetContractZoegameAssetContractZoeAlicemakeJoinInvitation()1makeInvitation(joinHandler, ...)2invitation3Invitation toJoin4offer(toJoin, proposal, { Price })5escrow Price pmt6joinHandler(gameSeat)7Alice \ No newline at end of file +Contractoffer-up.contractZoeoffer-up.contractZoeAlicemakeTradeInvitation()1makeInvitation(tradeHandler, ...)2invitation3Invitation toJoin4offer(toJoin, proposal, { Price })5escrow Price pmt6tradeHandler(buyerSeat)7Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-4.mmd b/main/guides/zoe/assets/trade-offer-safety-4.mmd index e3a07e24e..7297bdc7f 100644 --- a/main/guides/zoe/assets/trade-offer-safety-4.mmd +++ b/main/guides/zoe/assets/trade-offer-safety-4.mmd @@ -10,10 +10,10 @@ sequenceDiagram end box skyblue Contract - participant C as gameAssetContract + participant C as offer-up.contract end - Zoe--)C: joinHandler(gameSeat) + Zoe--)C: tradeHandler(buyerSeat) activate C C--)C: check proposal C--)Zoe: mintGains(want) @@ -25,4 +25,4 @@ sequenceDiagram Zoe--)Zoe: check offer safety - C--)Zoe: playerSeat.exit(true) \ No newline at end of file + C--)Zoe: buyerSeat.exit(true) \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-4.svg b/main/guides/zoe/assets/trade-offer-safety-4.svg index eb0b82e51..91e7d0bfb 100644 --- a/main/guides/zoe/assets/trade-offer-safety-4.svg +++ b/main/guides/zoe/assets/trade-offer-safety-4.svg @@ -1 +1 @@ -ContractgameAssetContractZoegameAssetContractZoeAlicejoinHandler(gameSeat)1check proposal2mintGains(want)3tmp seat4atomicRearrange(...)5check offer safety6playerSeat.exit(true)7Alice \ No newline at end of file +Contractoffer-up.contractZoeoffer-up.contractZoeAlicetradeHandler(buyerSeat)1check proposal2mintGains(want)3tmp seat4atomicRearrange(...)5check offer safety6buyerSeat.exit(true)7Alice \ No newline at end of file diff --git a/main/guides/zoe/assets/trade-offer-safety-5.mmd b/main/guides/zoe/assets/trade-offer-safety-5.mmd index f8fc74ca8..a814c8319 100644 --- a/main/guides/zoe/assets/trade-offer-safety-5.mmd +++ b/main/guides/zoe/assets/trade-offer-safety-5.mmd @@ -10,11 +10,11 @@ sequenceDiagram end box skyblue Contract - participant C as gameAssetContract + participant C as offer-up.contract end - A-)+Zoe: E(seat).getPayout('Places') + A-)+Zoe: E(seat).getPayout('Items') Note over Zoe: ... many steps above ... - Zoe--)-A: placesPayment - A-)+Zoe: E(issuers.Place).getAmountOf(placesPayment) - Zoe--)-A: { brand: Place brand,
value: [['Park Place, 1n], ['Boardwalk', 1n]] + Zoe--)-A: itemsPayment + A-)+Zoe: E(issuers.Items).getAmountOf(itemsPayment) + Zoe--)-A: { brand: Item brand,
value: [['map', 1n], ['scroll', 1n]] diff --git a/main/guides/zoe/assets/trade-offer-safety-5.svg b/main/guides/zoe/assets/trade-offer-safety-5.svg index 70b42dfe6..468fc5a6b 100644 --- a/main/guides/zoe/assets/trade-offer-safety-5.svg +++ b/main/guides/zoe/assets/trade-offer-safety-5.svg @@ -1 +1 @@ -ContractgameAssetContractZoegameAssetContractZoe... many steps above ...AliceE(seat).getPayout('Places')1placesPayment2E(issuers.Place).getAmountOf(placesPayment)3{ brand: Place brand, value: [['Park Place, 1n], ['Boardwalk', 1n]]4Alice \ No newline at end of file +Contractoffer-up.contractZoeoffer-up.contractZoe... many steps above ...AliceE(seat).getPayout('Items')1itemsPayment2E(issuers.Items).getAmountOf(itemsPayment)3{ brand: Item brand, value: [['map', 1n], ['scroll', 1n]]4Alice \ No newline at end of file diff --git a/main/guides/zoe/contract-upgrade.md b/main/guides/zoe/contract-upgrade.md index bb2cd68e0..a3e1b5bc9 100644 --- a/main/guides/zoe/contract-upgrade.md +++ b/main/guides/zoe/contract-upgrade.md @@ -9,7 +9,7 @@ The `adminFacet` is defined by Zoe and includes methods to upgrade the contract. Governance of the right to upgrade is a complex topic that we cover only briefly here. - When [BLD staker governance](https://community.agoric.com/t/about-the-governance-category/15) makes a decision to start a contract using [swingset.CoreEval](../coreeval/), - to date, the `adminFacet` is stored in the bootstrap vat, allowing + to date, the `adminFacet` is stored in the bootstrap [vat](/glossary/#vat), allowing the BLD stakers to upgrade such a contract in a later `swingset.CoreEval`. - The `adminFacet` reference can be discarded, so that noone can upgrade the contract from within the JavaScript VM. (BLD staker governace diff --git a/main/guides/zoe/contract-walkthru.md b/main/guides/zoe/contract-walkthru.md index 1cda82ec2..9eaa4c6df 100644 --- a/main/guides/zoe/contract-walkthru.md +++ b/main/guides/zoe/contract-walkthru.md @@ -96,9 +96,9 @@ t.is(typeof installation, 'object'); The `installation` identifies the basic contract that we'll go over in detail in the sections below. -::: details gameAssetContract.js listing +::: details offer-up.contract.js listing -<<< @/../snippets/zoe/src/gameAssetContract.js#file +<<< @/../snippets/zoe/src/offer-up.contract.js#file ::: @@ -113,7 +113,7 @@ yarn ava test/test-contract.js -m 'Start the contract' ``` ✔ Start the contract (652ms) ℹ terms: { - joinPrice: { + tradePrice: { brand: Object @Alleged: PlayMoney brand {}, value: 5n, }, @@ -130,7 +130,7 @@ the contract should use for its business: ```js{8} const money = makeIssuerKit('PlayMoney'); const issuers = { Price: money.issuer }; -const terms = { joinPrice: AmountMath.make(money.brand, 5n) }; +const terms = { tradePrice: AmountMath.make(money.brand, 5n) }; t.log('terms:', terms); /** @type {ERef>} */ @@ -146,14 +146,14 @@ _See also [E(zoe).startInstance(...)](/reference/zoe-api/zoe.md#e-zoe-startinsta Let's take a look at what happens in the contract when it starts. A _facet_ of Zoe, the _Zoe Contract Facet_, is passed to the contract `start` function. The contract uses this `zcf` to get its terms. Likewise it uses `zcf` to -make a `gameSeat` where it can store assets that it receives in trade -as well as a `mint` for making assets consisting of collections (bags) of Places: +make a `proceeds` seat where it can store assets that it receives in trade +as well as a `mint` for making assets consisting of collections (bags) of Items: -<<< @/../snippets/zoe/src/gameAssetContract.js#start +<<< @/../snippets/zoe/src/offer-up.contract.js#start -It defines a `joinShape` and `joinHandler` but doesn't do anything with them yet. They will come into play later. It defines and returns its `publicFacet` and stands by. +It defines a `proposalShape` and `tradeHandler` but doesn't do anything with them yet. They will come into play later. It defines and returns a [hardened](/glossary/#harden) `publicFacet` object and stands by. -<<< @/../snippets/zoe/src/gameAssetContract.js#started +<<< @/../snippets/zoe/src/offer-up.contract.js#started ## Trading with Offer Safety @@ -164,7 +164,7 @@ yarn ava test/test-contract.js -m 'Alice trades*' ``` ``` - ✔ Alice trades: give some play money, want some game places (674ms) + ✔ Alice trades: give some play money, want items (309ms) ℹ Object @Alleged: InstanceHandle {} ℹ Alice gives { Price: { @@ -172,15 +172,15 @@ yarn ava test/test-contract.js -m 'Alice trades*' value: 5n, }, } - ℹ Alice payout brand Object @Alleged: Place brand {} + ℹ Alice payout brand Object @Alleged: Item brand {} ℹ Alice payout value Object @copyBag { payload: [ [ - 'Park Place', + 'scroll', 1n, ], [ - 'Boardwalk', + 'map', 1n, ], ], @@ -209,14 +209,14 @@ Alice starts by using the `instance` to get the contract's `publicFacet` and `te <<< @/../snippets/zoe/contracts/alice-trade.js#queryInstance -Then she constructs a _proposal_ to give the `joinPrice` in exchange -for 1 Park Place and 1 Boardwalk, denominated in the game's `Place` brand; and she withdraws a payment from her purse: +Then she constructs a _proposal_ to give the `tradePrice` in exchange +for 1 map and 1 scroll, denominated in the game's `Item` brand; and she withdraws a payment from her purse: <<< @/../snippets/zoe/contracts/alice-trade.js#makeProposal She then requests an _invitation_ to join the game; makes an _offer_ with (a promise for) this invitation, her proposal, and her payment; -and awaits her **Places** payout: +and awaits her **Items** payout: @@ -241,33 +241,33 @@ when you are [creating an instance](#starting-a-contract-instance) or by using ::: -The contract gets Alice's `E(publicFacet).makeJoinInvitation()` call and uses `zcf` to make an invitation with an associated handler, description, and proposal shape. Zoe gets Alice's `E(zoe).offer(...)` call, checks the proposal against the proposal shape, escrows the payment, and invokes the handler. +The contract gets Alice's `E(publicFacet).makeTradeInvitation()` call and uses `zcf` to make an invitation with an associated handler, description, and proposal shape. Zoe gets Alice's `E(zoe).offer(...)` call, checks the proposal against the proposal shape, escrows the payment, and invokes the handler. -<<< @/../snippets/zoe/src/gameAssetContract.js#makeInvitation +<<< @/../snippets/zoe/src/offer-up.contract.js#makeInvitation The offer handler is invoked with a _seat_ representing the party making the offer. It extracts the `give` and `want` from the party's offer and checks that -they are giving at least the `joinPrice` and not asking for too many -places in return. +they are giving at least the `tradePrice` and not asking for too many +items in return. With all these prerequisites met, the handler instructs `zcf` to mint the requested -**Place** assets, allocate what the player is giving into its own `gameSeat`, -and allocate the minted places to the player. Finally, it concludes its business with the player. +**Item** assets, allocate what the player is giving into its own `proceeds` seat, +and allocate the minted items to the player. Finally, it concludes its business with the player. -<<< @/../snippets/zoe/src/gameAssetContract.js#handler +<<< @/../snippets/zoe/src/offer-up.contract.js#handler Zoe checks that the contract's instructions are consistent with the offer and with conservation of assets. Then it allocates -the escrowed payment to the contract's gameSeat and pays out +the escrowed payment to the contract's proceeds seat and pays out the place NFTs to Alice in response to the earlier `getPayout(...)` call. -Alice asks the `Place` issuer what her payout is worth +Alice asks the `Item` issuer what her payout is worth and tests that it's what she wanted. { // #region queryInstance const publicFacet = E(zoe).getPublicFacet(instance); const terms = await E(zoe).getTerms(instance); - const { issuers, brands, joinPrice } = terms; + const { issuers, brands, tradePrice } = terms; // #endregion queryInstance // #region makeProposal - const choices = ['Park Place', 'Boardwalk']; + const choices = ['map', 'scroll']; const choiceBag = makeCopyBag(choices.map(name => [name, 1n])); const proposal = { - give: { Price: joinPrice }, - want: { Places: AmountMath.make(brands.Place, choiceBag) }, + give: { Price: tradePrice }, + want: { Places: AmountMath.make(brands.Item, choiceBag) }, }; - const Price = await E(purse).withdraw(joinPrice); + const Price = await E(purse).withdraw(tradePrice); t.log('Alice gives', proposal.give); // #endregion makeProposal // #region trade - const toJoin = E(publicFacet).makeJoinInvitation(); + const toJoin = E(publicFacet).makeTradeInvitation(); const seat = E(zoe).offer(toJoin, proposal, { Price }); - const places = await E(seat).getPayout('Places'); + const items = await E(seat).getPayout('Items'); // #endregion trade // #region payouts - const actual = await E(issuers.Place).getAmountOf(places); + const actual = await E(issuers.Item).getAmountOf(items); t.log('Alice payout brand', actual.brand); t.log('Alice payout value', actual.value); - t.deepEqual(actual, proposal.want.Places); + t.deepEqual(actual, proposal.want.Items); // #endregion payouts }; diff --git a/snippets/zoe/contracts/test-bundle-source.js b/snippets/zoe/contracts/test-bundle-source.js index ae7258ee9..53e93318e 100644 --- a/snippets/zoe/contracts/test-bundle-source.js +++ b/snippets/zoe/contracts/test-bundle-source.js @@ -17,7 +17,7 @@ import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; // #region contractPath const myRequire = createRequire(import.meta.url); -const contractPath = myRequire.resolve(`../src/gameAssetContract.js`); +const contractPath = myRequire.resolve(`../src/offer-up.contract.js`); // #endregion contractPath test('bundleSource() bundles the contract for use with zoe', async t => { diff --git a/snippets/zoe/src/gameAssetContract.js b/snippets/zoe/src/gameAssetContract.js deleted file mode 100644 index 80ad5b174..000000000 --- a/snippets/zoe/src/gameAssetContract.js +++ /dev/null @@ -1,80 +0,0 @@ -// #region file -/** @file Contract to mint and sell Place NFTs for a hypothetical game. */ -// @ts-check - -import { Far } from '@endo/far'; -import { M, getCopyBagEntries } from '@endo/patterns'; -import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; -import { AmountShape } from '@agoric/ertp/src/typeGuards.js'; -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('Game', true); - -/** @param {Amount<'copyBag'>} amt */ -const bagValueSize = amt => { - /** @type {[unknown, bigint][]} */ - const entries = getCopyBagEntries(amt.value); // XXX getCopyBagEntries returns any??? - const total = entries.reduce((acc, [_place, qty]) => acc + qty, 0n); - return total; -}; - -/** - * @param {ZCF<{joinPrice: Amount}>} zcf - */ -// #region start -export const start = async zcf => { - const { joinPrice } = zcf.getTerms(); - - const { zcfSeat: gameSeat } = zcf.makeEmptySeatKit(); - const mint = await zcf.makeZCFMint('Place', AssetKind.COPY_BAG); - // #endregion start - - // #region handler - /** @param {ZCFSeat} playerSeat */ - const joinHandler = playerSeat => { - const { give, want } = playerSeat.getProposal(); - trace('join', 'give', give, 'want', want.Places.value); - - AmountMath.isGTE(give.Price, joinPrice) || - Fail`${q(give.Price)} below joinPrice of ${q(joinPrice)}}`; - - bagValueSize(want.Places) <= 3n || Fail`only 3 places allowed when joining`; - - const tmp = mint.mintGains(want); - atomicRearrange( - zcf, - harden([ - [playerSeat, gameSeat, give], - [tmp, playerSeat, want], - ]), - ); - - playerSeat.exit(true); - return 'welcome to the game'; - }; - // #endregion handler - - // #region makeInvitation - const joinShape = harden({ - give: { Price: AmountShape }, - want: { Places: AmountShape }, - exit: M.any(), - }); - - const publicFacet = Far('API', { - makeJoinInvitation: () => - zcf.makeInvitation(joinHandler, 'join', undefined, joinShape), - }); - // #endregion makeInvitation - - // #region started - return { publicFacet }; - // #endregion started -}; -harden(start); -// #endregion file diff --git a/snippets/zoe/src/offer-up.contract.js b/snippets/zoe/src/offer-up.contract.js new file mode 100644 index 000000000..9462e92ff --- /dev/null +++ b/snippets/zoe/src/offer-up.contract.js @@ -0,0 +1,97 @@ +// #region file +/** @file Contract to mint and sell a few Item NFTs at a time. */ +// @ts-check + +import { Far } from '@endo/far'; +import { M, getCopyBagEntries } from '@endo/patterns'; +import { AssetKind } from '@agoric/ertp/src/amountMath.js'; +import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; +import '@agoric/zoe/exported.js'; + +const { Fail, quote: q } = assert; + +// #region bagUtils +/** @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 bagUtils + +/** + * 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 + */ + +// #region start +/** @param {ZCF} zcf */ +export const start = async zcf => { + const { tradePrice, maxItems = 3n } = zcf.getTerms(); + + const itemMint = await zcf.makeZCFMint('Item', AssetKind.COPY_BAG); + // #endregion start + const { brand: itemBrand } = itemMint.getIssuerRecord(); + + /** a seat for allocating proceeds of sales */ + const proceeds = zcf.makeEmptySeatKit().zcfSeat; + // #region handler + /** @type {OfferHandler} */ + const tradeHandler = buyerSeat => { + // give and want are guaranteed by Zoe to match proposalShape + const { want } = buyerSeat.getProposal(); + + sum(bagCounts(want.Items.value)) <= maxItems || + Fail`max ${q(maxItems)} items allowed: ${q(want.Items)}`; + + 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'; + }; + // #endregion handler + + // #region makeInvitation + const proposalShape = harden({ + give: { Price: M.gte(tradePrice) }, + want: { Items: { brand: itemBrand, value: M.bag() } }, + exit: M.any(), + }); + + 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, + }); + // #endregion makeInvitation + + // #region started + return harden({ publicFacet }); + // #endregion started +}; + +harden(start); +// #endregion file