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

[Hackathon] Subscription service contract #98

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 13 additions & 5 deletions contract/src/offer-up-proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand All @@ -66,8 +69,12 @@ export const startOfferUpContract = async permittedPowers => {

const istIssuer = await istIssuerP;
const istBrand = await istBrandP;
// const timerService = await await chainTimerServiceP;

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;
Expand Down Expand Up @@ -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 } },
Expand Down
160 changes: 101 additions & 59 deletions contract/src/offer-up.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / unit (18)

'E' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 22 in contract/src/offer-up.contract.js

View workflow job for this annotation

GitHub Actions / unit (20)

'E' is defined but never used. Allowed unused vars must match /^_/u
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

View workflow job for this annotation

GitHub Actions / unit (18)

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

Check failure on line 51 in contract/src/offer-up.contract.js

View workflow job for this annotation

GitHub Actions / unit (20)

'subscriptionPeriod' is assigned a value but never used. Allowed unused vars must match /^_/u
servicesToAvail = ['Netflix', 'Amazon', 'HboMax', 'Disney'],
} = zcf.getTerms();

const subscriptionResources = {};

servicesToAvail.forEach(element => {

Check warning on line 57 in contract/src/offer-up.contract.js

View workflow job for this annotation

GitHub Actions / unit (18)

Prefer for...of instead of Array.forEach

Check warning on line 57 in contract/src/offer-up.contract.js

View workflow job for this annotation

GitHub Actions / unit (20)

Prefer for...of instead of Array.forEach
subscriptionResources[element] = [
`${element}_Movie_1`,
`${element}_Movie_2`,
];
});

/**
* a new ERTP mint for items, accessed thru the Zoe Contract Facet.
Expand All @@ -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}
Expand All @@ -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() } },
Copy link
Member

Choose a reason for hiding this comment

The 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;

/** @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

View workflow job for this annotation

GitHub Actions / unit (18)

'getSubscriptionResources' was used before it was defined

Check failure on line 129 in contract/src/offer-up.contract.js

View workflow job for this annotation

GitHub Actions / unit (20)

'getSubscriptionResources' was used before it was defined
}


};

/**
Expand All @@ -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

View workflow job for this annotation

GitHub Actions / unit (18)

'serviceType' is already declared in the upper scope on line 161 column 50

Check failure on line 168 in contract/src/offer-up.contract.js

View workflow job for this annotation

GitHub Actions / unit (20)

'serviceType' is already declared in the upper scope on line 161 column 50
return JSON.stringify(subscriptionResources[serviceType]);
} else {
// User doesn't have a valid subscription
return 'Access denied: You do not have a valid subscription.';
Copy link
Member

Choose a reason for hiding this comment

The 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 });
};
Expand Down
Loading
Loading