1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

[EPIC] Free Pricing (#681)

* Free Pricing Option at create Pricing (#621)

* Free Pricing Option + env var toggle

* Create Pricing step msg

* Default 'allowFreePricing' to true temp for review

* Fix price 0 on free tab

* Attempt fix useSiteMetadata

* Fix linting

* Feature/free price support consume compute (#654)

* Update fetch free price

* Feedback change UI remove 0's

* update button msg && fix

* compute algorithm list show 'Free' instead of '0'

* updateMetadata() v3 workaround solution for free pricing (#677)

* compute algorithm list show 'Free' instead of '0'

* workaround editMetaData free price

* utils function for compute & download

* `allowFreePricing` default to false
This commit is contained in:
Kris Liew 2021-06-16 09:32:11 +08:00 committed by GitHub
parent eb8c6afb62
commit e02babf2c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 341 additions and 41 deletions

View File

@ -11,5 +11,6 @@ GATSBY_NETWORK="rinkeby"
#GATSBY_PORTIS_ID="xxx"
#GATSBY_ALLOW_FIXED_PRICING="true"
#GATSBY_ALLOW_DYNAMIC_PRICING="true"
#GATSBY_ALLOW_FREE_PRICING="true"
#GATSBY_ALLOW_ADVANCED_SETTINGS="true"
#GATSBY_CREDENTIAL_TYPE="address"

View File

@ -2,8 +2,7 @@ module.exports = {
client: {
service: {
name: 'ocean',
url:
'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph',
url: 'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph',
// optional disable SSL validation check
skipSSLValidation: true
}

View File

@ -41,10 +41,11 @@ module.exports = {
// Wallets
portisId: process.env.GATSBY_PORTIS_ID || 'xxx',
// Used to show or hide the fixed and dynamic price options
// Used to show or hide the fixed, dynamic or free price options
// tab to publishers during the price creation.
allowFixedPricing: process.env.GATSBY_ALLOW_FIXED_PRICING || 'true',
allowDynamicPricing: process.env.GATSBY_ALLOW_DYNAMIC_PRICING || 'true',
allowFreePricing: process.env.GATSBY_ALLOW_FREE_PRICING || 'false',
// Used to show or hide advanced settings button in asset details page
allowAdvancedSettings: process.env.GATSBY_ALLOW_ADVANCED_SETTINGS || 'false',

View File

@ -21,6 +21,10 @@
"communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace fee..."
}
},
"free": {
"title": "Free",
"info": "Set your data set as free. The datatoken for this data set will be given for free via creating a faucet."
}
},
"pool": {

View File

@ -21,6 +21,8 @@ interface ButtonBuyProps {
onClick?: (e: FormEvent<HTMLButtonElement>) => void
stepText?: string
type?: 'submit'
priceType?: string
algorithmPriceType?: string
}
function getConsumeHelpText(
@ -87,15 +89,21 @@ export default function ButtonBuy({
onClick,
stepText,
isLoading,
type
type,
priceType,
algorithmPriceType
}: ButtonBuyProps): ReactElement {
const buttonText =
action === 'download'
? hasPreviousOrder
? 'Download'
: priceType === 'free'
? 'Get'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
: hasPreviousOrder && hasPreviousOrderSelectedComputeAsset
? 'Start Compute Job'
: priceType === 'free' && algorithmPriceType === 'free'
? 'Order Compute Job'
: `Buy Compute Job`
return (

View File

@ -42,6 +42,10 @@ export default function PriceUnit({
return (
<div className={styleClasses}>
{type && type === 'free' ? (
<div> Free </div>
) : (
<>
<div>
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
<span className={styles.symbol}>{symbol || 'OCEAN'}</span>
@ -49,8 +53,9 @@ export default function PriceUnit({
<Badge label="pool" className={styles.badge} />
)}
</div>
{conversion && <Conversion price={price} />}
</>
)}
</div>
)
}

View File

@ -16,7 +16,7 @@ export default function Price({
small?: boolean
conversion?: boolean
}): ReactElement {
return price?.value ? (
return price?.value || price?.type === 'free' ? (
<PriceUnit
price={`${price.value}`}
className={className}

View File

@ -107,7 +107,12 @@ export default function AssetSelection({
</Dotdotdot>
</label>
<PriceUnit price={asset.price} small className={styles.price} />
<PriceUnit
price={asset.price}
type={asset.price === '0' ? 'free' : undefined}
small
className={styles.price}
/>
</div>
))
)}

View File

@ -166,6 +166,8 @@ export default function FormStartCompute({
stepText={stepText}
isLoading={isLoading}
type="submit"
priceType={price?.type}
algorithmPriceType={algorithmPrice?.type}
/>
</Form>
)

View File

@ -160,6 +160,7 @@ export default function Consume({
assetType={type}
stepText={consumeStepText || pricingStepText}
isLoading={pricingIsLoading || isLoading}
priceType={price?.type}
/>
)

View File

@ -16,6 +16,10 @@ import { useUserPreferences } from '../../../../providers/UserPreferences'
import DebugEditCompute from './DebugEditCompute'
import styles from './index.module.css'
import { transformComputeFormToServiceComputePrivacy } from '../../../../utils/compute'
import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
const contentQuery = graphql`
query EditComputeDataQuery {
@ -62,7 +66,7 @@ export default function EditComputeDataset({
const { debug } = useUserPreferences()
const { ocean } = useOcean()
const { accountId } = useWeb3()
const { ddo, refreshDdo } = useAsset()
const { ddo, refreshDdo, price } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
@ -73,6 +77,15 @@ export default function EditComputeDataset({
resetForm: () => void
) {
try {
if (price.type === 'free') {
const tx = await setMinterToPublisher(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
const privacy = await transformComputeFormToServiceComputePrivacy(
values,
ocean
@ -99,6 +112,15 @@ export default function EditComputeDataset({
Logger.error(content.form.error)
return
} else {
if (price.type === 'free') {
const tx = await setMinterToDispenser(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
// Edit succeeded
setSuccess(content.form.success)
resetForm()

View File

@ -18,6 +18,10 @@ import MetadataFeedback from '../../../molecules/MetadataFeedback'
import { graphql, useStaticQuery } from 'gatsby'
import { useWeb3 } from '../../../../providers/Web3'
import { useOcean } from '../../../../providers/Ocean'
import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
const contentQuery = graphql`
query EditMetadataQuery {
@ -88,6 +92,15 @@ export default function Edit({
resetForm: () => void
) {
try {
if (price.type === 'free') {
const tx = await setMinterToPublisher(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
// Construct new DDO with new values
const ddoEditedMetdata = await ocean.assets.editMetadata(ddo, {
title: values.name,
@ -132,6 +145,15 @@ export default function Edit({
Logger.error(content.form.error)
return
} else {
if (price.type === 'free') {
const tx = await setMinterToDispenser(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
// Edit succeeded
setSuccess(content.form.success)
resetForm()

View File

@ -65,6 +65,7 @@ export default function AssetActions(): ReactElement {
// Check user balance against price
useEffect(() => {
if (price?.type === 'free') setIsBalanceSufficient(true)
if (!price?.value || !account || !balance?.ocean || !dtBalance) return
setIsBalanceSufficient(

View File

@ -0,0 +1,3 @@
.free {
composes: content from './index.module.css';
}

View File

@ -0,0 +1,21 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Free.module.css'
import FormHelp from '../../../../atoms/Input/Help'
import { DDO } from '@oceanprotocol/lib'
import Price from './Price'
export default function Free({
ddo,
content
}: {
ddo: DDO
content: any
}): ReactElement {
return (
<div className={styles.free}>
<FormHelp className={stylesIndex.help}>{content.info}</FormHelp>
<Price ddo={ddo} free />
</div>
)
}

View File

@ -10,10 +10,12 @@ import usePricing from '../../../../../hooks/usePricing'
export default function Price({
ddo,
firstPrice
firstPrice,
free
}: {
ddo: DDO
firstPrice?: string
free?: boolean
}): ReactElement {
const [field, meta] = useField('price')
const { getDTName, getDTSymbol } = usePricing()
@ -38,6 +40,15 @@ export default function Price({
<div className={styles.price}>
<div className={styles.grid}>
<div className={styles.form}>
{free ? (
<Input
value="0"
name="price"
type="number"
prefix="OCEAN"
readOnly
/>
) : (
<Input
value={field.value}
name="price"
@ -49,6 +60,7 @@ export default function Price({
<Conversion price={field.value} className={styles.conversion} />
}
/>
)}
<Error meta={meta} />
</div>
<div className={styles.datatoken}>

View File

@ -45,3 +45,8 @@
padding-left: var(--spacer);
padding-right: var(--spacer);
}
.free {
text-align: center;
margin-bottom: calc(var(--spacer) / 1.5);
}

View File

@ -3,6 +3,7 @@ import styles from './index.module.css'
import Tabs from '../../../../atoms/Tabs'
import Fixed from './Fixed'
import Dynamic from './Dynamic'
import Free from './Free'
import { useFormikContext } from 'formik'
import { useUserPreferences } from '../../../../../providers/UserPreferences'
import { PriceOptionsMarket } from '../../../../../@types/MetaData'
@ -33,6 +34,7 @@ export default function FormPricing({
const type = tabName.toLowerCase()
setFieldValue('type', type)
type === 'fixed' && setFieldValue('dtAmount', 1000)
type === 'free' && price < 1 && setFieldValue('price', 1)
}
// Always update everything when price value changes
@ -57,6 +59,12 @@ export default function FormPricing({
title: content.dynamic.title,
content: <Dynamic content={content.dynamic} ddo={ddo} />
}
: undefined,
appConfig.allowFreePricing === 'true'
? {
title: content.free.title,
content: <Free content={content.free} ddo={ddo} />
}
: undefined
].filter((tab) => tab !== undefined)

View File

@ -40,6 +40,10 @@ const query = graphql`
marketplaceFee
}
}
free {
title
info
}
}
}
}

View File

@ -5,7 +5,9 @@ import { Decimal } from 'decimal.js'
import {
getCreatePricingPoolFeedback,
getCreatePricingExchangeFeedback,
getBuyDTFeedback
getBuyDTFeedback,
getCreateFreePricingFeedback,
getDispenseFeedback
} from '../utils/feedback'
import { sleep } from '../utils'
@ -16,7 +18,7 @@ interface PriceOptions {
price: number
dtAmount: number
oceanAmount: number
type: 'fixed' | 'dynamic' | string
type: 'fixed' | 'dynamic' | 'free' | string
weightOnDataToken: string
swapFee: string
}
@ -68,7 +70,7 @@ function usePricing(): UsePricing {
// Helper for setting steps & feedback for all flows
async function setStep(
index: number,
type: 'pool' | 'exchange' | 'buy',
type: 'pool' | 'exchange' | 'free' | 'buy' | 'dispense',
ddo: DDO
) {
const dtSymbol = await getDTSymbol(ddo)
@ -84,9 +86,15 @@ function usePricing(): UsePricing {
case 'exchange':
messages = getCreatePricingExchangeFeedback(dtSymbol)
break
case 'free':
messages = getCreateFreePricingFeedback(dtSymbol)
break
case 'buy':
messages = getBuyDTFeedback(dtSymbol)
break
case 'dispense':
messages = getDispenseFeedback(dtSymbol)
break
}
setPricingStepText(messages[index])
@ -180,6 +188,28 @@ function usePricing(): UsePricing {
Logger.log('DT exchange buy response', tx)
break
}
case 'free': {
setStep(1, 'dispense', ddo)
const isDispensable = await ocean.OceanDispenser.isDispensable(
ddo.dataToken,
accountId,
'1'
)
if (!isDispensable) {
Logger.error(`Dispenser for ${ddo.dataToken} failed to dispense`)
return
}
tx = await ocean.OceanDispenser.dispense(
ddo.dataToken,
accountId,
'1'
)
setStep(2, 'dispense', ddo)
Logger.log('DT dispense response', tx)
break
}
}
} catch (error) {
setPricingError(error.message)
@ -219,9 +249,14 @@ function usePricing(): UsePricing {
setStep(99, 'pool', ddo)
try {
if (type === 'free') {
setStep(99, 'free', ddo)
await ocean.OceanDispenser.activate(dataToken, '1', '1', accountId)
} else {
// if fixedPrice set dt to max amount
if (!isPool) dtAmount = 1000
await mint(`${dtAmount}`, ddo)
}
// dtAmount for fixed price is set to max
const tx = isPool
@ -235,9 +270,13 @@ function usePricing(): UsePricing {
swapFee
)
.next((step: number) => setStep(step, 'pool', ddo))
: await ocean.fixedRateExchange
: type === 'fixed'
? await ocean.fixedRateExchange
.create(dataToken, `${price}`, accountId, `${dtAmount}`)
.next((step: number) => setStep(step, 'exchange', ddo))
: await ocean.OceanDispenser.makeMinter(dataToken, accountId).next(
(step: number) => setStep(step, 'free', ddo)
)
await sleep(20000)
return tx
} catch (error) {

View File

@ -27,6 +27,7 @@ interface UseSiteMetadata {
portisId: string
allowFixedPricing: string
allowDynamicPricing: string
allowFreePricing: string
allowAdvancedSettings: string
credentialType: string
}
@ -61,6 +62,7 @@ const query = graphql`
portisId
allowFixedPricing
allowDynamicPricing
allowFreePricing
allowAdvancedSettings
credentialType
}

View File

@ -13,7 +13,7 @@ export const validationSchema: Yup.SchemaOf<PriceOptionsMarket> =
.min(21, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
type: Yup.string()
.matches(/fixed|dynamic/g, { excludeEmptyString: true })
.matches(/fixed|dynamic|free/g, { excludeEmptyString: true })
.required('Required'),
weightOnDataToken: Yup.string().required('Required'),
weightOnOcean: Yup.string().required('Required'),

View File

@ -52,6 +52,17 @@ export function getCreatePricingExchangeFeedback(dtSymbol: string): {
}
}
export function getCreateFreePricingFeedback(dtSymbol: string): {
[key: number]: string
} {
return {
99: `Creating ${dtSymbol} faucet...`,
0: 'Setting faucet as minter ...',
1: 'Approving minter...',
2: 'Faucet created.'
}
}
export function getBuyDTFeedback(dtSymbol: string): { [key: number]: string } {
return {
1: '1/3 Approving OCEAN ...',
@ -67,3 +78,12 @@ export function getSellDTFeedback(dtSymbol: string): { [key: number]: string } {
3: `3/3 ${dtSymbol} sold.`
}
}
export function getDispenseFeedback(dtSymbol: string): {
[key: number]: string
} {
return {
1: `1/2 Requesting ${dtSymbol}...`,
2: `2/2 Received ${dtSymbol}.`
}
}

37
src/utils/freePrice.ts Normal file
View File

@ -0,0 +1,37 @@
import { Logger, Ocean } from '@oceanprotocol/lib'
export async function setMinterToPublisher(
ocean: Ocean,
dataTokenAddress: string,
accountId: string,
setError: (msg: string) => void
): Promise<any> {
// free pricing v3 workaround part1
const response = await ocean.OceanDispenser.cancelMinter(
dataTokenAddress,
accountId
)
if (!response) {
setError('Updating DDO failed.')
Logger.error('Failed at cancelMinter')
}
return response
}
export async function setMinterToDispenser(
ocean: Ocean,
dataTokenAddress: string,
accountId: string,
setError: (msg: string) => void
): Promise<any> {
// free pricing v3 workaround part2
const response = await ocean.OceanDispenser.makeMinter(
dataTokenAddress,
accountId
)
if (!response) {
setError('Updating DDO failed.')
Logger.error('Failed at makeMinter')
}
return response
}

View File

@ -10,6 +10,10 @@ import {
AssetsFrePrice_fixedRateExchanges as AssetsFrePriceFixedRateExchanges
} from '../@types/apollo/AssetsFrePrice'
import { AssetPreviousOrder } from '../@types/apollo/AssetPreviousOrder'
import {
AssetsFreePrice,
AssetsFreePrice_dispensers as AssetFreePriceDispenser
} from '../@types/apollo/AssetsFreePrice'
import web3 from 'web3'
export interface PriceList {
@ -25,6 +29,36 @@ interface DidAndDatatokenMap {
[name: string]: string
}
const FreeQuery = gql`
query AssetsFreePrice($datatoken_in: [String!]) {
dispensers(orderBy: id, where: { datatoken_in: $datatoken_in }) {
datatoken {
id
address
}
}
}
`
const AssetFreeQuery = gql`
query AssetFreePrice($datatoken: String) {
dispensers(orderBy: id, where: { datatoken: $datatoken }) {
active
owner {
id
}
minterApproved
isTrueMinter
maxTokens
maxBalance
balance
datatoken {
id
}
}
}
`
const FreQuery = gql`
query AssetsFrePrice($datatoken_in: [String!]) {
fixedRateExchanges(orderBy: id, where: { datatoken_in: $datatoken_in }) {
@ -146,7 +180,8 @@ export async function getPreviousOrders(
function transformPriceToBestPrice(
frePrice: AssetsFrePriceFixedRateExchanges[],
poolPrice: AssetsPoolPricePools[]
poolPrice: AssetsPoolPricePools[],
freePrice: AssetFreePriceDispenser[]
) {
if (poolPrice?.length > 0) {
const price: BestPrice = {
@ -176,6 +211,18 @@ function transformPriceToBestPrice(
isConsumable: 'true'
}
return price
} else if (freePrice?.length > 0) {
const price: BestPrice = {
type: 'free',
value: 0,
address: freePrice[0]?.datatoken.id,
exchange_id: '',
ocean: 0,
datatoken: 0,
pools: [],
isConsumable: 'true'
}
return price
} else {
const price: BestPrice = {
type: '',
@ -197,6 +244,7 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
[
ApolloQueryResult<AssetsPoolPrice>,
ApolloQueryResult<AssetsFrePrice>,
ApolloQueryResult<AssetsFreePrice>,
DidAndDatatokenMap
]
> {
@ -214,6 +262,10 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
datatokenAddress_in: dataTokenList
}
const freeVariables = {
datatoken_in: dataTokenList
}
const poolPriceResponse: ApolloQueryResult<AssetsPoolPrice> = await fetchData(
PoolQuery,
poolVariables
@ -223,7 +275,12 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
freVariables
)
return [poolPriceResponse, frePriceResponse, didDTMap]
const freePriceResponse: ApolloQueryResult<AssetsFreePrice> = await fetchData(
FreeQuery,
freeVariables
)
return [poolPriceResponse, frePriceResponse, freePriceResponse, didDTMap]
}
export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
@ -232,11 +289,13 @@ export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
const values: [
ApolloQueryResult<AssetsPoolPrice>,
ApolloQueryResult<AssetsFrePrice>,
ApolloQueryResult<AssetsFreePrice>,
DidAndDatatokenMap
] = await getAssetsPoolsExchangesAndDatatokenMap(assets)
const poolPriceResponse = values[0]
const frePriceResponse = values[1]
const didDTMap: DidAndDatatokenMap = values[2]
const freePriceResponse = values[2]
const didDTMap: DidAndDatatokenMap = values[3]
for (const poolPrice of poolPriceResponse.data?.pools) {
priceList[didDTMap[poolPrice.datatokenAddress]] =
@ -247,6 +306,9 @@ export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
for (const frePrice of frePriceResponse.data?.fixedRateExchanges) {
priceList[didDTMap[frePrice.datatoken?.address]] = frePrice.rate
}
for (const freePrice of freePriceResponse.data?.dispensers) {
priceList[didDTMap[freePrice.datatoken?.address]] = '0'
}
return priceList
}
@ -259,6 +321,10 @@ export async function getPrice(asset: DDO): Promise<BestPrice> {
datatokenAddress: asset?.dataToken.toLowerCase()
}
const freeVariables = {
datatoken: asset?.dataToken.toLowerCase()
}
const poolPriceResponse: ApolloQueryResult<AssetsPoolPrice> = await fetchData(
AssetPoolPriceQuerry,
poolVariables
@ -267,10 +333,15 @@ export async function getPrice(asset: DDO): Promise<BestPrice> {
AssetFreQuery,
freVariables
)
const freePriceResponse: ApolloQueryResult<AssetsFreePrice> = await fetchData(
AssetFreeQuery,
freeVariables
)
const bestPrice: BestPrice = transformPriceToBestPrice(
frePriceResponse.data.fixedRateExchanges,
poolPriceResponse.data.pools
poolPriceResponse.data.pools,
freePriceResponse.data.dispensers
)
return bestPrice
@ -284,15 +355,18 @@ export async function getAssetsBestPrices(
const values: [
ApolloQueryResult<AssetsPoolPrice>,
ApolloQueryResult<AssetsFrePrice>,
ApolloQueryResult<AssetsFreePrice>,
DidAndDatatokenMap
] = await getAssetsPoolsExchangesAndDatatokenMap(assets)
const poolPriceResponse = values[0]
const frePriceResponse = values[1]
const freePriceResponse = values[2]
for (const ddo of assets) {
const dataToken = ddo.dataToken.toLowerCase()
const poolPrice: AssetsPoolPricePools[] = []
const frePrice: AssetsFrePriceFixedRateExchanges[] = []
const freePrice: AssetFreePriceDispenser[] = []
const pool = poolPriceResponse.data?.pools.find(
(pool: any) => pool.datatokenAddress === dataToken
)
@ -301,7 +375,11 @@ export async function getAssetsBestPrices(
(fre: any) => fre.datatoken.address === dataToken
)
fre && frePrice.push(fre)
const bestPrice = transformPriceToBestPrice(frePrice, poolPrice)
const free = freePriceResponse.data?.dispensers.find(
(free: any) => free.datatoken.address === dataToken
)
free && freePrice.push(free)
const bestPrice = transformPriceToBestPrice(frePrice, poolPrice, freePrice)
assetsWithPrice.push({
ddo: ddo,
price: bestPrice