diff --git a/contract/.gitignore b/contract/.gitignore index 32339d8f..dade5bf3 100644 --- a/contract/.gitignore +++ b/contract/.gitignore @@ -1,3 +1,3 @@ -start-agoric-basics-permit.json -start-agoric-basics.js +start-sell-concert-tickets-permit.json +start-sell-concert-tickets.js bundles/ diff --git a/contract/Makefile b/contract/Makefile index e0601c11..1fa4f265 100644 --- a/contract/Makefile +++ b/contract/Makefile @@ -103,8 +103,8 @@ print-key: /root/.agoric/user1.key @agd keys show user1 -a --keyring-backend="test" @echo -SCRIPT=start-agoric-basics.js -PERMIT=start-agoric-basics-permit.json +SCRIPT=start-sell-concert-tickets.js +PERMIT=start-sell-concert-tickets-permit.json start-contract: $(SCRIPT) $(PERMIT) install-bundles scripts/propose-start-contract.sh diff --git a/contract/package.json b/contract/package.json index 410fb9de..ed97ef41 100644 --- a/contract/package.json +++ b/contract/package.json @@ -1,5 +1,5 @@ { - "name": "dapp-agoric-basics-contract", + "name": "agoric-basics-contract", "version": "0.1.0", "private": true, "description": "Agoric Basics Contract", diff --git a/contract/scripts/build-contract-deployer.js b/contract/scripts/build-contract-deployer.js index 305110bf..66107c57 100644 --- a/contract/scripts/build-contract-deployer.js +++ b/contract/scripts/build-contract-deployer.js @@ -4,7 +4,7 @@ * Creates files for starting an instance of the contract: * * contract source and instantiation proposal bundles to be published via * `agd tx swingset install-bundle` - * * start-agoric-basics-permit.json and start-agoric-basics.js to submit the + * * start-sell-concert-tickets-permit.json and start-sell-concert-tickets.js to submit the * instantiation proposal via `agd tx gov submit-proposal swingset-core-eval` * * Usage: @@ -12,19 +12,22 @@ */ import { makeHelpers } from '@agoric/deploy-script-support'; -import { getManifestForAgoricBasics } from '../src/agoric-basics-proposal.js'; +import { getManifestForSellConcertTickets } from '../src/sell-concert-tickets-proposal.js'; /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ -export const agoricBasicsProposalBuilder = async ({ publishRef, install }) => { +export const sellConcertTicketsProposalBuilder = async ({ + publishRef, + install, +}) => { return harden({ - sourceSpec: '../src/agoric-basics-proposal.js', + sourceSpec: '../src/sell-concert-tickets-proposal.js', getManifestCall: [ - getManifestForAgoricBasics.name, + getManifestForSellConcertTickets.name, { - agoricBasicsRef: publishRef( + sellConcertTicketsRef: publishRef( install( - '../src/agoric-basics.contract.js', - '../bundles/bundle-agoric-basics.js', + '../src/sell-concert-tickets.contract.js', + '../bundles/bundle-sell-concert-tickets.js', { persist: true, }, @@ -38,5 +41,5 @@ export const agoricBasicsProposalBuilder = async ({ publishRef, install }) => { /** @type {DeployScriptFunction} */ export default async (homeP, endowments) => { const { writeCoreProposal } = await makeHelpers(homeP, endowments); - await writeCoreProposal('start-agoric-basics', agoricBasicsProposalBuilder); + await writeCoreProposal('start-sell-concert-tickets', sellConcertTicketsProposalBuilder); }; diff --git a/contract/scripts/propose-start-contract.sh b/contract/scripts/propose-start-contract.sh index 8960ec65..55b9efa0 100755 --- a/contract/scripts/propose-start-contract.sh +++ b/contract/scripts/propose-start-contract.sh @@ -3,8 +3,8 @@ set -xueo pipefail cd /workspace/contract -SCRIPT=start-agoric-basics.js -PERMIT=start-agoric-basics-permit.json +SCRIPT=start-sell-concert-tickets.js +PERMIT=start-sell-concert-tickets-permit.json ls -sh "$SCRIPT" "$PERMIT" PROPOSAL=$(agd query gov proposals --output json | jq -c '.proposals | length | .+1') @@ -12,7 +12,7 @@ PROPOSAL=$(agd query gov proposals --output json | jq -c '.proposals | length | make fund-acct agd tx gov submit-proposal swingset-core-eval "$PERMIT" "$SCRIPT" \ - --title="Start Agoric Basics Contract" --description="Evaluate $SCRIPT" \ + --title="Start Sell Concert Tickets Contract" --description="Evaluate $SCRIPT" \ --deposit=10000000ubld --gas=auto --gas-adjustment=1.2 \ --from user1 --chain-id agoriclocal --keyring-backend=test \ --yes -b block diff --git a/contract/src/agoric-basics.contract.js b/contract/src/agoric-basics.contract.js deleted file mode 100644 index 32708e53..00000000 --- a/contract/src/agoric-basics.contract.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @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 { 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'; - -const { Fail, quote: q } = assert; - -// #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; - * }} AgoricBasicsTerms - */ - -export const meta = { - customTermsShape: M.splitRecord( - { tradePrice: AmountShape }, - { maxItems: M.bigint() }, - ), -}; -// compatibility with an earlier contract metadata API -export const customTermsShape = meta.customTermsShape; - -/** - * 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} zcf - */ -export const start = async zcf => { - const { tradePrice, maxItems = 3n } = zcf.getTerms(); - - /** - * 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(); - - /** - * 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(), - }); - - /** a seat for allocating proceeds of sales */ - const proceeds = zcf.makeEmptySeatKit().zcfSeat; - - /** @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'; - }; - - /** - * 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); diff --git a/contract/src/agoric-basics-proposal.js b/contract/src/sell-concert-tickets-proposal.js similarity index 59% rename from contract/src/agoric-basics-proposal.js rename to contract/src/sell-concert-tickets-proposal.js index ef6b7a1b..5ed25ea1 100644 --- a/contract/src/agoric-basics-proposal.js +++ b/contract/src/sell-concert-tickets-proposal.js @@ -13,7 +13,29 @@ const BOARD_AUX = 'boardAux'; const marshalData = makeMarshal(_val => Fail`data only`); const IST_UNIT = 1_000_000n; -const CENT = IST_UNIT / 100n; + +export const makeInventory = (brand, baseUnit) => { + return { + frontRow: { + tradePrice: AmountMath.make(brand, baseUnit * 3n), + maxTickets: 3n, + }, + middleRow: { + tradePrice: AmountMath.make(brand, baseUnit * 2n), + maxTickets: 3n, + }, + lastRow: { + tradePrice: AmountMath.make(brand, baseUnit * 1n), + maxTickets: 3n, + }, + }; +}; + +export const makeTerms = (brand, baseUnit) => { + return { + inventory: makeInventory(brand, baseUnit), + }; +}; /** * Make a storage node for auxilliary data for a value on the board. @@ -41,47 +63,47 @@ const publishBrandInfo = async (chainStorage, board, brand) => { * * @param {BootstrapPowers} permittedPowers */ -export const startAgoricBasicsContract = async permittedPowers => { - console.error('startAgoricBasicsContract()...'); +export const startSellConcertTicketsContract = async permittedPowers => { + console.error('startSellConcertTicketsContract()...'); const { consume: { board, chainStorage, startUpgradable, zoe }, brand: { consume: { IST: istBrandP }, // @ts-expect-error dynamic extension to promise space - produce: { Item: produceItemBrand }, + produce: { Ticket: produceTicketBrand }, }, issuer: { consume: { IST: istIssuerP }, // @ts-expect-error dynamic extension to promise space - produce: { Item: produceItemIssuer }, + produce: { Ticket: produceTicketIssuer }, }, installation: { - consume: { agoricBasics: agoricBasicsInstallationP }, + consume: { sellConcertTickets: sellConcertTicketsInstallationP }, }, instance: { // @ts-expect-error dynamic extension to promise space - produce: { agoricBasics: produceInstance }, + produce: { sellConcertTickets: produceInstance }, }, } = permittedPowers; const istIssuer = await istIssuerP; const istBrand = await istBrandP; - const terms = { tradePrice: AmountMath.make(istBrand, 25n * CENT) }; + const terms = makeTerms(istBrand, 1n * IST_UNIT); // agoricNames gets updated each time; the promise space only once XXXXXXX - const installation = await agoricBasicsInstallationP; + const installation = await sellConcertTicketsInstallationP; const { instance } = await E(startUpgradable)({ installation, issuerKeywordRecord: { Price: istIssuer }, - label: 'agoricBasics', + label: 'sellConcertTickets', terms, }); console.log('CoreEval script: started contract', instance); const { - brands: { Item: brand }, - issuers: { Item: issuer }, + brands: { Ticket: brand }, + issuers: { Ticket: issuer }, } = await E(zoe).getTerms(instance); console.log('CoreEval script: share via agoricNames:', brand); @@ -89,18 +111,18 @@ export const startAgoricBasicsContract = async permittedPowers => { produceInstance.reset(); produceInstance.resolve(instance); - produceItemBrand.reset(); - produceItemIssuer.reset(); - produceItemBrand.resolve(brand); - produceItemIssuer.resolve(issuer); + produceTicketBrand.reset(); + produceTicketIssuer.reset(); + produceTicketBrand.resolve(brand); + produceTicketIssuer.resolve(issuer); await publishBrandInfo(chainStorage, board, brand); - console.log('agoricBasics (re)started'); + console.log('sellConcertTickets (re)started'); }; /** @type { import("@agoric/vats/src/core/lib-boot").BootstrapManifest } */ -const agoricBasicsManifest = { - [startAgoricBasicsContract.name]: { +const sellConcertTicketsManifest = { + [startSellConcertTicketsContract.name]: { consume: { agoricNames: true, board: true, // to publish boardAux info for NFT brand @@ -108,19 +130,22 @@ const agoricBasicsManifest = { startUpgradable: true, // to start contract and save adminFacet zoe: true, // to get contract terms, including issuer/brand }, - installation: { consume: { agoricBasics: true } }, - issuer: { consume: { IST: true }, produce: { Item: true } }, - brand: { consume: { IST: true }, produce: { Item: true } }, - instance: { produce: { agoricBasics: true } }, + installation: { consume: { sellConcertTickets: true } }, + issuer: { consume: { IST: true }, produce: { Ticket: true } }, + brand: { consume: { IST: true }, produce: { Ticket: true } }, + instance: { produce: { sellConcertTickets: true } }, }, }; -harden(agoricBasicsManifest); +harden(sellConcertTicketsManifest); -export const getManifestForAgoricBasics = ({ restoreRef }, { agoricBasicsRef }) => { +export const getManifestForSellConcertTickets = ( + { restoreRef }, + { sellConcertTicketsRef }, +) => { return harden({ - manifest: agoricBasicsManifest, + manifest: sellConcertTicketsManifest, installations: { - agoricBasics: restoreRef(agoricBasicsRef), + sellConcertTickets: restoreRef(sellConcertTicketsRef), }, }); }; diff --git a/contract/src/sell-concert-tickets.contract.js b/contract/src/sell-concert-tickets.contract.js new file mode 100644 index 00000000..7ee86439 --- /dev/null +++ b/contract/src/sell-concert-tickets.contract.js @@ -0,0 +1,217 @@ +/** + * @file Contract to mint and sell a few ticket 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, makeCopyBag } 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'; +import { AmountMath, AmountShape } from '@agoric/ertp'; + +const { Fail, quote: q } = assert; + +// #region bag utilities +/** + * + * @param {Amount} amount + * @param {number} n + * @returns {Amount} + */ +const multiply = (amount, n) => { + const arr = Array.from({ length: n }); + return arr.reduce( + (sum, _) => AmountMath.add(amount, sum), + AmountMath.make(amount.brand, 0n), + ); +}; + +/** + * + * @param {Amount} sum + * @param {[string, bigint]} entry + * @param {Inventory} inventory + * @returns {Amount} + */ +const addMultiples = (sum, entry, inventory) => { + const multiple = multiply(inventory[entry[0]].tradePrice, Number(entry[1])); + return AmountMath.add(multiple, sum); +}; + +/** + * + * @param {import('@endo/patterns').CopyBag} bag + * @param {Inventory} inventory + * @returns {Amount} + */ +export const bagPrice = (bag, inventory) => { + /** @type {[string, bigint][]} */ + const entries = getCopyBagEntries(bag); + const values = Object.values(inventory); + const brand = values[0].tradePrice.brand; + return entries.reduce( + (sum, entry) => addMultiples(sum, entry, inventory), + AmountMath.makeEmpty(brand), + ); +}; +// #endregion + +/** + * Inventory contains price and maximum for each type of tickets + * @example + * { + * frontRow: { + * tradePrice: AmountMath.make(istBrand, 3n), + * maxTickets: 3n, + * }, + * } + * @typedef {{[key: string]: {tradePrice: Amount, maxTickets: NatValue}}} Inventory + */ +const InventoryShape = M.recordOf(M.string(), { + tradePrice: AmountShape, + maxTickets: M.nat(), +}); + +/** + * In addition to the standard `issuers` and `brands` terms, + * this contract is parameterized by terms for inventory + * + * @typedef {{ + * inventory: Inventory + * }} SellConcertTicketsTerms + */ + +export const meta = harden({ + customTermsShape: { inventory: InventoryShape }, +}); +// compatibility with an earlier contract metadata API +export const customTermsShape = meta.customTermsShape; + +/** + * Start a contract that + * - creates a new semi-fungible asset type for Tickets, and + * - handles offers to buy as many tickets as inventory allows + * + * @param {ZCF} zcf + */ +export const start = async zcf => { + const { inventory } = zcf.getTerms(); + + const inventoryValues = Object.values(inventory); + + // make sure inventory is not empty + inventoryValues.length > 0 || Fail`inventory must not be empty`; + // make sure all kinds of tickets have the same brand for tradePrice + inventoryValues.every( + v => v.tradePrice.brand === inventoryValues[0].tradePrice.brand, + ) || Fail`inventory must have the same brand for all tickets' tradePrice`; + + /** + * a new ERTP mint for tickets, 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 frontRow tickets and 1 middleRow ticket. + */ + const ticketMint = await zcf.makeZCFMint('Ticket', AssetKind.COPY_BAG); + const { brand: ticketBrand } = ticketMint.getIssuerRecord(); + + const inventoryBag = makeCopyBag( + Object.entries(inventory).map(([ticket, { maxTickets }], _i) => [ + ticket, + maxTickets, + ]), + ); + const toMint = { + Tickets: { + brand: ticketBrand, + value: inventoryBag, + }, + }; + /** + * Mint the whole inventory at contract start time, this also allows us to + * check if user is buying more than our inventory allows using + * `AmountMath.GTE` function + */ + const inventorySeat = ticketMint.mintGains(toMint); + + /** + * a pattern to constrain proposals given to {@link tradeHandler} + * + * The `Price` amount must be AmountShape. + * The `Tickets` amount must use the `Ticket` brand and a bag value. + */ + const proposalShape = harden({ + give: { Price: AmountShape }, + want: { Tickets: { brand: ticketBrand, value: M.bag() } }, + exit: M.any(), + }); + + /** a seat for allocating proceeds of sales */ + const proceeds = zcf.makeEmptySeatKit().zcfSeat; + + /** @type {OfferHandler} */ + const tradeHandler = buyerSeat => { + // give and want are guaranteed by Zoe to match proposalShape + const { give, want } = buyerSeat.getProposal(); + + // check that we have enough inventory + AmountMath.isGTE( + inventorySeat.getCurrentAllocation().Tickets, + want.Tickets, + ) || Fail`Not enough inventory, ${q(want.Tickets)} wanted`; + + // check that user is paying enough for all the tickets + const totalPrice = bagPrice(want.Tickets.value, inventory); + AmountMath.isGTE(give.Price, totalPrice) || + Fail`Total price is ${q(totalPrice)}, but ${q(give.Price)} was given`; + + atomicRearrange( + zcf, + harden([ + // price from buyer to proceeds + [buyerSeat, proceeds, { Price: totalPrice }], + // tickets from inventory to buyer + [inventorySeat, buyerSeat, want], + ]), + ); + + buyerSeat.exit(true); + return 'trade complete'; + }; + + /** + * Make an invitation to trade for tickets. + * + * Proposal Keywords used in offers using these invitations: + * - give: `Price` + * - want: `Tickets` + */ + const makeTradeInvitation = () => + zcf.makeInvitation(tradeHandler, 'buy tickets', undefined, proposalShape); + + // Mark the publicFacet Far, i.e. reachable from outside the contract + const publicFacet = Far('Tickets Public Facet', { + makeTradeInvitation, + }); + return harden({ publicFacet }); +}; +harden(start); diff --git a/contract/test/test-bundle-source.js b/contract/test/test-bundle-source.js index 3d80f2ac..29e2f3f4 100644 --- a/contract/test/test-bundle-source.js +++ b/contract/test/test-bundle-source.js @@ -12,7 +12,7 @@ import { E, passStyleOf } from '@endo/far'; import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; const myRequire = createRequire(import.meta.url); -const contractPath = myRequire.resolve(`../src/agoric-basics.contract.js`); +const contractPath = myRequire.resolve(`../src/sell-concert-tickets.contract.js`); test('bundleSource() bundles the contract for use with zoe', async t => { const bundle = await bundleSource(contractPath); diff --git a/contract/test/test-contract.js b/contract/test/test-sell-concert-tickets-contract.js similarity index 66% rename from contract/test/test-contract.js rename to contract/test/test-sell-concert-tickets-contract.js index 9ffc3cd3..e7a984a0 100644 --- a/contract/test/test-contract.js +++ b/contract/test/test-sell-concert-tickets-contract.js @@ -1,5 +1,5 @@ /** - * @file Test basic trading using the agoric basics contract. + * @file Test basic trading using the sell concert tickets contract. */ // @ts-check @@ -15,12 +15,19 @@ import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { makeStableFaucet } from './mintStable.js'; -import { startAgoricBasicsContract } from '../src/agoric-basics-proposal.js'; +import { + startSellConcertTicketsContract, + makeInventory, + makeTerms, +} from '../src/sell-concert-tickets-proposal.js'; +import { bagPrice } from '../src/sell-concert-tickets.contract.js'; -/** @typedef {typeof import('../src/agoric-basics.contract.js').start} AssetContractFn */ +/** @typedef {typeof import('../src/sell-concert-tickets.contract.js').start} AssetContractFn */ const myRequire = createRequire(import.meta.url); -const contractPath = myRequire.resolve(`../src/agoric-basics.contract.js`); +const contractPath = myRequire.resolve( + `../src/sell-concert-tickets.contract.js`, +); /** @type {import('ava').TestFn>>} */ const test = anyTest; @@ -47,6 +54,22 @@ const makeTestContext = async _t => { test.before(async t => (t.context = await makeTestContext(t))); +test('bagPrice calculates the total price correctly', async t => { + const money = makeIssuerKit('PlayMoney'); + const inventory = makeInventory(money.brand, 1n); + const bag = makeCopyBag([ + ['frontRow', 3n], + ['middleRow', 2n], + ['lastRow', 1n], + ]); + t.true( + AmountMath.isEqual( + bagPrice(bag, inventory), + AmountMath.make(money.brand, 14n), + ), + ); +}); + // IDEA: use test.serial and pass work products // between tests using t.context. @@ -63,7 +86,7 @@ test('Start the contract', async t => { const money = makeIssuerKit('PlayMoney'); const issuers = { Price: money.issuer }; - const terms = { tradePrice: AmountMath.make(money.brand, 5n) }; + const terms = makeTerms(money.brand, 1n); t.log('terms:', terms); /** @type {ERef>} */ @@ -80,40 +103,62 @@ test('Start the contract', async t => { * @param {ZoeService} zoe * @param {ERef} instance * @param {Purse} purse - * @param {string[]} choices + * @param {[string, NatValue][]} choices + * @param {boolean} expectSuccessfulTrade */ -const alice = async (t, zoe, instance, purse, choices = ['map', 'scroll']) => { +const alice = async ( + t, + zoe, + instance, + purse, + choices = [ + ['frontRow', 1n], + ['middleRow', 1n], + ], + expectSuccessfulTrade = true, +) => { const publicFacet = E(zoe).getPublicFacet(instance); // @ts-expect-error Promise seems to work const terms = await E(zoe).getTerms(instance); - const { issuers, brands, tradePrice } = terms; + const { issuers, brands } = terms; - const choiceBag = makeCopyBag(choices.map(name => [name, 1n])); + const choiceBag = makeCopyBag(choices); + const totalPrice = bagPrice(choiceBag, terms.inventory); const proposal = { - give: { Price: tradePrice }, - want: { Items: AmountMath.make(brands.Item, choiceBag) }, + give: { Price: totalPrice }, + want: { Tickets: AmountMath.make(brands.Ticket, choiceBag) }, }; - const pmt = await E(purse).withdraw(tradePrice); + const pmt = await E(purse).withdraw(totalPrice); t.log('Alice gives', proposal.give); - // #endregion makeProposal const toTrade = E(publicFacet).makeTradeInvitation(); const seat = E(zoe).offer(toTrade, proposal, { Price: pmt }); - const items = await E(seat).getPayout('Items'); - - const actual = await E(issuers.Item).getAmountOf(items); + const resultP = E(seat).getOfferResult(); + await (expectSuccessfulTrade ? t.notThrowsAsync(resultP) : t.throwsAsync(resultP)); + if (!expectSuccessfulTrade) { + return; + } + const result = await resultP; + t.log('result', result); + + const tickets = await E(seat).getPayout('Tickets'); + const actual = await E(issuers.Ticket).getAmountOf(tickets); t.log('Alice payout brand', actual.brand); t.log('Alice payout value', actual.value); - t.deepEqual(actual, proposal.want.Items); + if (expectSuccessfulTrade) { + t.deepEqual(actual, proposal.want.Tickets); + } else { + t.deepEqual(actual, AmountMath.makeEmptyFromAmount(actual)); + } }; -test('Alice trades: give some play money, want items', async t => { +test('Alice trades: give some play money, want tickets', async t => { const { zoe, bundle } = t.context; const money = makeIssuerKit('PlayMoney'); const issuers = { Price: money.issuer }; - const terms = { tradePrice: AmountMath.make(money.brand, 5n) }; + const terms = makeTerms(money.brand, 1n); /** @type {ERef>} */ const installation = E(zoe).install(bundle); @@ -128,6 +173,26 @@ test('Alice trades: give some play money, want items', async t => { await alice(t, zoe, instance, alicePurse); }); +test('Alice trades: want too many tickets', async t => { + const { zoe, bundle } = t.context; + + const money = makeIssuerKit('PlayMoney'); + const issuers = { Price: money.issuer }; + const terms = makeTerms(money.brand, 1n); + + /** @type {ERef>} */ + const installation = E(zoe).install(bundle); + const { instance } = await E(zoe).startInstance(installation, issuers, terms); + t.log(instance); + t.is(typeof instance, 'object'); + + const alicePurse = money.issuer.makeEmptyPurse(); + const amountOfMoney = AmountMath.make(money.brand, 10n); + const moneyPayment = money.mint.mintPayment(amountOfMoney); + alicePurse.deposit(moneyPayment); + await alice(t, zoe, instance, alicePurse, [['lastRow', 4n]], false); +}); + test('Trade in IST rather than play money', async t => { /** * Start the contract, providing it with @@ -140,12 +205,8 @@ test('Trade in IST rather than play money', async t => { const installation = E(zoe).install(bundle); const feeIssuer = await E(zoe).getFeeIssuer(); const feeBrand = await E(feeIssuer).getBrand(); - const tradePrice = AmountMath.make(feeBrand, 25n * CENT); - return E(zoe).startInstance( - installation, - { Price: feeIssuer }, - { tradePrice }, - ); + const terms = makeTerms(feeBrand, 5n * CENT); + return E(zoe).startInstance(installation, { Price: feeIssuer }, terms); }; const { zoe, bundle, bundleCache, feeMintAccess } = t.context; @@ -197,14 +258,16 @@ test('use the code that will go on chain to start the contract', async t => { consume: { zoe, chainStorage, startUpgradable, board }, brand: { consume: { IST: pFor(feeBrand) }, - produce: { Item: sync.brand }, + produce: { Ticket: sync.brand }, }, issuer: { consume: { IST: pFor(feeIssuer) }, - produce: { Item: sync.issuer }, + produce: { Ticket: sync.issuer }, + }, + installation: { + consume: { sellConcertTickets: sync.installation.promise }, }, - installation: { consume: { agoricBasics: sync.installation.promise } }, - instance: { produce: { agoricBasics: sync.instance } }, + instance: { produce: { sellConcertTickets: sync.instance } }, }; return powers; }; @@ -219,7 +282,7 @@ test('use the code that will go on chain to start the contract', async t => { // When the BLD staker governance proposal passes, // the startup function gets called. - await startAgoricBasicsContract(powers); + await startSellConcertTicketsContract(powers); const instance = await sync.instance.promise; // Now that we have the instance, resume testing as above.