-
Notifications
You must be signed in to change notification settings - Fork 5
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
[Hackathon] Subscription service contract #98
base: main
Are you sure you want to change the base?
Changes from all commits
0d46237
f63a18c
42e935b
3ac0d4a
ea8a272
7d0049e
d7e87d1
a7ddc25
505135a
cc33cde
0816f87
9de205f
d8ac30e
e89f98d
80eb847
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,58 +19,47 @@ | |
*/ | ||
// @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 { Far, E } from '@endo/far'; | ||
Check failure on line 22 in contract/src/offer-up.contract.js GitHub Actions / unit (18)
|
||
import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js'; | ||
import { makeCopyBag, M } from '@endo/patterns'; | ||
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; | ||
* }} OfferUpTerms | ||
* subscriptionPrice: Amount; | ||
* subscriptionPeriod?: string; | ||
* servicesToAvail?: Array<string>; | ||
* }} SubscriptionServiceTerms | ||
*/ | ||
|
||
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<OfferUpTerms>} zcf | ||
* @param {ZCF<SubscriptionServiceTerms>} zcf | ||
*/ | ||
export const start = async zcf => { | ||
const { tradePrice, maxItems = 3n } = zcf.getTerms(); | ||
const { | ||
// timerService, | ||
subscriptionPrice, | ||
subscriptionPeriod = 'MONTHLY', | ||
Check failure on line 51 in contract/src/offer-up.contract.js GitHub Actions / unit (18)
|
||
servicesToAvail = ['Netflix', 'Amazon', 'HboMax', 'Disney'], | ||
} = zcf.getTerms(); | ||
|
||
const subscriptionResources = {}; | ||
|
||
servicesToAvail.forEach(element => { | ||
Check warning on line 57 in contract/src/offer-up.contract.js GitHub Actions / unit (18)
|
||
subscriptionResources[element] = [ | ||
`${element}_Movie_1`, | ||
`${element}_Movie_2`, | ||
]; | ||
}); | ||
|
||
/** | ||
* a new ERTP mint for items, accessed thru the Zoe Contract Facet. | ||
|
@@ -81,7 +70,8 @@ | |
* amounts such as: 3 potions and 1 map. | ||
*/ | ||
const itemMint = await zcf.makeZCFMint('Item', AssetKind.COPY_BAG); | ||
const { brand: itemBrand } = itemMint.getIssuerRecord(); | ||
|
||
const { brand } = itemMint.getIssuerRecord(); | ||
|
||
/** | ||
* a pattern to constrain proposals given to {@link tradeHandler} | ||
|
@@ -90,36 +80,56 @@ | |
* 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() } }, | ||
give: { Price: M.eq(subscriptionPrice) }, | ||
want: { Items: { brand: M.any(), 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], | ||
]), | ||
); | ||
const subscriptions = new Map(); | ||
|
||
buyerSeat.exit(true); | ||
newItems.exit(); | ||
return 'trade complete'; | ||
/** @type {OfferHandler} */ | ||
const tradeHandler = (buyerSeat, offerArgs) => { | ||
// @ts-ignore | ||
const { userAddress, serviceType, offerType } = offerArgs; | ||
// const currentTimeRecord = await E(timerService).getCurrentTimestamp(); | ||
|
||
if (offerType === 'BUY_SUBSCRIPTION') { | ||
const amountObject = AmountMath.make( | ||
brand, | ||
makeCopyBag([[{ serviceStarted: '123', serviceType }, 1n]]), | ||
); | ||
const want = { Items: amountObject }; | ||
|
||
const newSubscription = itemMint.mintGains(want); | ||
|
||
atomicRearrange( | ||
zcf, | ||
harden([ | ||
// price from buyer to proceeds | ||
[buyerSeat, proceeds, { Price: subscriptionPrice }], | ||
// new items to buyer | ||
[newSubscription, buyerSeat, want], | ||
]), | ||
); | ||
|
||
const subscriptionKey = `${userAddress}_${serviceType}`; | ||
subscriptions.set(subscriptionKey, want.Items); | ||
|
||
|
||
buyerSeat.exit(true); | ||
newSubscription.exit(); | ||
return 'Subscription Granted'; | ||
|
||
} | ||
else if (offerType === 'VIEW_SUBSCRIPTION') { | ||
buyerSeat.exit(); | ||
return getSubscriptionResources(userAddress, serviceType); | ||
Check failure on line 129 in contract/src/offer-up.contract.js GitHub Actions / unit (18)
|
||
} | ||
|
||
|
||
}; | ||
|
||
/** | ||
|
@@ -130,11 +140,43 @@ | |
* - want: `Items` | ||
*/ | ||
const makeTradeInvitation = () => | ||
zcf.makeInvitation(tradeHandler, 'buy items', undefined, proposalShape); | ||
zcf.makeInvitation( | ||
tradeHandler, | ||
'buy subscription', | ||
undefined, | ||
proposalShape, | ||
); | ||
|
||
const isSubscriptionValid = userSubscription => { | ||
if (!userSubscription || !userSubscription.value.payload) return false; | ||
|
||
const serviceStarted = userSubscription.value.payload[0][0].serviceStarted; | ||
|
||
// Here we'll check with current time from time service. | ||
if (!serviceStarted || serviceStarted !== '123') return false; | ||
return true; | ||
// | ||
}; | ||
|
||
const getSubscriptionResources = (userAddress, serviceType) => { | ||
const subscriptionKey = `${userAddress}_${serviceType}`; | ||
const userSubscription = subscriptions.get(subscriptionKey); | ||
|
||
const isValidSub = isSubscriptionValid(userSubscription); | ||
if (isValidSub) { | ||
// User has a valid subscription, return the resources | ||
const serviceType = userSubscription.value.payload[0][0].serviceType; | ||
Check failure on line 168 in contract/src/offer-up.contract.js GitHub Actions / unit (18)
|
||
return JSON.stringify(subscriptionResources[serviceType]); | ||
} else { | ||
// User doesn't have a valid subscription | ||
return 'Access denied: You do not have a valid subscription.'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samsiegart what was the hackathon project you worked on where services would look for NFTs in wallets (using vstorage queries) to gate access? Ah... right... https://github.com/agoric-labs/agoric-passport-express |
||
} | ||
}; | ||
|
||
// Mark the publicFacet Far, i.e. reachable from outside the contract | ||
const publicFacet = Far('Items Public Facet', { | ||
makeTradeInvitation, | ||
getSubscriptionResources, | ||
}); | ||
return harden({ publicFacet }); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not itemBrand?