Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: contract code to mint tickets #8

Merged
merged 26 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d553e19
chore: replace all occurrences of item with ticket in contract code
LuqiPan Feb 8, 2024
11754dd
Merge branch 'main' into 939-mint-tickets
LuqiPan Feb 8, 2024
791417c
chore: remove customTermsShape
LuqiPan Feb 8, 2024
81fcca5
feat: alter the contract terms to contain ticket inventory
LuqiPan Feb 9, 2024
cc658e7
test: update most tests to use inventory as terms and make them pass
LuqiPan Feb 9, 2024
c101065
chore: fix problems from yarn lint
LuqiPan Feb 9, 2024
10388d6
test: make the last test in test-contract.js pass
LuqiPan Feb 9, 2024
f706103
Revert "test: make the last test in test-contract.js pass"
LuqiPan Feb 9, 2024
0332a6e
test: make the last test in test-contract.js pass
LuqiPan Feb 9, 2024
bd8bef9
chore: update JSDoc and make terms with more sense
LuqiPan Feb 9, 2024
d5d5482
feat: mint the whole inventory at contract start
LuqiPan Feb 12, 2024
2bc29bb
chore: fix lint
LuqiPan Feb 12, 2024
5d9e6f8
chore: remove hasInventory helper fuction as it's no longer needed
LuqiPan Feb 13, 2024
0cdb78c
chore: update comments in code files accordingly
LuqiPan Feb 13, 2024
fec0cad
chore: use makeInventory and makeTerms more widely
LuqiPan Feb 13, 2024
8774bd6
chore: use @example tag
LuqiPan Feb 13, 2024
216b293
chore: replace all occurrences of agoric-basics with sell-concert-tic…
LuqiPan Feb 15, 2024
02bde11
feat: check inventory is not empty and has the same brand at contract…
LuqiPan Feb 15, 2024
f828cad
chore: add back customTermsShape
LuqiPan Feb 15, 2024
c5c4b31
workaround: use M.any() to make it pass for now
LuqiPan Feb 16, 2024
4438977
test: add a test case for when Alice wants too many tickets
LuqiPan Feb 16, 2024
9a56f16
test: fix test by getting and handling offer result
LuqiPan Feb 20, 2024
727b350
test: improve the test to handle the error throwing flow better
LuqiPan Feb 21, 2024
1b8a012
chore: update file names for SCRIPT and PERMIT
LuqiPan Feb 21, 2024
5be8bbb
fix: tickets are of semi-fungible asset type
LuqiPan Feb 22, 2024
70a347c
chore(sell): add InventoryShape for use in customTermsShape
dckc Mar 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contract/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "dapp-agoric-basics-contract",
"name": "agoric-basics-contract",
"version": "0.1.0",
"private": true,
"description": "Agoric Basics Contract",
Expand Down
20 changes: 10 additions & 10 deletions contract/src/agoric-basics-proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ export const startAgoricBasicsContract = async permittedPowers => {
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 },
Expand All @@ -80,19 +80,19 @@ export const startAgoricBasicsContract = async permittedPowers => {
});
console.log('CoreEval script: started contract', instance);
const {
brands: { Item: brand },
dckc marked this conversation as resolved.
Show resolved Hide resolved
issuers: { Item: issuer },
brands: { Ticket: brand },
issuers: { Ticket: issuer },
} = await E(zoe).getTerms(instance);

console.log('CoreEval script: share via agoricNames:', brand);

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');
Expand All @@ -109,8 +109,8 @@ const agoricBasicsManifest = {
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 } },
issuer: { consume: { IST: true }, produce: { Ticket: true } },
brand: { consume: { IST: true }, produce: { Ticket: true } },
instance: { produce: { agoricBasics: true } },
},
};
Expand Down
130 changes: 95 additions & 35 deletions contract/src/agoric-basics.contract.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @file Contract to mint and sell a few Item NFTs at a time.
* @file Contract to mint and sell a few ticket NFTs at a time.
LuqiPan marked this conversation as resolved.
Show resolved Hide resolved
*
* We declare variables (including functions) before using them,
* so you may want to skip ahead and come back to some details.
Expand All @@ -22,76 +22,130 @@
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';
import { AmountMath, AmountShape } from '@agoric/ertp';

const { Fail, quote: q } = assert;

// #region bag utilities
/** @type { (xs: bigint[]) => bigint } */
const sum = xs => xs.reduce((acc, x) => acc + x, 0n);

Check failure on line 33 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

'sum' is assigned a value but never used. Allowed unused vars must match /^_/u

/**
* @param {import('@endo/patterns').CopyBag} bag
* @returns {bigint[]}
*/
const bagCounts = bag => {

Check failure on line 39 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

'bagCounts' is assigned a value but never used. Allowed unused vars must match /^_/u
const entries = getCopyBagEntries(bag);
return entries.map(([_k, ct]) => ct);
};

/**
*
* @param {import('@endo/patterns').CopyBag} bag
* @param {Object.<string, {tradePrice: Amount, maxTickets: bigint}>} inventory

Check warning on line 47 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

Use object shorthand or index signatures instead of `Object`, e.g., `{[key: string]: string}`
* @returns {boolean}
*/
export const hasInventory = (bag, inventory) => {
LuqiPan marked this conversation as resolved.
Show resolved Hide resolved
const entries = getCopyBagEntries(bag);
for (const [k, ct] of entries) {
if (k in inventory) {
const { maxTickets } = inventory[k];
if (ct > maxTickets) {
return false;
}
} else {
return false;
}
}

return true;
};

/**
*
* @param {Amount} amount
* @param {number} n
* @returns {Amount}
*/
const multiply = (amount, n) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question

I didn't see a AmountMath.multiply function so I implemented it myself

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import {
  makeRatio,
  multiplyBy,
} from '@agoric/zoe/src/contractSupport/ratio.js';
import { Nat } from '@endo/nat';

/**
 *
 * @param {Amount<'nat'>} amount
 * @param {number} n
 * @returns {Amount}
 */
const multiply = (amount, n) => {
  const r = makeRatio(Nat(n), amount.brand, 1n);
  return multiplyBy(amount, r);
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I seem to have some trouble making multiply work for Amount<AssetKind> instead of Amount<'nat'>, am I missing something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our ratio library doesn't support non-fungible amounts and says so in its type annotations... which means you may have to trace thru all the callers and update their types to Amount<'nat'>.

In some places, a typecast is called for. For example, zcf.getTerms() doesn't know whether the brands are for fungible assets or not. And you're not in a position to doubt the caller that started the contract - that's part of the reliance set. You could ask the brand what its AssetKind is, but it could lie, so it's not worth the bother.

const arr = Array.from({ length: n });
return arr.reduce(
(sum, _) => AmountMath.add(amount, sum),

Check failure on line 75 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

'sum' is already declared in the upper scope on line 33 column 7
AmountMath.make(amount.brand, 0n),
);
};

/**
*
* @param {Amount} sum
* @param {[string, bigint]} entry
* @param {Object.<string, {tradePrice: Amount, maxTickets: bigint}>} inventory

Check warning on line 84 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

Use object shorthand or index signatures instead of `Object`, e.g., `{[key: string]: string}`
* @returns {Amount}
*/
const addMultiples = (sum, entry, inventory) => {

Check failure on line 87 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

'sum' is already declared in the upper scope on line 33 column 7
const multiple = multiply(inventory[entry[0]].tradePrice, Number(entry[1]));
return AmountMath.add(multiple, sum);
};

/**
*
* @param {import('@endo/patterns').CopyBag} bag
* @param {Object.<string, {tradePrice: Amount, maxTickets: bigint}>} inventory

Check warning on line 95 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

Use object shorthand or index signatures instead of `Object`, e.g., `{[key: string]: string}`
* @returns {Amount}
*/
export const bagPrice = (bag, inventory) => {
const entries = getCopyBagEntries(bag);
const brand = Object.values(inventory)[0].tradePrice.brand;
LuqiPan marked this conversation as resolved.
Show resolved Hide resolved
return entries.reduce(
(sum, entry) => addMultiples(sum, entry, inventory),

Check failure on line 102 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

'sum' is already declared in the upper scope on line 33 column 7
dckc marked this conversation as resolved.
Show resolved Hide resolved
// TODO: a better way to create empty amount
AmountMath.make(brand, 0n),
LuqiPan marked this conversation as resolved.
Show resolved Hide resolved
);
};
// #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).
* optionally, a maximum number of tickets sold for that price (default: 3).
*
* @typedef {{

Check warning on line 114 in contract/src/agoric-basics.contract.js

View workflow job for this annotation

GitHub Actions / all

Use object shorthand or index signatures instead of `Object`, e.g., `{[key: string]: string}`
* tradePrice: Amount;
* maxItems?: bigint;
* inventory: Object.<string, {tradePrice: Amount, maxTickets: 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;
LuqiPan marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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.
* - creates a new non-fungible asset type for Tickets, and
* - handles offers to buy up to `maxTickets` tickets at a time.
*
* @param {ZCF<AgoricBasicsTerms>} zcf
*/
export const start = async zcf => {
const { tradePrice, maxItems = 3n } = zcf.getTerms();
const { inventory } = zcf.getTerms();

/**
* a new ERTP mint for items, accessed thru the Zoe Contract Facet.
* 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 potions and 1 map.
*/
const itemMint = await zcf.makeZCFMint('Item', AssetKind.COPY_BAG);
const { brand: itemBrand } = itemMint.getIssuerRecord();
const ticketMint = await zcf.makeZCFMint('Ticket', AssetKind.COPY_BAG);
const { brand: ticketBrand } = ticketMint.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.
* The `Tickets` amount must use the `Ticket` brand and a bag value.
*/
const proposalShape = harden({
give: { Price: M.gte(tradePrice) },
want: { Items: { brand: itemBrand, value: M.bag() } },
give: { Price: AmountShape },
want: { Tickets: { brand: ticketBrand, value: M.bag() } },
exit: M.any(),
});

Expand All @@ -101,39 +155,45 @@
/** @type {OfferHandler} */
const tradeHandler = buyerSeat => {
// give and want are guaranteed by Zoe to match proposalShape
const { want } = buyerSeat.getProposal();
const { give, want } = buyerSeat.getProposal();

hasInventory(want.Tickets.value, inventory) ||
dckc marked this conversation as resolved.
Show resolved Hide resolved
Fail`${q(want.Tickets.value)} wanted, which exceeds inventory ${q(
inventory,
)}`;

sum(bagCounts(want.Items.value)) <= maxItems ||
Fail`max ${q(maxItems)} items allowed: ${q(want.Items)}`;
const totalPrice = bagPrice(want.Tickets.value, inventory);
AmountMath.isGTE(give.Price, totalPrice) ||
Fail`Total price is ${q(totalPrice)}, but ${q(give.Price)} was given`;

const newItems = itemMint.mintGains(want);
const newTickets = ticketMint.mintGains(want);
atomicRearrange(
zcf,
harden([
// price from buyer to proceeds
[buyerSeat, proceeds, { Price: tradePrice }],
// new items to buyer
[newItems, buyerSeat, want],
[buyerSeat, proceeds, { Price: totalPrice }],
// new tickets to buyer
[newTickets, buyerSeat, want],
]),
);

buyerSeat.exit(true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question, for my learning

What to do with proceeds and inventorySeat?

Seems like we could get the proceeds when contract is closed by the creator via creatorFacet. Should we keep inventorySeat open even in the case when all tickets are sold?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, a creatorFacet method is a typical way to collect the proceeds. (Seems fine to postpone to a later PR, if we bother to do it at all)
See, for example, collectFees.js and the contracts that import it.

as to inventorySeat... when they're all sold, we could shut the contract down (there's a shutdown method on zcf)

Or we could have a creatorFacet method to resupply it for the next event or something. But that seems like over-kill.

newItems.exit();
newTickets.exit();
return 'trade complete';
};

/**
* Make an invitation to trade for items.
* Make an invitation to trade for tickets.
*
* Proposal Keywords used in offers using these invitations:
* - give: `Price`
* - want: `Items`
* - want: `Tickets`
*/
const makeTradeInvitation = () =>
zcf.makeInvitation(tradeHandler, 'buy items', undefined, proposalShape);
zcf.makeInvitation(tradeHandler, 'buy tickets', undefined, proposalShape);

// Mark the publicFacet Far, i.e. reachable from outside the contract
const publicFacet = Far('Items Public Facet', {
const publicFacet = Far('Tickets Public Facet', {
makeTradeInvitation,
});
return harden({ publicFacet });
Expand Down
Loading
Loading