Merge branch 'main' into feature/multinetwork

This commit is contained in:
Norbi 2021-07-01 14:44:21 +03:00
commit 14f2f2a747
41 changed files with 26099 additions and 254 deletions

View File

@ -1,15 +1,27 @@
# Default network, possible values:
# "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha"
# "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha",
# "gaiaxtestnet", "mumbai", "bsc"
GATSBY_NETWORK="rinkeby"
## Define a GATSBY_RBAC_URL to implement permission based restrictions
#GATSBY_RBAC_URL="http://localhost:3000"
#GATSBY_INFURA_PROJECT_ID="xxx"
#GATSBY_MARKET_FEE_ADDRESS="0xxx"
#GATSBY_ANALYTICS_ID="xxx"
#GATSBY_PORTIS_ID="xxx"
#
# ADVANCED SETTINGS
#
# Toggle pricing options presented during price creation
#GATSBY_ALLOW_FIXED_PRICING="true"
#GATSBY_ALLOW_DYNAMIC_PRICING="true"
#GATSBY_ALLOW_FREE_PRICING="false"
# Define RBAC server URL to implement permission based restrictions
#GATSBY_RBAC_URL="http://localhost:3000"
# Enables another asset editing button holder further advanced settings
#GATSBY_ALLOW_ADVANCED_SETTINGS="true"
# Allow/Deny Lists
#GATSBY_CREDENTIAL_TYPE="address"

View File

@ -26,6 +26,7 @@
- [⬆️ Deployment](#-deployment)
- [💖 Contributing](#-contributing)
- [🍴 Forking](#-forking)
- [💻 Advanced Features](#-advanced-features)
- [🏛 License](#-license)
## 🏄 Get Started
@ -374,6 +375,16 @@ Additionally, we would also advise that your retain the text saying "Powered by
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
## 💻 Advanced Features
Ocean Market also includes a number of advanced features that are suitable for an enterprise data market, such as:
- Role based access control
- Allow and deny lists
- Free pricing
[See our seperate guide on advanced features](docs/advancedSettings.md)
## 🏛 License
```text

View File

@ -47,10 +47,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": {

29
docs/advancedSettings.md Normal file
View File

@ -0,0 +1,29 @@
# Advanced Settings
**Table of Contents**
- [Role based Access Control](#rbac-settings)
- [Allow and Deny lists](#allow-and-deny-list-settings)
- [Free Pricing](#free-pricing-settings)
## RBAC settings
- Setup and host the Ocean role based access control (RBAC) server. Follow the instructions in the [RBAC repository](https://github.com/oceanprotocol/RBAC-Server)
- The RBAC server can store roles in [Keycloak](https://www.keycloak.org/) or a json file.
- In your .env file, set the value of the `GATSBY_RBAC_URL` environmental variable to the URL of the Ocean RBAC server that you have hosted, e.g. `GATSBY_RBAC_URL= "http://localhost:3000"`
- Users of your marketplace will now require the correct role ("user", "consumer", "publisher") to access features in your marketplace. The market will check the role that has been allocated to the user based on the address that they have connected to the market with.
- The following features have been wrapped in the `Permission` component and will be restricted once the `GATSBY_RBAC_URL` has been defined:
- Viewing or searching datasets requires the user to have permison to `browse`
- Purchasing or trading a datatoken, or adding liquidity to a pool require the user to have permison to `consume`
- Publishing a dataset requires the user to have permison to `publish`
- You can change the permission resrictions by either removing the `Permission` component or passing in a different eventType prop e.g. `<Permission eventType="browse">`.
## Allow and Deny List Settings
- To enable allow and deny lists you need to add the following environmental variable to your .env file: `GATSBY_ALLOW_ADVANCED_SETTINGS="true"`
- Publishers in your market will now have the ability to restrict who can consume their datasets.
## Free Pricing Settings
- To allow publishers to set pricing as "Free" you need to add the following environmental variable to your .env file: `GATSBY_ALLOW_FREE_PRICING="true"`
- This allocates the datatokens to the [dispenser contract](https://github.com/oceanprotocol/contracts/blob/main/contracts/dispenser/Dispenser.sol) which dispenses data tokens to users for free. Publishers in your market will now be able to offer their datasets to users for free (excluding gas costs).

25638
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@
"@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "^5.15.0",
"@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.15.1",
"@oceanprotocol/lib": "^0.16.1",
"@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^4.0.4",
"@sindresorhus/slugify": "^2.1.0",

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,15 +42,20 @@ export default function PriceUnit({
return (
<div className={styleClasses}>
<div>
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
<span className={styles.symbol}>{symbol || 'OCEAN'}</span>
{type && type === 'pool' && (
<Badge label="pool" className={styles.badge} />
)}
</div>
{conversion && <Conversion price={price} />}
{type && type === 'free' ? (
<div> Free </div>
) : (
<>
<div>
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
<span className={styles.symbol}>{symbol || 'OCEAN'}</span>
{type && type === 'pool' && (
<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

@ -23,7 +23,11 @@ export default function SearchBar({
e.preventDefault()
if (value === '') value = ' '
const urlEncodedValue = encodeURIComponent(value)
const url = await addExistingParamsToUrl(location, 'text')
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=${urlEncodedValue}`)
}
@ -31,7 +35,11 @@ export default function SearchBar({
const searchParams = new URLSearchParams(window.location.href)
const text = searchParams.get('text')
if (text !== ('' || undefined || null)) {
const url = await addExistingParamsToUrl(location, 'text')
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=%20`)
}
}

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

@ -13,6 +13,7 @@ import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing'
import { useConsume } from '../../../hooks/useConsume'
import ButtonBuy from '../../atoms/ButtonBuy'
import { secondsToString } from '../../../utils/metadata'
import AlgorithmDatasetsListForCompute from '../AssetContent/AlgorithmDatasetsListForCompute'
import styles from './Consume.module.css'
@ -156,10 +157,11 @@ export default function Consume({
dtSymbol={ddo.dataTokenInfo?.symbol}
dtBalance={dtBalance}
onClick={handleConsume}
assetTimeout={assetTimeout}
assetTimeout={secondsToString(parseInt(assetTimeout))}
assetType={type}
stepText={consumeStepText || pricingStepText}
isLoading={pricingIsLoading || isLoading}
priceType={price?.type}
/>
)

View File

@ -16,6 +16,10 @@ import {
} from '../../../../models/FormEditCredential'
import DebugEditCredential from './DebugEditAdvancedSettings'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import {
setMinterToDispenser,
setMinterToPublisher
} from '../../../../utils/freePrice'
const contentQuery = graphql`
query EditAvanceSettingsQuery {
@ -68,7 +72,7 @@ export default function EditAdvancedSettings({
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { metadata, ddo, refreshDdo } = useAsset()
const { metadata, ddo, refreshDdo, price } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const { appConfig } = useSiteMetadata()
@ -82,6 +86,16 @@ export default function EditAdvancedSettings({
resetForm: () => void
) {
try {
if (price.type === 'free') {
const tx = await setMinterToPublisher(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
let newDdo: DDO
newDdo = await ocean.assets.updateCredentials(
ddo,
@ -103,6 +117,15 @@ export default function EditAdvancedSettings({
Logger.error(content.form.error)
return
} else {
if (price.type === 'free') {
const tx = await setMinterToDispenser(
ocean,
ddo.dataToken,
accountId,
setError
)
if (!tx) return
}
setSuccess(content.form.success)
resetForm()
}

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

@ -123,11 +123,10 @@ export default function Graph(): ReactElement {
const { price } = useAsset()
const [lastBlock, setLastBlock] = useState(0)
const [lastBlock, setLastBlock] = useState<number>(0)
const [priceHistory, setPriceHistory] = useState([])
const [liquidityHistory, setLiquidityHistory] = useState([])
const [timestamps, setTimestamps] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [graphData, setGraphData] = useState<ChartData>()
@ -156,7 +155,6 @@ export default function Graph(): ReactElement {
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
})
]
setTimestamps(latestTimestamps)
const latestLiquidtyHistory = [
@ -170,17 +168,20 @@ export default function Graph(): ReactElement {
...priceHistory,
...data.poolTransactions.map((item) => item.spotPrice)
]
setPriceHistory(latestPriceHistory)
if (data.poolTransactions.length > 0) {
const newBlock =
data.poolTransactions[data.poolTransactions.length - 1].block
if (newBlock === lastBlock) return
setLastBlock(
data.poolTransactions[data.poolTransactions.length - 1].block
)
refetch()
} else {
setIsLoading(false)
setGraphData({
labels: timestamps.slice(0),
labels: latestTimestamps.slice(0),
datasets: [
{
...lineStyle,
@ -194,6 +195,7 @@ export default function Graph(): ReactElement {
}
]
})
setIsLoading(false)
}
}, [data, graphType])

View File

@ -12,7 +12,7 @@ import { useAsset } from '../../../providers/Asset'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import Web3Feedback from '../../molecules/Web3Feedback'
import { getFileInfo } from '../../../utils/provider'
import { fileinfo, getFileInfo } from '../../../utils/provider'
import axios from 'axios'
export default function AssetActions(): ReactElement {
@ -27,29 +27,27 @@ export default function AssetActions(): ReactElement {
const isCompute = Boolean(ddo?.findServiceByType('compute'))
useEffect(() => {
if (!config) return
const source = axios.CancelToken.source()
async function initFileInfo() {
setFileIsLoading(true)
try {
const fileInfo = await getFileInfo(
DID.parse(`${ddo.id}`),
config.providerUri,
source.token
)
setFileMetadata(fileInfo.data[0])
} catch (error) {
Logger.error(error.message)
} finally {
setFileIsLoading(false)
}
}
initFileInfo()
return () => {
source.cancel()
}
const { attributes } = ddo.findServiceByType('metadata')
setFileMetadata(attributes.main.files[0])
// !!!!! do not remove this, we will enable this again after fileInfo endpoint is fixed !!!
// if (!config) return
// const source = axios.CancelToken.source()
// async function initFileInfo() {
// setFileIsLoading(true)
// try {
// const fileInfo = await getFileInfo(
// DID.parse(`${ddo.id}`),
// config.providerUri,
// source.token
// )
// setFileMetadata(fileInfo.data[0])
// } catch (error) {
// Logger.error(error.message)
// } finally {
// setFileIsLoading(false)
// }
// }
// initFileInfo()
}, [config, ddo.id])
// Get and set user DT balance
@ -72,6 +70,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,17 +40,27 @@ export default function Price({
<div className={styles.price}>
<div className={styles.grid}>
<div className={styles.form}>
<Input
value={field.value}
name="price"
type="number"
prefix="OCEAN"
min="1"
{...field}
additionalComponent={
<Conversion price={field.value} className={styles.conversion} />
}
/>
{free ? (
<Input
value="0"
name="price"
type="number"
prefix="OCEAN"
readOnly
/>
) : (
<Input
value={field.value}
name="price"
type="number"
prefix="OCEAN"
min="1"
{...field}
additionalComponent={
<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

@ -61,7 +61,11 @@ const columns = [
{
name: 'Finished',
selector: function getTimeRow(row: ComputeJobMetaData) {
return <Time date={row.dateFinished} isUnix relative />
return row.dateFinished ? (
<Time date={row.dateFinished} isUnix relative />
) : (
''
)
}
},
{
@ -105,7 +109,7 @@ export default function ComputeJobs(): ReactElement {
const { accountId } = useWeb3()
const [isLoading, setIsLoading] = useState(true)
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const { data } = useQuery<ComputeOrders>(getComputeOrders, {
const { data, refetch } = useQuery<ComputeOrders>(getComputeOrders, {
variables: {
user: accountId?.toLowerCase()
}
@ -116,6 +120,7 @@ export default function ComputeJobs(): ReactElement {
setIsLoading(true)
await refetch()
const dtList = []
const computeJobs: ComputeJobMetaData[] = []
for (let i = 0; i < data.tokenOrders.length; i++) {

View File

@ -85,7 +85,7 @@ function SectionQueryResult({
appConfig.metadataCacheUri,
source.token
)
if (result.totalResults <= 15) {
if (queryData && result.totalResults > 0 && result.totalResults <= 15) {
const searchDIDs = queryData.split(' ')
const sortedAssets = sortElements(result.results, searchDIDs)
// We take more assets than we need from the subgraph (to make sure
@ -95,7 +95,6 @@ function SectionQueryResult({
sortedAssets.splice(sortedAssets.length - overflow, overflow)
result.results = sortedAssets
}
if (result.results.length === 0) return
setResult(result)
setLoading(false)
} catch (error) {

View File

@ -25,7 +25,7 @@ export default function FilterPrice({
const [serviceSelections, setServiceSelections] = useState<string[]>([])
async function applyServiceFilter(filterBy: string) {
let urlLocation = await addExistingParamsToUrl(location, 'serviceType')
let urlLocation = await addExistingParamsToUrl(location, ['serviceType'])
if (filterBy && location.search.indexOf('&serviceType') === -1) {
urlLocation = `${urlLocation}&serviceType=${filterBy}`
}
@ -59,7 +59,7 @@ export default function FilterPrice({
}
async function applyClearFilter() {
let urlLocation = await addExistingParamsToUrl(location, 'serviceType')
let urlLocation = await addExistingParamsToUrl(location, ['serviceType'])
urlLocation = `${urlLocation}`

View File

@ -32,7 +32,6 @@ export default function SearchPage({
useEffect(() => {
if (!appConfig.metadataCacheUri) return
async function initSearch() {
setLoading(true)
setTotalResults(undefined)
@ -66,7 +65,7 @@ export default function SearchPage({
<Permission eventType="browse">
<>
<div className={styles.search}>
{(text || owner) && (
{(text || owner || tags) && (
<SearchBar initialValue={(text || owner) as string} />
)}
<div className={styles.row}>

View File

@ -11,7 +11,10 @@ import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const sortItems = [{ display: 'Published', value: SortTermOptions.Created }]
const sortItems = [
// { display: 'Relevance', value: SortTermOptions.Relevance },
{ display: 'Published', value: SortTermOptions.Created }
]
export default function Sort({
sortType,
@ -31,10 +34,11 @@ export default function Sort({
async function sortResults(sortBy?: string, direction?: string) {
let urlLocation: string
if (sortBy) {
urlLocation = await addExistingParamsToUrl(location, ['sort'])
urlLocation = `${urlLocation}&sort=${sortBy}`
setSortType(sortBy)
} else if (direction) {
urlLocation = await addExistingParamsToUrl(location, 'sortOrder')
urlLocation = await addExistingParamsToUrl(location, ['sortOrder'])
urlLocation = `${urlLocation}&sortOrder=${direction}`
setSortDirection(direction)
}

View File

@ -1,12 +1,10 @@
import {
SearchQuery,
QueryResult
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import { MetadataCache, Logger } from '@oceanprotocol/lib'
import queryString from 'query-string'
export const SortTermOptions = {
Created: 'created'
Created: 'created',
Relevance: '_score'
} as const
type SortTermOptions = typeof SortTermOptions[keyof typeof SortTermOptions]
@ -39,8 +37,11 @@ function addTypeFilterToQuery(sortTerm: string, typeFilter: string): string {
return sortTerm
}
function getSortType(): string {
const sortTerm = SortTermOptions.Created
function getSortType(sortParam: string): string {
const sortTerm =
sortParam === SortTermOptions.Created
? SortTermOptions.Created
: SortTermOptions.Relevance
return sortTerm
}
@ -54,9 +55,11 @@ export function getSearchQuery(
sort?: string,
sortOrder?: string,
serviceType?: string
): SearchQuery {
const sortTerm = getSortType()
): any {
const sortTerm = getSortType(sort)
const sortValue = sortOrder === SortValueOptions.Ascending ? 1 : -1
const emptySearchTerm = text === undefined || text === ''
let searchTerm = owner
? `(publicKey.owner:${owner})`
: tags
@ -67,41 +70,84 @@ export function getSearchQuery(
`(service.attributes.additionalInformation.categories:\"${categories}\")`
: text || ''
// HACK: resolves the case sensitivity related to dataTokenInfo.symbol
searchTerm = '*' + searchTerm.toUpperCase() + '*'
searchTerm = searchTerm.trim()
let modifiedSearchTerm = searchTerm.split(' ').join(' OR ').trim()
modifiedSearchTerm = addTypeFilterToQuery(modifiedSearchTerm, serviceType)
searchTerm = addTypeFilterToQuery(searchTerm, serviceType)
const prefixedSearchTerm =
emptySearchTerm && searchTerm
? searchTerm
: !emptySearchTerm && searchTerm
? '*' + searchTerm + '*'
: '**'
return {
page: Number(page) || 1,
offset: Number(offset) || 21,
query: {
query_string: {
query: `${searchTerm} -isInPurgatory:true`,
fields: [
'dataTokenInfo.name',
'dataTokenInfo.symbol',
'service.attributes.main.name',
'service.attributes.main.author',
'service.attributes.additionalInformation.description'
],
default_operator: 'AND'
bool: {
must: [
{
bool: {
should: [
{
query_string: {
query: `${modifiedSearchTerm}`,
fields: [
'id',
'publicKey.owner',
'dataToken',
'dataTokenInfo.name',
'dataTokenInfo.symbol',
'service.attributes.main.name^10',
'service.attributes.main.author',
'service.attributes.additionalInformation.description',
'service.attributes.additionalInformation.tags'
],
minimum_should_match: '2<75%',
phrase_slop: 2,
boost: 5
}
},
{
match_phrase: {
content: {
query: `${searchTerm}`,
boost: 10
}
}
},
{
query_string: {
query: `${prefixedSearchTerm}`,
fields: [
'id',
'publicKey.owner',
'dataToken',
'dataTokenInfo.name',
'dataTokenInfo.symbol',
'service.attributes.main.name',
'service.attributes.main.author',
'service.attributes.additionalInformation.description',
'service.attributes.additionalInformation.tags'
],
default_operator: 'AND'
}
}
]
}
},
{
term: {
isInPurgatory: false
}
}
]
}
// ...(owner && { 'publicKey.owner': [owner] }),
// ...(tags && { tags: [tags] }),
// ...(categories && { categories: [categories] })
},
sort: {
[sortTerm]: sortValue
}
// Something in ocean.js is weird when using 'tags: [tag]'
// which is the only way the query actually returns desired results.
// But it doesn't follow 'SearchQuery' interface so we have to assign
// it here.
// } as SearchQuery
// And the next hack,
// nativeSearch is not implmeneted on ocean.js typings
}
}
@ -123,9 +169,9 @@ export async function getResults(
text,
owner,
tags,
categories,
page,
offset,
categories,
sort,
sortOrder,
serviceType
@ -143,25 +189,20 @@ export async function getResults(
sortOrder,
serviceType
)
const queryResult = await metadataCache.queryMetadata(searchQuery)
return queryResult
}
export async function addExistingParamsToUrl(
location: Location,
excludedParam: string,
secondExcludedParam?: string
excludedParams: string[]
): Promise<string> {
const parsed = queryString.parse(location.search)
let urlLocation = '/search?'
if (Object.keys(parsed).length > 0) {
for (const querryParam in parsed) {
if (
querryParam !== excludedParam &&
querryParam !== secondExcludedParam
) {
if (querryParam === 'page' && excludedParam === 'text') {
if (!excludedParams.includes(querryParam)) {
if (querryParam === 'page' && excludedParams.includes('text')) {
Logger.log('remove page when starting a new search')
} else {
const value = parsed[querryParam]
@ -170,7 +211,10 @@ export async function addExistingParamsToUrl(
}
}
} else {
urlLocation = `${urlLocation}sort=${SortTermOptions.Created}&sortOrder=${SortValueOptions.Descending}&`
// sort should be relevance when fixed in aqua
urlLocation = `${urlLocation}sort=${encodeURIComponent(
SortTermOptions.Created
)}&sortOrder=${SortValueOptions.Descending}&`
}
urlLocation = urlLocation.slice(0, -1)
return urlLocation

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 fixedPrice set dt to max amount
if (!isPool) dtAmount = 1000
await mint(`${dtAmount}`, ddo)
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

@ -29,6 +29,7 @@ interface UseSiteMetadata {
portisId: string
allowFixedPricing: string
allowDynamicPricing: string
allowFreePricing: string
allowAdvancedSettings: string
credentialType: string
}
@ -65,6 +66,7 @@ const query = graphql`
portisId
allowFixedPricing
allowDynamicPricing
allowFreePricing
allowAdvancedSettings
credentialType
}

View File

@ -27,7 +27,9 @@ function getCredentialList(
const credentialByType = credential.find(
(credential) => credential.type === credentialType
)
return credentialByType.value && credentialByType.value.length > 0
return credentialByType &&
credentialByType.value &&
credentialByType.value.length > 0
? credentialByType.value
: []
}

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

@ -1,6 +1,8 @@
const cleanupContentType = (contentType: string): string => {
// strip away the `charset=utf-8`
const contentSplit = contentType.split(';')[0]
// strip away the 'application/' part
const contentTypeSplit = contentType.split('/')[1]
const contentTypeSplit = contentSplit.split('/')[1]
if (!contentTypeSplit) return contentType

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

@ -15,9 +15,12 @@ export function getOceanConfig(
const config = new ConfigHelper().getConfig(
network,
network === 'polygon' ||
network === 137 ||
network === 'moonbeamalpha' ||
network === 1287
network === 1287 ||
network === 'bsc' ||
network === 56 ||
network === 'gaiaxtestnet' ||
network === 2021000
? undefined
: process.env.GATSBY_INFURA_PROJECT_ID
)

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

View File

@ -44,6 +44,9 @@ export function getNetworkDisplayName(
case 8996:
displayName = 'Development'
break
case 2021000:
displayName = 'GAIA-X'
break
default:
displayName = `${data.chain} ${
data.network === 'mainnet' ? '' : data.network