-
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 14 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 |
---|---|---|
|
@@ -12,9 +12,6 @@ const BOARD_AUX = 'boardAux'; | |
|
||
const marshalData = makeMarshal(_val => Fail`data only`); | ||
|
||
const IST_UNIT = 1_000_000n; | ||
const CENT = IST_UNIT / 100n; | ||
|
||
/** | ||
* Make a storage node for auxilliary data for a value on the board. | ||
* | ||
|
@@ -44,7 +41,13 @@ const publishBrandInfo = async (chainStorage, board, brand) => { | |
export const startOfferUpContract = async permittedPowers => { | ||
console.error('startOfferUpContract()...'); | ||
const { | ||
consume: { board, chainStorage, startUpgradable, zoe }, | ||
consume: { | ||
board, | ||
chainStorage, | ||
startUpgradable, | ||
zoe, | ||
chainTimerService: chainTimerServiceP, | ||
}, | ||
brand: { | ||
consume: { IST: istBrandP }, | ||
// @ts-expect-error dynamic extension to promise space | ||
|
@@ -66,8 +69,12 @@ export const startOfferUpContract = async permittedPowers => { | |
|
||
const istIssuer = await istIssuerP; | ||
const istBrand = await istBrandP; | ||
const timerService = await await chainTimerServiceP; | ||
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. I don't know whether |
||
|
||
const terms = { tradePrice: AmountMath.make(istBrand, 25n * CENT) }; | ||
const terms = { | ||
subscriptionPrice: AmountMath.make(istBrand, 10000000n), | ||
timerService, | ||
}; | ||
|
||
// agoricNames gets updated each time; the promise space only once XXXXXXX | ||
const installation = await offerUpInstallationP; | ||
|
@@ -107,6 +114,7 @@ const offerUpManifest = { | |
chainStorage: true, // to publish boardAux info for NFT brand | ||
startUpgradable: true, // to start contract and save adminFacet | ||
zoe: true, // to get contract terms, including issuer/brand | ||
chainTimerService: true, | ||
}, | ||
installation: { consume: { offerUp: true } }, | ||
issuer: { consume: { IST: true }, produce: { Item: true } }, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,58 +19,48 @@ | |
*/ | ||
// @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'; | ||
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 | ||
* timerService: any; | ||
* 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, | ||
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. The timer service is sufficiently powerful that it should be closely held; i.e. passed in privateArgs rather than terms. See also: |
||
subscriptionPrice, | ||
subscriptionPeriod = 'MONTHLY', | ||
servicesToAvail = ['Netflix', 'Amazon', 'HboMax', 'Disney'], | ||
} = zcf.getTerms(); | ||
|
||
const subscriptionResources = {}; | ||
|
||
servicesToAvail.forEach(element => { | ||
subscriptionResources[element] = [ | ||
`${element}_Movie_1`, | ||
`${element}_Movie_2`, | ||
]; | ||
}); | ||
|
||
/** | ||
* a new ERTP mint for items, accessed thru the Zoe Contract Facet. | ||
|
@@ -81,7 +71,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 +81,47 @@ | |
* 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() } }, | ||
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. why not itemBrand? |
||
exit: M.any(), | ||
}); | ||
|
||
/** a seat for allocating proceeds of sales */ | ||
const proceeds = zcf.makeEmptySeatKit().zcfSeat; | ||
|
||
const subscriptions = new Map(); | ||
|
||
/** @type {OfferHandler} */ | ||
const tradeHandler = buyerSeat => { | ||
// give and want are guaranteed by Zoe to match proposalShape | ||
const { want } = buyerSeat.getProposal(); | ||
const tradeHandler = async (buyerSeat, offerArgs) => { | ||
// @ts-ignore | ||
const userAddress = offerArgs.userAddress; | ||
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. what's the goal here? 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. see suggested alternative below |
||
// @ts-ignore | ||
const serviceType = offerArgs.serviceType; | ||
const currentTimeRecord = await E(timerService).getCurrentTimestamp(); | ||
|
||
const amountObject = AmountMath.make( | ||
brand, | ||
makeCopyBag([[{ serviceStarted: currentTimeRecord, serviceType }, 1n]]), | ||
); | ||
const want = { Items: amountObject }; | ||
|
||
sum(bagCounts(want.Items.value)) <= maxItems || | ||
Fail`max ${q(maxItems)} items allowed: ${q(want.Items)}`; | ||
const newSubscription = itemMint.mintGains(want); | ||
|
||
const newItems = itemMint.mintGains(want); | ||
atomicRearrange( | ||
zcf, | ||
harden([ | ||
// price from buyer to proceeds | ||
[buyerSeat, proceeds, { Price: tradePrice }], | ||
[buyerSeat, proceeds, { Price: subscriptionPrice }], | ||
// new items to buyer | ||
[newItems, buyerSeat, want], | ||
[newSubscription, buyerSeat, want], | ||
]), | ||
); | ||
|
||
subscriptions.set(userAddress, want.Items); | ||
|
||
buyerSeat.exit(true); | ||
newItems.exit(); | ||
return 'trade complete'; | ||
newSubscription.exit(); | ||
return 'Subscription Granted'; | ||
}; | ||
|
||
/** | ||
|
@@ -130,11 +132,42 @@ | |
* - 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 => { | ||
const userSubscription = subscriptions.get(userAddress); | ||
|
||
const isValidSub = isSubscriptionValid(userSubscription); | ||
if (isValidSub) { | ||
// User has a valid subscription, return the resources | ||
const serviceType = userSubscription.value.payload[0][0].serviceType; | ||
return 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.
dapp dependencies are, unfortunately, tremendously fragile. Mixing u12 and u16 is asking for trouble, IME.
We should probably have a better scoped issue, but the tale of woe is in...
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.
I tried rolling back to the u12 version of
@agoric/time
(60e2308) but the symptoms remain.