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

pull from origin main

This commit is contained in:
Jamie Hewitt 2021-05-26 17:31:58 +03:00
commit 20361578ca
49 changed files with 1119 additions and 765 deletions

View File

@ -25,6 +25,7 @@
- [🛳 Production](#-production)
- [⬆️ Deployment](#-deployment)
- [💖 Contributing](#-contributing)
- [🍴 Forking](#-forking)
- [🏛 License](#-license)
## 🏄 Get Started
@ -358,6 +359,18 @@ We welcome contributions in form of bug reports, feature requests, code changes,
- [Code of Conduct →](https://docs.oceanprotocol.com/concepts/code-of-conduct/)
- [Reporting Vulnerabilities →](https://docs.oceanprotocol.com/concepts/vulnerabilities/)
## 🍴 Forking
We encourage you to fork this repository and create your own data marketplace. When you publish your forked version of this market there are a few elements that you are required to change for copyright reasons:
- The typeface is copyright protected and needs to be changed unless you purchase a license for it.
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
## 🏛 License
```text

View File

@ -28,8 +28,8 @@
"label": "Docker Image",
"placeholder": "e.g. python3.7",
"help": "Please select an image to run your algorithm.",
"type": "select",
"options": ["node:latest", "python:latest", "custom image"],
"type": "boxSelection",
"options": [],
"required": true
},
{

View File

@ -34,7 +34,7 @@
"name": "access",
"label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "select",
"type": "boxSelection",
"options": ["Download", "Compute"],
"required": true
},

2
package-lock.json generated
View File

@ -18101,7 +18101,7 @@
}
},
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1a27c59c15ab1e95ee8e5c4ed6ad814c49cc439e",
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1ce6a1d64235fabe2aaf827fd606def55693508f",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.11.8",

View File

@ -0,0 +1,71 @@
.button {
display: inline-block;
position: relative;
min-width: auto;
}
.button:hover,
.button:focus {
transform: none;
}
.logoWrap {
position: relative;
display: inline-block;
z-index: 1;
}
.logoWrap::before {
content: '+';
color: var(--color-secondary);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
font-size: 1.25em;
position: absolute;
right: 0.05em;
top: 0.05em;
line-height: 0;
}
.logo {
width: 1.6em;
height: 1.6em;
display: inline-block;
margin-bottom: -0.35em;
border-radius: 50%;
border: 0.065rem solid var(--color-secondary);
margin-right: calc(var(--spacer) / 10);
transition: 0.2s ease-out;
}
.button:hover .logo,
.button:focus .logo {
border-color: var(--color-primary);
}
.button:hover .logoWrap::before,
.button:focus .logoWrap::before {
color: var(--color-primary);
}
.text {
display: inline-block;
position: relative;
}
.minimal .text {
opacity: 0;
transform: translate3d(-1rem, 0, 0);
transition: 0.2s ease-out;
z-index: 0;
white-space: pre;
position: absolute;
left: 100%;
top: 0.15rem;
}
.minimal:hover .text,
.minimal:focus .text {
opacity: 1;
transform: translate3d(0, 0, 0);
}

View File

@ -0,0 +1,53 @@
import React, { ReactElement } from 'react'
import classNames from 'classnames/bind'
import { addTokenToWallet } from '../../utils/web3'
import { useWeb3 } from '../../providers/Web3'
import Button from './Button'
import styles from './AddToken.module.css'
const cx = classNames.bind(styles)
export default function AddToken({
address,
symbol,
logo,
text,
className,
minimal
}: {
address: string
symbol: string
logo: string // needs to be a remote image
text?: string
className?: string
minimal?: boolean
}): ReactElement {
const { web3Provider } = useWeb3()
const styleClasses = cx({
button: true,
minimal: minimal,
[className]: className
})
async function handleAddToken() {
if (!web3Provider) return
await addTokenToWallet(web3Provider, address, symbol, logo)
}
return (
<Button
className={styleClasses}
style="text"
size="small"
onClick={handleAddToken}
>
<span className={styles.logoWrap}>
<img src={logo} className={styles.logo} width="16" height="16" />
</span>
<span className={styles.text}>{text || `Add ${symbol}`}</span>
</Button>
)
}

View File

@ -1,20 +1,30 @@
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import { ReactComponent as External } from '../../images/external.svg'
import styles from './ExplorerLink.module.css'
import classNames from 'classnames/bind'
import { ConfigHelperConfig } from '@oceanprotocol/lib'
import { useOcean } from '../../providers/Ocean'
import styles from './ExplorerLink.module.css'
const cx = classNames.bind(styles)
export default function ExplorerLink({
path,
children
children,
className
}: {
networkId: number
path: string
children: ReactNode
className?: string
}): ReactElement {
const { config } = useOcean()
const [url, setUrl] = useState<string>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => {
setUrl((config as ConfigHelperConfig).explorerUri)
}, [config])
@ -25,7 +35,7 @@ export default function ExplorerLink({
title={`View on ${(config as ConfigHelperConfig).explorerUri}`}
target="_blank"
rel="noreferrer"
className={styles.link}
className={styleClasses}
>
{children} <External />
</a>

View File

@ -4,6 +4,9 @@ import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../../molecules/FormFields/FilesInput'
import Terms from '../../molecules/FormFields/Terms'
import BoxSelection, {
BoxSelectionOption
} from '../../molecules/FormFields/BoxSelection'
import Datatoken from '../../molecules/FormFields/Datatoken'
import classNames from 'classnames/bind'
import AssetSelection, {
@ -91,7 +94,7 @@ export default function InputElement({
id={slugify(option)}
type={type}
name={name}
checked={props.defaultChecked}
defaultChecked={props.defaultChecked}
{...props}
/>
<label className={styles.radioLabel} htmlFor={slugify(option)}>
@ -125,6 +128,15 @@ export default function InputElement({
return <Datatoken name={name} {...field} {...props} />
case 'terms':
return <Terms name={name} options={options} {...field} {...props} />
case 'boxSelection':
return (
<BoxSelection
name={name}
options={(options as unknown) as BoxSelectionOption[]}
{...field}
{...props}
/>
)
default:
return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -68,7 +68,6 @@ export default function Publisher({
>
{name}
</Link>
<div className={styles.links}>
{' — '}
{profile && (

View File

@ -2,9 +2,10 @@ import AssetTeaser from '../molecules/AssetTeaser'
import * as React from 'react'
import { DDO } from '@oceanprotocol/lib'
import ddo from '../../../tests/unit/__fixtures__/ddo'
import { AssetListPrices } from '../../utils/subgraph'
export default {
title: 'Molecules/Asset Teaser'
}
export const Default = () => <AssetTeaser ddo={ddo as DDO} />
export const Default = () => <AssetTeaser ddo={ddo as DDO} price={undefined} />

View File

@ -3,7 +3,7 @@ import { Link } from 'gatsby'
import Dotdotdot from 'react-dotdotdot'
import Price from '../atoms/Price'
import styles from './AssetTeaser.module.css'
import { DDO } from '@oceanprotocol/lib'
import { DDO, BestPrice } from '@oceanprotocol/lib'
import removeMarkdown from 'remove-markdown'
import Publisher from '../atoms/Publisher'
import Time from '../atoms/Time'
@ -11,9 +11,13 @@ import AssetType from '../atoms/AssetType'
declare type AssetTeaserProps = {
ddo: DDO
price: BestPrice
}
const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
const AssetTeaser: React.FC<AssetTeaserProps> = ({
ddo,
price
}: AssetTeaserProps) => {
const { attributes } = ddo.findServiceByType('metadata')
const { name, type } = attributes.main
const { dataTokenInfo } = ddo
@ -47,7 +51,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
</div>
<footer className={styles.foot}>
<Price price={ddo.price} small />
<Price price={price} small />
<p className={styles.date}>
<Time date={ddo?.created} relative />
</p>

View File

@ -0,0 +1,57 @@
.boxSelectionsWrapper {
display: flex;
justify-content: space-between;
gap: 0 calc(var(--spacer) / 4);
}
.boxSelectionsWrapper > div {
width: 100%;
}
.boxSelection {
display: block;
flex: 1 1 0px;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
color: var(--color-secondary);
cursor: pointer;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.title {
font-weight: var(--font-weight-bold);
text-align: center;
}
.boxSelectionsWrapper input[type='radio'] {
position: fixed;
opacity: 0;
pointer-events: none;
}
input[type='radio']:checked + label {
color: var(--font-color-text);
border-color: var(--color-secondary);
}
.boxSelection svg {
width: var(--font-size-h4);
height: var(--font-size-h4);
fill: currentColor;
margin-bottom: calc(var(--spacer) / 5);
}
.boxSelectionsWrapper label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
color: var(--color-secondary);
font-weight: normal;
}

View File

@ -0,0 +1,70 @@
import React, { ChangeEvent } from 'react'
import classNames from 'classnames/bind'
import Loader from '../../atoms/Loader'
import styles from './BoxSelection.module.css'
const cx = classNames.bind(styles)
export interface BoxSelectionOption {
name: string
checked: boolean
title: JSX.Element | string
icon?: JSX.Element
text?: JSX.Element | string
}
export default function BoxSelection({
name,
options,
disabled,
handleChange,
...props
}: {
name: string
options: BoxSelectionOption[]
disabled?: boolean
handleChange?: (event: ChangeEvent<HTMLInputElement>) => void
}): JSX.Element {
const styleClassesWrapper = cx({
boxSelectionsWrapper: true,
[styles.disabled]: disabled
})
const styleClassesInput = cx({
input: true,
radio: true
})
return (
<div className={styleClassesWrapper}>
{!options ? (
<Loader />
) : (
options.map((value: BoxSelectionOption) => (
<div key={value.name}>
<input
id={value.name}
type="radio"
className={styleClassesInput}
defaultChecked={value.checked}
onChange={(event) => handleChange(event)}
{...props}
disabled={disabled}
value={value.name}
name={name}
/>
<label
className={`${styles.boxSelection} ${styles.label}`}
htmlFor={value.name}
title={value.name}
>
{value.icon}
<span className={styles.title}>{value.title}</span>
{value.text}
</label>
</div>
))
)}
</div>
)
}

View File

@ -2,7 +2,6 @@ import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '../../providers/Web3'
import { addCustomNetwork, NetworkObject } from '../../utils/web3'
import { getOceanConfig } from '../../utils/ocean'
import { getProviderInfo } from 'web3modal'
import { useOcean } from '../../providers/Ocean'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import AnnouncementBanner, {
@ -19,7 +18,7 @@ const networkMatic: NetworkObject = {
}
export default function NetworkBanner(): ReactElement {
const { web3Provider } = useWeb3()
const { web3Provider, web3ProviderInfo } = useWeb3()
const { config, connect } = useOcean()
const { announcement } = useSiteMetadata()
@ -51,10 +50,9 @@ export default function NetworkBanner(): ReactElement {
}
useEffect(() => {
if (!web3Provider && !config) return
if (!web3ProviderInfo || (!web3Provider && !config)) return
const providerInfo = getProviderInfo(web3Provider)
switch (providerInfo?.name) {
switch (web3ProviderInfo.name) {
case 'Web3':
if (config.networkId !== 137) {
setText(announcement.main)
@ -80,7 +78,7 @@ export default function NetworkBanner(): ReactElement {
setAction(undefined)
}
}
}, [web3Provider, config, announcement])
}, [web3Provider, web3ProviderInfo, config, announcement])
return <AnnouncementBanner text={text} action={action} />
}

View File

@ -30,3 +30,7 @@
color: var(--font-color-text);
border-color: var(--color-secondary);
}
.appearances div[class*='boxSelectionsWrapper'] {
padding-bottom: calc(var(--spacer) / 8);
}

View File

@ -1,42 +1,44 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, ChangeEvent } from 'react'
import { DarkMode } from 'use-dark-mode'
import Button from '../../atoms/Button'
import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label'
import styles from './Appearance.module.css'
import { ReactComponent as Moon } from '../../../images/moon.svg'
import { ReactComponent as Sun } from '../../../images/sun.svg'
const buttons = ['Light', 'Dark']
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import styles from './Appearance.module.css'
export default function Appearance({
darkMode
}: {
darkMode: DarkMode
}): ReactElement {
return (
<li>
<Label htmlFor="">Appearance</Label>
<div className={styles.buttons}>
{buttons.map((button) => {
const isDark = button === 'Dark'
const selected =
(isDark && darkMode.value) || (!isDark && !darkMode.value)
const options: BoxSelectionOption[] = [
{
name: 'Light',
checked: !darkMode.value,
title: 'Light',
icon: <Sun />
},
{
name: 'Dark',
checked: darkMode.value,
title: 'Dark',
icon: <Moon />
}
]
return (
<Button
key={button}
className={`${styles.button} ${selected ? styles.selected : ''}`}
size="small"
style="text"
onClick={() => (isDark ? darkMode.enable() : darkMode.disable())}
>
{isDark ? <Moon /> : <Sun />}
{button}
</Button>
)
})}
</div>
function handleChange(event: ChangeEvent<HTMLInputElement>) {
event.target.value === 'Dark' ? darkMode.enable() : darkMode.disable()
}
return (
<li className={styles.appearances}>
<Label htmlFor="">Appearance</Label>
<BoxSelection
options={options}
name="appearanceMode"
handleChange={handleChange}
/>
<FormHelp>Defaults to your OS setting, select to override.</FormHelp>
</li>
)

View File

@ -17,3 +17,14 @@
.selected {
composes: selected from './Appearance.module.css';
}
.chains div[class*='boxSelectionsWrapper'] {
display: grid;
gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8);
}
.chains label[class*='boxSelection'] {
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
}

View File

@ -1,53 +1,58 @@
import { ConfigHelperConfig } from '@oceanprotocol/lib'
import React, { ReactElement } from 'react'
import React, { ReactElement, ChangeEvent } from 'react'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import { getOceanConfig } from '../../../utils/ocean'
import Button from '../../atoms/Button'
import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import styles from './Chain.module.css'
export default function Chain(): ReactElement {
const { web3 } = useWeb3()
const { config, connect } = useOcean()
async function connectOcean(networkName: string) {
const config = getOceanConfig(networkName)
async function connectOcean(event: ChangeEvent<HTMLInputElement>) {
const config = getOceanConfig(event.target.value)
await connect(config)
}
const chains = [
{ name: 'ETH', oceanConfig: 'mainnet', label: 'Mainnet' },
{ name: 'Polygon/Matic', oceanConfig: 'polygon', label: 'Mainnet' },
{ name: 'Moonbase Alpha', oceanConfig: 'moonbeamalpha', label: 'Testnet' }
function isNetworkSelected(oceanConfig: string) {
return (config as ConfigHelperConfig).network === oceanConfig
}
const options: BoxSelectionOption[] = [
{
name: 'mainnet',
checked: isNetworkSelected('mainnet'),
title: 'ETH',
text: 'Mainnet'
},
{
name: 'polygon',
checked: isNetworkSelected('polygon'),
title: 'Polygon/Matic',
text: 'Mainnet'
},
{
name: 'moonbeamalpha',
checked: isNetworkSelected('moonbeamalpha'),
title: 'Moonbase Alpha',
text: 'Testnet'
}
]
// TODO: to fully solve https://github.com/oceanprotocol/market/issues/432
// there are more considerations for users with a wallet connected (wallet network vs. setting network).
// For now, only show the setting for non-wallet users.
return !web3 ? (
<li>
<li className={styles.chains}>
<Label htmlFor="">Chain</Label>
<div className={styles.buttons}>
{chains.map((button) => {
const selected =
(config as ConfigHelperConfig).network === button.oceanConfig
return (
<Button
key={button.name}
className={`${styles.button} ${selected ? styles.selected : ''}`}
size="small"
style="text"
onClick={() => connectOcean(button.oceanConfig)}
>
{button.name}
<span>{button.label}</span>
</Button>
)
})}
</div>
<BoxSelection
options={options}
name="chain"
handleChange={connectOcean}
/>
<FormHelp>Switch the data source for the interface.</FormHelp>
</li>
) : null

View File

@ -42,7 +42,7 @@
justify-content: space-between;
}
.actions span {
.walletLogoWrap {
display: block;
}
@ -84,3 +84,7 @@
.walletInfo button {
margin-top: calc(var(--spacer) / 5) !important;
}
.addToken {
margin-left: 0.3rem;
}

View File

@ -1,33 +1,29 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Button from '../../atoms/Button'
import styles from './Details.module.css'
import { useOcean } from '../../../providers/Ocean'
import Web3Feedback from './Feedback'
import { getProviderInfo, IProviderInfo } from 'web3modal'
import Conversion from '../../atoms/Price/Conversion'
import { formatCurrency } from '@coingecko/cryptoformat'
import { useOcean } from '../../../providers/Ocean'
import { useUserPreferences } from '../../../providers/UserPreferences'
import Button from '../../atoms/Button'
import AddToken from '../../atoms/AddToken'
import Conversion from '../../atoms/Price/Conversion'
import { useWeb3 } from '../../../providers/Web3'
import { addOceanToWallet } from '../../../utils/web3'
import { Logger } from '@oceanprotocol/lib'
import Web3Feedback from './Feedback'
import styles from './Details.module.css'
export default function Details(): ReactElement {
const { web3Provider, connect, logout, networkData } = useWeb3()
const {
web3Provider,
web3ProviderInfo,
connect,
logout,
networkData
} = useWeb3()
const { balance, config } = useOcean()
const { locale } = useUserPreferences()
const [providerInfo, setProviderInfo] = useState<IProviderInfo>()
const [mainCurrency, setMainCurrency] = useState<string>()
// const [portisNetwork, setPortisNetwork] = useState<string>()
// Workaround cause getInjectedProviderName() always returns `MetaMask`
// https://github.com/oceanprotocol/market/issues/332
useEffect(() => {
if (!web3Provider) return
const providerInfo = getProviderInfo(web3Provider)
setProviderInfo(providerInfo)
}, [web3Provider])
useEffect(() => {
if (!networkData) return
@ -61,11 +57,11 @@ export default function Details(): ReactElement {
<li className={styles.actions}>
<div title="Connected provider" className={styles.walletInfo}>
<span>
<img className={styles.walletLogo} src={providerInfo?.logo} />
{providerInfo?.name}
<span className={styles.walletLogoWrap}>
<img className={styles.walletLogo} src={web3ProviderInfo?.logo} />
{web3ProviderInfo?.name}
</span>
{/* {providerInfo?.name === 'Portis' && (
{/* {web3ProviderInfo?.name === 'Portis' && (
<InputElement
name="network"
type="select"
@ -75,20 +71,17 @@ export default function Details(): ReactElement {
onChange={handlePortisNetworkChange}
/>
)} */}
{providerInfo?.name === 'MetaMask' && (
<Button
style="text"
size="small"
onClick={() => {
addOceanToWallet(config, web3Provider)
}}
>
{`Add ${config.oceanTokenSymbol}`}
</Button>
{web3ProviderInfo?.name === 'MetaMask' && (
<AddToken
address={config.oceanTokenAddress}
symbol={config.oceanTokenSymbol}
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png"
className={styles.addToken}
/>
)}
</div>
<p>
{providerInfo?.name === 'Portis' && (
{web3ProviderInfo?.name === 'Portis' && (
<Button
style="text"
size="small"

View File

@ -3,7 +3,6 @@ import {
DDO,
File as FileMetadata,
Logger,
ServiceType,
publisherTrustedAlgorithm,
BestPrice
} from '@oceanprotocol/lib'
@ -37,11 +36,8 @@ import FormStartComputeDataset from './FormComputeDataset'
import styles from './index.module.css'
import SuccessConfetti from '../../../atoms/SuccessConfetti'
import Button from '../../../atoms/Button'
import { gql, useQuery } from '@apollo/client'
import { FrePrice } from '../../../../@types/apollo/FrePrice'
import { PoolPrice } from '../../../../@types/apollo/PoolPrice'
import { secondsToString } from '../../../../utils/metadata'
import { getPreviousOrders } from '../../../../utils/subgraph'
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
const SuccessAction = () => (
<Button style="text" to="/history" size="small">
@ -49,23 +45,6 @@ const SuccessAction = () => (
</Button>
)
const freQuery = gql`
query AlgorithmFrePrice($datatoken: String) {
fixedRateExchanges(orderBy: id, where: { datatoken: $datatoken }) {
rate
id
}
}
`
const poolQuery = gql`
query AlgorithmPoolPrice($datatoken: String) {
pools(where: { datatokenAddress: $datatoken }) {
spotPrice
consumePrice
}
}
`
export default function Compute({
isBalanceSufficient,
dtBalance,
@ -95,7 +74,6 @@ export default function Compute({
)
const [algorithmDTBalance, setalgorithmDTBalance] = useState<string>()
const [algorithmPrice, setAlgorithmPrice] = useState<BestPrice>()
const [variables, setVariables] = useState({})
const [
previousAlgorithmOrderId,
setPreviousAlgorithmOrderId
@ -103,25 +81,6 @@ export default function Compute({
const [datasetTimeout, setDatasetTimeout] = useState<string>()
const [algorithmTimeout, setAlgorithmTimeout] = useState<string>()
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
refetch: refetchFre,
startPolling: startPollingFre,
data: frePrice
} = useQuery<FrePrice>(freQuery, {
variables,
skip: false
})
const {
refetch: refetchPool,
startPolling: startPollingPool,
data: poolPrice
} = useQuery<PoolPrice>(poolQuery, {
variables,
skip: false
})
/* eslint-enable @typescript-eslint/no-unused-vars */
const isComputeButtonDisabled =
isJobStarting === true || file === null || !ocean || !isBalanceSufficient
const hasDatatoken = Number(dtBalance) >= 1
@ -167,7 +126,7 @@ export default function Compute({
const algorithmQuery =
trustedAlgorithmList.length > 0 ? `(${algoQuerry}) AND` : ``
const query = {
page: 1,
offset: 500,
query: {
query_string: {
query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true`
@ -215,40 +174,10 @@ export default function Compute({
setDatasetTimeout(secondsToString(timeout))
}, [ddo])
useEffect(() => {
if (
!frePrice ||
frePrice.fixedRateExchanges.length === 0 ||
algorithmPrice.type !== 'exchange'
)
return
setAlgorithmPrice((prevState) => ({
...prevState,
value: frePrice.fixedRateExchanges[0].rate,
address: frePrice.fixedRateExchanges[0].id
}))
}, [frePrice])
useEffect(() => {
if (
!poolPrice ||
poolPrice.pools.length === 0 ||
algorithmPrice.type !== 'pool'
)
return
setAlgorithmPrice((prevState) => ({
...prevState,
value:
poolPrice.pools[0].consumePrice === '-1'
? poolPrice.pools[0].spotPrice
: poolPrice.pools[0].consumePrice
}))
}, [poolPrice])
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
if (!ddo) return
setAlgorithmPrice(ddo.price)
setVariables({ datatoken: ddo?.dataToken.toLowerCase() })
const price = await getPrice(ddo)
setAlgorithmPrice(price)
}, [])
useEffect(() => {
@ -430,7 +359,8 @@ export default function Compute({
Logger.log('[compute] Starting compute job response: ', response)
setHasPreviousDatasetOrder(true)
await checkPreviousOrders(selectedAlgorithmAsset)
await checkPreviousOrders(ddo)
setIsPublished(true)
} catch (error) {
setError('Failed to start job!')

View File

@ -44,7 +44,7 @@ export default function FormEditComputeDataset({
): Promise<AssetSelectionAsset[]> {
const source = axios.CancelToken.source()
const query = {
page: 1,
offset: 500,
query: {
query_string: {
query: `service.attributes.main.type:algorithm -isInPurgatory:true`

View File

@ -81,6 +81,8 @@ export default function Remove({
const [minOceanAmount, setMinOceanAmount] = useState<string>('0')
const [minDatatokenAmount, setMinDatatokenAmount] = useState<string>('0')
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
async function handleRemoveLiquidity() {
setIsLoading(true)
try {
@ -157,12 +159,18 @@ export default function Remove({
])
useEffect(() => {
const minOceanAmount =
(Number(amountOcean) * (100 - Number(slippage))) / 100
const minDatatokenAmount =
(Number(amountDatatoken) * (100 - Number(slippage))) / 100
setMinOceanAmount(`${minOceanAmount}`)
setMinDatatokenAmount(`${minDatatokenAmount}`)
const minOceanAmount = new Decimal(amountOcean)
.mul(new Decimal(100).minus(new Decimal(slippage)))
.dividedBy(100)
.toString()
const minDatatokenAmount = new Decimal(amountDatatoken)
.mul(new Decimal(100).minus(new Decimal(slippage)))
.dividedBy(100)
.toString()
setMinOceanAmount(minOceanAmount.slice(0, 18))
setMinDatatokenAmount(minDatatokenAmount.slice(0, 18))
}, [slippage, amountOcean, amountDatatoken, isAdvanced])
// Set amountPoolShares based on set slider value
@ -173,9 +181,9 @@ export default function Remove({
const amountPoolShares = new Decimal(e.target.value)
.dividedBy(100)
.mul(new Decimal(poolTokens))
.toPrecision(18) // in some cases the returned value contain more than 18 digits which break conversion to wei
.toString()
setAmountPoolShares(`${amountPoolShares}`)
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
}
function handleMaxButton(e: ChangeEvent<HTMLInputElement>) {
@ -185,9 +193,9 @@ export default function Remove({
const amountPoolShares = new Decimal(amountMaxPercent)
.dividedBy(100)
.mul(new Decimal(poolTokens))
.toPrecision(18)
.toString()
setAmountPoolShares(`${amountPoolShares}`)
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
}
function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) {

View File

@ -7,7 +7,7 @@ import { useAsset } from '../../../../providers/Asset'
export default function Transactions(): ReactElement {
const [open, setOpen] = useState(false)
const { ddo } = useAsset()
const { price } = useAsset()
function handleClick() {
setOpen(!open)
}
@ -29,7 +29,7 @@ export default function Transactions(): ReactElement {
</Button>
</h3>
{open === true && (
<PoolTransactions poolAddress={ddo.price?.address} minimal />
<PoolTransactions poolAddress={price?.address} minimal />
)}
</div>
)

View File

@ -95,8 +95,8 @@ export default function Pool(): ReactElement {
const [refreshPool, setRefreshPool] = useState(false)
const { data: dataLiquidity } = useQuery<PoolLiquidity>(poolLiquidityQuery, {
variables: {
id: ddo.price.address.toLowerCase(),
shareId: `${ddo.price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}`
id: price.address.toLowerCase(),
shareId: `${price.address.toLowerCase()}-${ddo.publicKey[0].owner.toLowerCase()}`
},
pollInterval: 5000
})
@ -170,7 +170,8 @@ export default function Pool(): ReactElement {
const totalUserLiquidityInOcean =
userLiquidity?.ocean + userLiquidity?.datatoken * price?.value
setTotalUserLiquidityInOcean(totalUserLiquidityInOcean)
const totalLiquidityInOcean = price?.ocean + price?.datatoken * price?.value
const totalLiquidityInOcean =
Number(price?.ocean) + Number(price?.datatoken) * Number(price?.value)
setTotalLiquidityInOcean(totalLiquidityInOcean)
}, [userLiquidity, price, poolTokens, totalPoolTokens])

View File

@ -19,7 +19,6 @@ export default function AssetActions(): ReactElement {
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
const isCompute = Boolean(ddo?.findServiceByType('compute'))
// Get and set user DT balance
@ -75,10 +74,7 @@ export default function AssetActions(): ReactElement {
}
]
// Check from metadata, cause that is available earlier
const hasPool = ddo?.price?.type === 'pool'
hasPool &&
price?.type === 'pool' &&
tabs.push(
{
title: 'Pool',

View File

@ -27,6 +27,11 @@
margin-right: calc(var(--spacer) / 4);
}
.datatoken {
white-space: pre;
margin-right: calc(var(--spacer) / 3);
}
.byline {
font-size: var(--font-size-small);
}
@ -34,3 +39,13 @@
.updated {
font-size: var(--font-size-mini);
}
.addWrap {
padding-left: calc(var(--spacer) / 5);
border-left: 1px solid var(--border-color);
display: inline-block;
}
.add {
font-size: var(--font-size-mini);
}

View File

@ -3,13 +3,14 @@ import { useAsset } from '../../../providers/Asset'
import { useWeb3 } from '../../../providers/Web3'
import ExplorerLink from '../../atoms/ExplorerLink'
import Publisher from '../../atoms/Publisher'
import AddToken from '../../atoms/AddToken'
import Time from '../../atoms/Time'
import styles from './MetaMain.module.css'
import AssetType from '../../atoms/AssetType'
import styles from './MetaMain.module.css'
export default function MetaMain(): ReactElement {
const { ddo, owner, type } = useAsset()
const { networkId } = useWeb3()
const { networkId, web3ProviderInfo } = useWeb3()
const isCompute = Boolean(ddo?.findServiceByType('compute'))
const accessType = isCompute ? 'compute' : 'access'
@ -22,6 +23,7 @@ export default function MetaMain(): ReactElement {
className={styles.assetType}
/>
<ExplorerLink
className={styles.datatoken}
networkId={networkId}
path={
networkId === 137 || networkId === 1287
@ -31,6 +33,19 @@ export default function MetaMain(): ReactElement {
>
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink>
{web3ProviderInfo?.name === 'MetaMask' && (
<span className={styles.addWrap}>
<AddToken
address={ddo?.dataTokenInfo.address}
symbol={ddo?.dataTokenInfo.symbol}
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/datatoken.png"
text={`Add ${ddo?.dataTokenInfo.symbol} to wallet`}
className={styles.add}
minimal
/>
</span>
)}
</header>
<div className={styles.byline}>

View File

@ -16,3 +16,9 @@
font-size: var(--font-size-small);
font-style: italic;
}
.loaderWrap {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -1,17 +1,28 @@
import AssetTeaser from '../molecules/AssetTeaser'
import React from 'react'
import React, { useEffect, useState } from 'react'
import Pagination from '../molecules/Pagination'
import styles from './AssetList.module.css'
import { DDO } from '@oceanprotocol/lib'
import classNames from 'classnames/bind'
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
import Loader from '../atoms/Loader'
const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
declare type AssetListProps = {
assets: DDO[]
showPagination: boolean
page?: number
totalPages?: number
isLoading?: boolean
onPageChange?: React.Dispatch<React.SetStateAction<number>>
className?: string
}
@ -21,9 +32,22 @@ const AssetList: React.FC<AssetListProps> = ({
showPagination,
page,
totalPages,
isLoading,
onPageChange,
className
}) => {
const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>()
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
if (!assets) return
isLoading && setLoading(true)
getAssetsBestPrices(assets).then((asset) => {
setAssetWithPrices(asset)
setLoading(false)
})
}, [assets])
// // This changes the page field inside the query
function handlePageChange(selected: number) {
onPageChange(selected + 1)
@ -34,11 +58,19 @@ const AssetList: React.FC<AssetListProps> = ({
[className]: className
})
return (
return assetsWithPrices &&
!loading &&
(isLoading === undefined || isLoading === false) ? (
<>
<div className={styleClasses}>
{assets.length > 0 ? (
assets.map((ddo) => <AssetTeaser ddo={ddo} key={ddo.id} />)
{assetsWithPrices.length > 0 ? (
assetsWithPrices.map((assetWithPrice) => (
<AssetTeaser
ddo={assetWithPrice.ddo}
price={assetWithPrice.price}
key={assetWithPrice.ddo.id}
/>
))
) : (
<div className={styles.empty}>No results found.</div>
)}
@ -52,6 +84,8 @@ const AssetList: React.FC<AssetListProps> = ({
/>
)}
</>
) : (
<LoaderArea />
)
}

View File

@ -2,3 +2,25 @@
text-transform: uppercase;
color: var(--color-secondary);
}
.computeTableContainer {
display: flex;
align-items: center;
}
.refresh,
.refresh:first-child {
margin-bottom: calc(var(--spacer) / 2);
margin-left: auto;
margin-right: auto;
display: block;
}
.refresh svg {
display: inline-block;
fill: currentColor;
width: 1em;
height: 1em;
margin-right: calc(var(--spacer) / 6);
margin-bottom: -0.1rem;
}

View File

@ -6,6 +6,7 @@ import { DDO, Logger, Service, Provider } from '@oceanprotocol/lib'
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
import Dotdotdot from 'react-dotdotdot'
import Table from '../../../atoms/Table'
import Button from '../../../atoms/Button'
import { useOcean } from '../../../../providers/Ocean'
import { gql, useQuery } from '@apollo/client'
import { useWeb3 } from '../../../../providers/Web3'
@ -13,8 +14,9 @@ import { queryMetadata } from '../../../../utils/aquarius'
import axios, { CancelToken } from 'axios'
import { ComputeOrders } from '../../../../@types/apollo/ComputeOrders'
import Details from './Details'
import styles from './index.module.css'
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { ReactComponent as Refresh } from '../../../../images/refresh.svg'
import styles from './index.module.css'
const getComputeOrders = gql`
query ComputeOrders($user: String!) {
@ -99,7 +101,7 @@ async function getAssetMetadata(
export default function ComputeJobs(): ReactElement {
const { ocean, account, config } = useOcean()
const { accountId } = useWeb3()
const [isLoading, setIsLoading] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const { data } = useQuery<ComputeOrders>(getComputeOrders, {
variables: {
@ -107,139 +109,158 @@ export default function ComputeJobs(): ReactElement {
}
})
useEffect(() => {
if (data === undefined || !config?.metadataCacheUri) return
async function getJobs() {
if (!ocean || !account) return
async function getJobs() {
if (!ocean || !account) return
setIsLoading(true)
setIsLoading(true)
const dtList = []
const computeJobs: ComputeJobMetaData[] = []
for (let i = 0; i < data.tokenOrders.length; i++) {
dtList.push(data.tokenOrders[i].datatokenId.address)
}
const queryDtList = JSON.stringify(dtList)
.replace(/,/g, ' ')
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
const dtList = []
const computeJobs: ComputeJobMetaData[] = []
try {
const source = axios.CancelToken.source()
const assets = await getAssetMetadata(
queryDtList,
config.metadataCacheUri,
source.token
)
const providers: Provider[] = []
const serviceEndpoints: string[] = []
for (let i = 0; i < data.tokenOrders.length; i++) {
dtList.push(data.tokenOrders[i].datatokenId.address)
try {
const did = web3.utils
.toChecksumAddress(data.tokenOrders[i].datatokenId.address)
.replace('0x', 'did:op:')
const ddo = assets.filter((x) => x.id === did)[0]
if (!ddo) continue
const service = ddo.service.filter(
(x: Service) => x.index === data.tokenOrders[i].serviceId
)[0]
if (!service || service.type !== 'compute') continue
const { serviceEndpoint } = service
const wasProviderQueried =
serviceEndpoints.filter((x) => x === serviceEndpoint).length > 0
if (wasProviderQueried) continue
serviceEndpoints.push(serviceEndpoint)
} catch (err) {
Logger.error(err)
}
}
const queryDtList = JSON.stringify(dtList)
.replace(/,/g, ' ')
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
try {
const source = axios.CancelToken.source()
const assets = await getAssetMetadata(
queryDtList,
config.metadataCacheUri,
source.token
)
const providers: Provider[] = []
const serviceEndpoints: string[] = []
for (let i = 0; i < data.tokenOrders.length; i++) {
try {
const did = web3.utils
.toChecksumAddress(data.tokenOrders[i].datatokenId.address)
.replace('0x', 'did:op:')
for (let i = 0; i < serviceEndpoints.length; i++) {
const instanceConfig = {
config,
web3: config.web3Provider,
logger: Logger,
ocean: ocean
}
const provider = await Provider.getInstance(instanceConfig)
await provider.setBaseUrl(serviceEndpoints[i])
const hasSameCompute =
providers.filter(
(x) => x.computeAddress === provider.computeAddress
).length > 0
if (!hasSameCompute) providers.push(provider)
}
} catch (err) {
Logger.error(err)
}
for (let i = 0; i < providers.length; i++) {
try {
const providerComputeJobs = (await providers[i].computeStatus(
'',
account,
undefined,
undefined,
false
)) as ComputeJob[]
// means the provider uri is not good, so we ignore it and move on
if (!providerComputeJobs) continue
providerComputeJobs.sort((a, b) => {
if (a.dateCreated > b.dateCreated) {
return -1
}
if (a.dateCreated < b.dateCreated) {
return 1
}
return 0
})
for (let j = 0; j < providerComputeJobs.length; j++) {
const job = providerComputeJobs[j]
const did = job.inputDID[0]
const ddo = assets.filter((x) => x.id === did)[0]
if (!ddo) continue
const service = ddo.service.filter(
(x: Service) => x.index === data.tokenOrders[i].serviceId
const serviceMetadata = ddo.service.filter(
(x: Service) => x.type === 'metadata'
)[0]
if (!service || service.type !== 'compute') continue
const { serviceEndpoint } = service
const wasProviderQueried =
serviceEndpoints.filter((x) => x === serviceEndpoint).length > 0
if (wasProviderQueried) continue
serviceEndpoints.push(serviceEndpoint)
} catch (err) {
Logger.error(err)
}
}
try {
for (let i = 0; i < serviceEndpoints.length; i++) {
const instanceConfig = {
config,
web3: config.web3Provider,
logger: Logger,
ocean: ocean
const compJob: ComputeJobMetaData = {
...job,
assetName: serviceMetadata.attributes.main.name,
assetDtSymbol: ddo.dataTokenInfo.symbol
}
const provider = await Provider.getInstance(instanceConfig)
await provider.setBaseUrl(serviceEndpoints[i])
const hasSameCompute =
providers.filter(
(x) => x.computeAddress === provider.computeAddress
).length > 0
if (!hasSameCompute) providers.push(provider)
computeJobs.push(compJob)
}
} catch (err) {
Logger.error(err)
}
for (let i = 0; i < providers.length; i++) {
try {
const providerComputeJobs = (await providers[i].computeStatus(
'',
account,
undefined,
undefined,
false
)) as ComputeJob[]
// means the provider uri is not good, so we ignore it and move on
if (!providerComputeJobs) continue
providerComputeJobs.sort((a, b) => {
if (a.dateCreated > b.dateCreated) {
return -1
}
if (a.dateCreated < b.dateCreated) {
return 1
}
return 0
})
for (let j = 0; j < providerComputeJobs.length; j++) {
const job = providerComputeJobs[j]
const did = job.inputDID[0]
const ddo = assets.filter((x) => x.id === did)[0]
if (!ddo) continue
const serviceMetadata = ddo.service.filter(
(x: Service) => x.type === 'metadata'
)[0]
const compJob: ComputeJobMetaData = {
...job,
assetName: serviceMetadata.attributes.main.name,
assetDtSymbol: ddo.dataTokenInfo.symbol
}
computeJobs.push(compJob)
}
} catch (err) {
Logger.error(err)
}
}
setJobs(computeJobs)
} catch (error) {
Logger.log(error.message)
} finally {
setIsLoading(false)
}
setJobs(computeJobs)
} catch (error) {
Logger.log(error.message)
} finally {
setIsLoading(false)
}
return true
}
useEffect(() => {
if (data === undefined || !config?.metadataCacheUri) {
setIsLoading(false)
return
}
getJobs()
}, [ocean, account, data, config?.metadataCacheUri])
return (
<Table
columns={columns}
data={jobs}
isLoading={isLoading}
defaultSortField="row.dateCreated"
defaultSortAsc={false}
/>
<>
{jobs.length > 0 && (
<Button
style="text"
size="small"
title="Refresh compute jobs"
onClick={() => getJobs()}
disabled={isLoading}
className={styles.refresh}
>
<Refresh />
Refresh
</Button>
)}
<Table
columns={columns}
data={jobs}
isLoading={isLoading}
defaultSortField="row.dateCreated"
defaultSortAsc={false}
/>
</>
)
}

View File

@ -1,7 +1,6 @@
import { Logger } from '@oceanprotocol/lib'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import React, { ReactElement, useEffect, useState } from 'react'
import Loader from '../../atoms/Loader'
import AssetList from '../../organisms/AssetList'
import axios from 'axios'
import { queryMetadata } from '../../../utils/aquarius'
@ -48,14 +47,13 @@ export default function PublishedList(): ReactElement {
getPublished()
}, [accountId, page, config.metadataCacheUri])
return isLoading ? (
<Loader />
) : accountId && queryResult ? (
return accountId ? (
<AssetList
assets={queryResult.results}
assets={queryResult?.results}
isLoading={isLoading}
showPagination
page={queryResult.page}
totalPages={queryResult.totalPages}
page={queryResult?.page}
totalPages={queryResult?.totalPages}
onPageChange={(newPage) => {
setPage(newPage)
}}

View File

@ -16,9 +16,3 @@
.section [class*='button'] {
margin-top: var(--spacer);
}
.loaderWrap {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -7,13 +7,13 @@ import {
SearchQuery
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import Container from '../atoms/Container'
import Loader from '../atoms/Loader'
import { useOcean } from '../../providers/Ocean'
import Button from '../atoms/Button'
import Bookmarks from '../molecules/Bookmarks'
import axios from 'axios'
import { queryMetadata } from '../../utils/aquarius'
import Permission from '../organisms/Permission'
import { useWeb3 } from '../../providers/Web3'
const queryHighest = {
page: 1,
@ -37,14 +37,6 @@ const queryLatest = {
sort: { created: -1 }
}
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
function SectionQueryResult({
title,
query,
@ -56,11 +48,10 @@ function SectionQueryResult({
}) {
const { config } = useOcean()
const [result, setResult] = useState<QueryResult>()
const [loading, setLoading] = useState(true)
const { web3Loading } = useWeb3()
useEffect(() => {
if (!config?.metadataCacheUri) return
if (!config?.metadataCacheUri || web3Loading) return
const source = axios.CancelToken.source()
async function init() {
@ -70,23 +61,18 @@ function SectionQueryResult({
source.token
)
setResult(result)
setLoading(false)
}
init()
return () => {
source.cancel()
}
}, [config?.metadataCacheUri, query])
}, [config?.metadataCacheUri, query, web3Loading])
return (
<section className={styles.section}>
<h3>{title}</h3>
{loading ? (
<LoaderArea />
) : (
result && <AssetList assets={result.results} showPagination={false} />
)}
<AssetList assets={result?.results} showPagination={false} />
{action && action}
</section>
)

View File

@ -14,7 +14,6 @@ import Button from '../../atoms/Button'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormAlgorithm } from '../../../@types/MetaData'
import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish'
import stylesIndex from './index.module.css'
const query = graphql`
@ -63,6 +62,24 @@ export default function FormPublish(): ReactElement {
initialValues.dockerImage
)
const dockerImageOptions = [
{
name: 'node:latest',
title: 'node:latest',
checked: true
},
{
name: 'python:latest',
title: 'python:latest',
checked: false
},
{
name: 'custom image',
title: 'custom image',
checked: false
}
]
// reset form validation on every mount
useEffect(() => {
setErrors({})
@ -135,6 +152,11 @@ export default function FormPublish(): ReactElement {
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection'
? dockerImageOptions
: field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)

View File

@ -7,6 +7,8 @@ import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish'
import { useOcean } from '../../../providers/Ocean'
import { ReactComponent as Download } from '../../../images/download.svg'
import { ReactComponent as Compute } from '../../../images/compute.svg'
import stylesIndex from './index.module.css'
import styles from './FormPublish.module.css'
@ -61,6 +63,19 @@ export default function FormPublish(): ReactElement {
// setSubmitting(false)
}, [setErrors, setTouched])
const accessTypeOptions = [
{
name: 'Download',
title: 'Download',
icon: <Download />
},
{
name: 'Compute',
title: 'Compute',
icon: <Compute />
}
]
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
@ -94,6 +109,9 @@ export default function FormPublish(): ReactElement {
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection' ? accessTypeOptions : field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)

View File

@ -23,7 +23,7 @@ export default function PageTemplateAssetDetails({
setPageTitle(isInPurgatory ? '' : title)
}, [ddo, error, isInPurgatory, title])
return ddo ? (
return ddo && pageTitle ? (
<>
<Page title={pageTitle} uri={uri}>
<Router basepath="/asset">

View File

@ -1,188 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useNavigate } from '@reach/router'
import styles from './filterPrice.module.css'
import classNames from 'classnames/bind'
import {
addExistingParamsToUrl,
FilterByPriceOptions,
FilterByTypeOptions
} from './utils'
import Button from '../../atoms/Button'
const cx = classNames.bind(styles)
const clearFilters = [{ display: 'Clear', value: '' }]
const priceFilterItems = [
{ display: 'fixed price', value: FilterByPriceOptions.Fixed },
{ display: 'dynamic price', value: FilterByPriceOptions.Dynamic }
]
const serviceFilterItems = [
{ display: 'data sets', value: FilterByTypeOptions.Data },
{ display: 'algorithms', value: FilterByTypeOptions.Algorithm }
]
export default function FilterPrice({
priceType,
serviceType,
setPriceType,
setServiceType
}: {
priceType: string
setPriceType: React.Dispatch<React.SetStateAction<string>>
serviceType: string
setServiceType: React.Dispatch<React.SetStateAction<string>>
}): ReactElement {
const navigate = useNavigate()
const [priceSelections, setPriceSelections] = useState<string[]>([])
const [serviceSelections, setServiceSelections] = useState<string[]>([])
async function applyPriceFilter(filterBy: string) {
let urlLocation = await addExistingParamsToUrl(location, 'priceType')
if (filterBy) {
urlLocation = `${urlLocation}&priceType=${filterBy}`
}
setPriceType(filterBy)
navigate(urlLocation)
}
async function applyServiceFilter(filterBy: string) {
let urlLocation = await addExistingParamsToUrl(location, 'serviceType')
if (filterBy && location.search.indexOf('&serviceType') === -1) {
urlLocation = `${urlLocation}&serviceType=${filterBy}`
}
setServiceType(filterBy)
navigate(urlLocation)
}
async function handleSelectedFilter(isSelected: boolean, value: string) {
if (
value === FilterByPriceOptions.Fixed ||
value === FilterByPriceOptions.Dynamic
) {
if (isSelected) {
if (priceSelections.length > 1) {
// both selected -> select the other one
const otherValue = priceFilterItems.find((p) => p.value !== value)
.value
await applyPriceFilter(otherValue)
} else {
// only the current one selected -> deselect it
await applyPriceFilter(undefined)
}
} else {
if (priceSelections.length > 0) {
// one already selected -> both selected
await applyPriceFilter(FilterByPriceOptions.All)
setPriceSelections(priceFilterItems.map((p) => p.value))
} else {
// none selected -> select
await applyPriceFilter(value)
setPriceSelections([value])
}
}
} else {
if (isSelected) {
if (serviceSelections.length > 1) {
const otherValue = serviceFilterItems.find((p) => p.value !== value)
.value
await applyServiceFilter(otherValue)
setServiceSelections([otherValue])
} else {
await applyServiceFilter(undefined)
}
} else {
if (serviceSelections.length) {
await applyServiceFilter(undefined)
setServiceSelections(serviceFilterItems.map((p) => p.value))
} else {
await applyServiceFilter(value)
setServiceSelections([value])
}
}
}
}
async function applyClearFilter() {
let urlLocation = await addExistingParamsToUrl(
location,
'priceType',
'serviceType'
)
urlLocation = `${urlLocation}`
setServiceSelections([])
setPriceSelections([])
setPriceType(undefined)
setServiceType(undefined)
navigate(urlLocation)
}
return (
<div className={styles.filterList}>
{priceFilterItems.map((e, index) => {
const isPriceSelected =
e.value === priceType || priceSelections.includes(e.value)
const selectFilter = cx({
[styles.selected]: isPriceSelected,
[styles.filter]: true
})
return (
<Button
size="small"
style="text"
key={index}
className={selectFilter}
onClick={async () => {
handleSelectedFilter(isPriceSelected, e.value)
}}
>
{e.display}
</Button>
)
})}
{serviceFilterItems.map((e, index) => {
const isServiceSelected =
e.value === serviceType || serviceSelections.includes(e.value)
const selectFilter = cx({
[styles.selected]: isServiceSelected,
[styles.filter]: true
})
return (
<Button
size="small"
style="text"
key={index}
className={selectFilter}
onClick={async () => {
handleSelectedFilter(isServiceSelected, e.value)
}}
>
{e.display}
</Button>
)
})}
{clearFilters.map((e, index) => {
const showClear =
priceSelections.length > 0 || serviceSelections.length > 0
return (
<Button
size="small"
style="text"
key={index}
className={showClear ? styles.showClear : styles.hideClear}
onClick={async () => {
applyClearFilter()
}}
>
{e.display}
</Button>
)
})}
</div>
)
}

View File

@ -47,6 +47,10 @@ button.filter,
margin-bottom: calc(var(--spacer) / 6);
}
.showClear:hover {
display: inline-flex;
color: var(--color-primary);
}
.showClear {
display: inline-flex;
text-transform: capitalize;

View File

@ -0,0 +1,111 @@
import React, { ReactElement, useState } from 'react'
import { useNavigate } from '@reach/router'
import styles from './filterService.module.css'
import classNames from 'classnames/bind'
import { addExistingParamsToUrl, FilterByTypeOptions } from './utils'
import Button from '../../atoms/Button'
const cx = classNames.bind(styles)
const clearFilters = [{ display: 'Clear', value: '' }]
const serviceFilterItems = [
{ display: 'data sets', value: FilterByTypeOptions.Data },
{ display: 'algorithms', value: FilterByTypeOptions.Algorithm }
]
export default function FilterPrice({
serviceType,
setServiceType
}: {
serviceType: string
setServiceType: React.Dispatch<React.SetStateAction<string>>
}): ReactElement {
const navigate = useNavigate()
const [serviceSelections, setServiceSelections] = useState<string[]>([])
async function applyServiceFilter(filterBy: string) {
let urlLocation = await addExistingParamsToUrl(location, 'serviceType')
if (filterBy && location.search.indexOf('&serviceType') === -1) {
urlLocation = `${urlLocation}&serviceType=${filterBy}`
}
setServiceType(filterBy)
navigate(urlLocation)
}
async function handleSelectedFilter(isSelected: boolean, value: string) {
if (isSelected) {
if (serviceSelections.length > 1) {
const otherValue = serviceFilterItems.find((p) => p.value !== value)
.value
await applyServiceFilter(otherValue)
setServiceSelections([otherValue])
} else {
await applyServiceFilter(undefined)
if (serviceSelections.includes(value)) {
serviceSelections.pop()
}
}
} else {
if (serviceSelections.length) {
await applyServiceFilter(undefined)
setServiceSelections(serviceFilterItems.map((p) => p.value))
} else {
await applyServiceFilter(value)
setServiceSelections([value])
}
}
}
async function applyClearFilter() {
let urlLocation = await addExistingParamsToUrl(location, 'serviceType')
urlLocation = `${urlLocation}`
setServiceSelections([])
setServiceType(undefined)
navigate(urlLocation)
}
return (
<div className={styles.filterList}>
{serviceFilterItems.map((e, index) => {
const isServiceSelected =
e.value === serviceType || serviceSelections.includes(e.value)
const selectFilter = cx({
[styles.selected]: isServiceSelected,
[styles.filter]: true
})
return (
<Button
size="small"
style="text"
key={index}
className={selectFilter}
onClick={async () => {
handleSelectedFilter(isServiceSelected, e.value)
}}
>
{e.display}
</Button>
)
})}
{clearFilters.map((e, index) => {
const showClear = serviceSelections.length > 0
return (
<Button
size="small"
style="text"
key={index}
className={showClear ? styles.showClear : styles.hideClear}
onClick={async () => {
applyClearFilter()
}}
>
{e.display}
</Button>
)
})}
</div>
)
}

View File

@ -5,7 +5,7 @@ import SearchBar from '../../molecules/SearchBar'
import AssetList from '../../organisms/AssetList'
import styles from './index.module.css'
import queryString from 'query-string'
import PriceFilter from './filterPrice'
import ServiceFilter from './filterService'
import Sort from './sort'
import { getResults } from './utils'
import { navigate } from 'gatsby'
@ -22,19 +22,9 @@ export default function SearchPage({
}): ReactElement {
const { config } = useOcean()
const parsed = queryString.parse(location.search)
const {
text,
owner,
tags,
page,
sort,
sortOrder,
priceType,
serviceType
} = parsed
const { text, owner, tags, page, sort, sortOrder, serviceType } = parsed
const [queryResult, setQueryResult] = useState<QueryResult>()
const [loading, setLoading] = useState<boolean>()
const [price, setPriceType] = useState<string>(priceType as string)
const [service, setServiceType] = useState<string>(serviceType as string)
const [sortType, setSortType] = useState<string>(sort as string)
const [sortDirection, setSortDirection] = useState<string>(
@ -59,7 +49,6 @@ export default function SearchPage({
tags,
sort,
page,
priceType,
serviceType,
sortOrder,
config.metadataCacheUri
@ -82,10 +71,8 @@ export default function SearchPage({
<SearchBar initialValue={(text || owner) as string} />
)}
<div className={styles.row}>
<PriceFilter
priceType={price}
<ServiceFilter
serviceType={service}
setPriceType={setPriceType}
setServiceType={setServiceType}
/>
<Sort
@ -93,25 +80,18 @@ export default function SearchPage({
sortDirection={sortDirection}
setSortType={setSortType}
setSortDirection={setSortDirection}
setPriceType={setPriceType}
setServiceType={setServiceType}
/>
</div>
</div>
<div className={styles.results}>
{loading ? (
<Loader />
) : queryResult ? (
<AssetList
assets={queryResult.results}
showPagination
page={queryResult.page}
totalPages={queryResult.totalPages}
onPageChange={setPage}
/>
) : (
''
)}
<AssetList
assets={queryResult?.results}
showPagination
isLoading={loading}
page={queryResult?.page}
totalPages={queryResult?.totalPages}
onPageChange={setPage}
/>
</div>
</>
</Permission>

View File

@ -3,9 +3,7 @@ import { useNavigate } from '@reach/router'
import {
addExistingParamsToUrl,
SortTermOptions,
SortValueOptions,
FilterByPriceOptions,
FilterByTypeOptions
SortValueOptions
} from './utils'
import Button from '../../atoms/Button'
import styles from './sort.module.css'
@ -13,26 +11,18 @@ import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const sortItems = [
{ display: 'Published', value: SortTermOptions.Created },
{ display: 'Liquidity', value: SortTermOptions.Liquidity },
{ display: 'Price', value: SortTermOptions.Price }
]
const sortItems = [{ display: 'Published', value: SortTermOptions.Created }]
export default function Sort({
sortType,
setSortType,
sortDirection,
setSortDirection,
setPriceType,
setServiceType
setSortDirection
}: {
sortType: string
setSortType: React.Dispatch<React.SetStateAction<string>>
sortDirection: string
setSortDirection: React.Dispatch<React.SetStateAction<string>>
setPriceType: React.Dispatch<React.SetStateAction<string>>
setServiceType: React.Dispatch<React.SetStateAction<string>>
}): ReactElement {
const navigate = useNavigate()
const directionArrow = String.fromCharCode(
@ -41,12 +31,7 @@ export default function Sort({
async function sortResults(sortBy?: string, direction?: string) {
let urlLocation: string
if (sortBy) {
urlLocation = await addExistingParamsToUrl(location, 'sort', 'priceType')
urlLocation = `${urlLocation}&sort=${sortBy}`
if (sortBy === SortTermOptions.Liquidity) {
urlLocation = `${urlLocation}&priceType=${FilterByPriceOptions.Dynamic}`
setPriceType(FilterByPriceOptions.Dynamic)
}
setSortType(sortBy)
} else if (direction) {
urlLocation = await addExistingParamsToUrl(location, 'sortOrder')

View File

@ -6,8 +6,6 @@ import { MetadataCache, Logger } from '@oceanprotocol/lib'
import queryString from 'query-string'
export const SortTermOptions = {
Liquidity: 'liquidity',
Price: 'price',
Created: 'created'
} as const
type SortTermOptions = typeof SortTermOptions[keyof typeof SortTermOptions]
@ -25,36 +23,12 @@ export const SortValueOptions = {
} as const
type SortValueOptions = typeof SortValueOptions[keyof typeof SortValueOptions]
export const FilterByPriceOptions = {
Fixed: 'exchange',
Dynamic: 'pool',
All: 'all'
} as const
type FilterByPriceOptions = typeof FilterByPriceOptions[keyof typeof FilterByPriceOptions]
export const FilterByTypeOptions = {
Data: 'dataset',
Algorithm: 'algorithm'
} as const
type FilterByTypeOptions = typeof FilterByTypeOptions[keyof typeof FilterByTypeOptions]
function addPriceFilterToQuery(sortTerm: string, priceFilter: string): string {
if (priceFilter === FilterByPriceOptions.All) {
sortTerm = priceFilter
? sortTerm === ''
? `(price.type:${FilterByPriceOptions.Fixed} OR price.type:${FilterByPriceOptions.Dynamic})`
: `${sortTerm} AND (price.type:${FilterByPriceOptions.Dynamic} OR price.type:${FilterByPriceOptions.Fixed})`
: sortTerm
} else {
sortTerm = priceFilter
? sortTerm === ''
? `price.type:${priceFilter}`
: `${sortTerm} AND price.type:${priceFilter}`
: sortTerm
}
return sortTerm
}
function addTypeFilterToQuery(sortTerm: string, typeFilter: string): string {
sortTerm = typeFilter
? sortTerm === ''
@ -64,13 +38,8 @@ function addTypeFilterToQuery(sortTerm: string, typeFilter: string): string {
return sortTerm
}
function getSortType(sortParam: string): string {
const sortTerm =
sortParam === SortTermOptions.Liquidity
? SortElasticTerm.Liquidity
: sortParam === SortTermOptions.Price
? SortElasticTerm.Price
: SortTermOptions.Created
function getSortType(): string {
const sortTerm = SortTermOptions.Created
return sortTerm
}
@ -83,10 +52,9 @@ export function getSearchQuery(
offset?: string,
sort?: string,
sortOrder?: string,
priceType?: string,
serviceType?: string
): SearchQuery {
const sortTerm = getSortType(sort)
const sortTerm = getSortType()
const sortValue = sortOrder === SortValueOptions.Ascending ? 1 : -1
let searchTerm = owner
? `(publicKey.owner:${owner})`
@ -97,15 +65,25 @@ export function getSearchQuery(
? // eslint-disable-next-line no-useless-escape
`(service.attributes.additionalInformation.categories:\"${categories}\")`
: text || ''
// HACK: resolves the case sensitivity related to dataTokenInfo.symbol
searchTerm = '*' + searchTerm.toUpperCase() + '*'
searchTerm = addTypeFilterToQuery(searchTerm, serviceType)
searchTerm = addPriceFilterToQuery(searchTerm, priceType)
return {
page: Number(page) || 1,
offset: Number(offset) || 21,
query: {
query_string: {
query: `${searchTerm} -isInPurgatory:true`
query: `${searchTerm} -isInPurgatory:true`,
fields: [
'dataTokenInfo.name',
'dataTokenInfo.symbol',
'service.attributes.main.name',
'service.attributes.main.author',
'service.attributes.additionalInformation.description'
],
default_operator: 'AND'
}
// ...(owner && { 'publicKey.owner': [owner] }),
// ...(tags && { tags: [tags] }),
@ -136,7 +114,6 @@ export async function getResults(
offset?: string
sort?: string
sortOrder?: string
priceType?: string
serviceType?: string
},
metadataCacheUri: string
@ -150,10 +127,10 @@ export async function getResults(
categories,
sort,
sortOrder,
priceType,
serviceType
} = params
const metadataCache = new MetadataCache(metadataCacheUri, Logger)
const searchQuery = getSearchQuery(
text,
owner,
@ -163,11 +140,10 @@ export async function getResults(
offset,
sort,
sortOrder,
priceType,
serviceType
)
const queryResult = await metadataCache.queryMetadata(searchQuery)
const queryResult = await metadataCache.queryMetadata(searchQuery)
return queryResult
}

View File

@ -12,11 +12,9 @@ import { PurgatoryData } from '@oceanprotocol/lib/dist/node/ddo/interfaces/Purga
import getAssetPurgatoryData from '../utils/purgatory'
import axios, { CancelToken } from 'axios'
import { retrieveDDO } from '../utils/aquarius'
import { getPrice } from '../utils/subgraph'
import { MetadataMarket } from '../@types/MetaData'
import { useOcean } from './Ocean'
import { gql, useQuery } from '@apollo/client'
import { PoolPrice } from '../@types/apollo/PoolPrice'
import { FrePrice } from '../@types/apollo/FrePrice'
interface AssetProviderValue {
isInPurgatory: boolean
@ -33,26 +31,6 @@ interface AssetProviderValue {
refreshDdo: (token?: CancelToken) => Promise<void>
}
const poolQuery = gql`
query PoolPrice($datatoken: String) {
pools(where: { datatokenAddress: $datatoken }) {
spotPrice
consumePrice
datatokenReserve
oceanReserve
}
}
`
const freQuery = gql`
query FrePrice($datatoken: String) {
fixedRateExchanges(orderBy: id, where: { datatoken: $datatoken }) {
rate
id
}
}
`
const AssetContext = createContext({} as AssetProviderValue)
const refreshInterval = 10000 // 10 sec.
@ -75,68 +53,6 @@ function AssetProvider({
const [owner, setOwner] = useState<string>()
const [error, setError] = useState<string>()
const [type, setType] = useState<MetadataMain['type']>()
const [variables, setVariables] = useState({})
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
refetch: refetchFre,
startPolling: startPollingFre,
data: frePrice
} = useQuery<FrePrice>(freQuery, {
variables,
skip: false
})
const {
refetch: refetchPool,
startPolling: startPollingPool,
data: poolPrice
} = useQuery<PoolPrice>(poolQuery, {
variables,
skip: false
})
/* eslint-enable @typescript-eslint/no-unused-vars */
// this is not working as expected, thus we need to fetch both pool and fre
// useEffect(() => {
// if (!ddo || !variables || variables === '') return
// if (ddo.price.type === 'exchange') {
// refetchFre(variables)
// startPollingFre(refreshInterval)
// } else {
// refetchPool(variables)
// startPollingPool(refreshInterval)
// }
// }, [ddo, variables])
useEffect(() => {
if (
!frePrice ||
frePrice.fixedRateExchanges.length === 0 ||
price.type !== 'exchange'
)
return
setPrice((prevState) => ({
...prevState,
value: frePrice.fixedRateExchanges[0].rate,
address: frePrice.fixedRateExchanges[0].id
}))
}, [frePrice])
useEffect(() => {
if (!poolPrice || poolPrice.pools.length === 0 || price.type !== 'pool')
return
setPrice((prevState) => ({
...prevState,
value:
poolPrice.pools[0].consumePrice === '-1'
? poolPrice.pools[0].spotPrice
: poolPrice.pools[0].consumePrice,
ocean: poolPrice.pools[0].oceanReserve,
datatoken: poolPrice.pools[0].datatokenReserve,
isConsumable: poolPrice.pools[0].consumePrice === '-1' ? 'false' : 'true'
}))
}, [poolPrice])
const fetchDdo = async (token?: CancelToken) => {
Logger.log('[asset] Init asset, get DDO')
@ -201,10 +117,8 @@ function AssetProvider({
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
if (!ddo) return
// Set price & metadata from DDO first
// TODO Hacky hack, temporary™: set isConsumable to true by default since Aquarius can't be trusted.
setPrice({ ...ddo.price, isConsumable: 'true' })
setVariables({ datatoken: ddo?.dataToken.toLowerCase() })
const returnedPrice = await getPrice(ddo)
setPrice({ ...returnedPrice })
// Get metadata from DDO
const { attributes } = ddo.findServiceByType('metadata')

View File

@ -8,7 +8,7 @@ import React, {
useCallback
} from 'react'
import Web3 from 'web3'
import Web3Modal from 'web3modal'
import Web3Modal, { getProviderInfo, IProviderInfo } from 'web3modal'
import { infuraProjectId as infuraId, portisId } from '../../app.config'
import WalletConnectProvider from '@walletconnect/web3-provider'
import { Logger } from '@oceanprotocol/lib'
@ -24,6 +24,7 @@ interface Web3ProviderValue {
web3: Web3
web3Provider: any
web3Modal: Web3Modal
web3ProviderInfo: IProviderInfo
accountId: string
networkId: number
networkDisplayName: string
@ -106,6 +107,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
const [web3, setWeb3] = useState<Web3>()
const [web3Provider, setWeb3Provider] = useState<any>()
const [web3Modal, setWeb3Modal] = useState<Web3Modal>()
const [web3ProviderInfo, setWeb3ProviderInfo] = useState<IProviderInfo>()
const [networkId, setNetworkId] = useState<number>()
const [networkDisplayName, setNetworkDisplayName] = useState<string>()
const [networkData, setNetworkData] = useState<EthereumListsChain>()
@ -148,7 +150,10 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
// Create initial Web3Modal instance
// -----------------------------------
useEffect(() => {
if (web3Modal) return
if (web3Modal) {
setWeb3Loading(false)
return
}
async function init() {
// note: needs artificial await here so the log message is reached and output
@ -209,6 +214,18 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
getBlock()
}, [web3, networkId])
// -----------------------------------
// Get and set web3 provider info
// -----------------------------------
// Workaround cause getInjectedProviderName() always returns `MetaMask`
// https://github.com/oceanprotocol/market/issues/332
useEffect(() => {
if (!web3Provider) return
const providerInfo = getProviderInfo(web3Provider)
setWeb3ProviderInfo(providerInfo)
}, [web3Provider])
// -----------------------------------
// Logout helper
// -----------------------------------
@ -255,6 +272,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
web3,
web3Provider,
web3Modal,
web3ProviderInfo,
accountId,
networkId,
networkDisplayName,

View File

@ -10,7 +10,7 @@ import {
SearchQuery
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import { AssetSelectionAsset } from '../components/molecules/FormFields/AssetSelection'
import { PriceList, getAssetPrices } from './subgraph'
import { PriceList, getAssetsPriceList } from './subgraph'
import axios, { CancelToken, AxiosResponse } from 'axios'
// TODO: import directly from ocean.js somehow.
@ -113,7 +113,7 @@ export async function transformDDOToAssetSelection(
): Promise<AssetSelectionAsset[]> {
const source = axios.CancelToken.source()
const didList: string[] = []
const priceList: PriceList = await getAssetPrices(ddoList)
const priceList: PriceList = await getAssetsPriceList(ddoList)
const symbolList: any = {}
for (const ddo of ddoList) {
didList.push(ddo.id)

View File

@ -1,14 +1,32 @@
import { gql, DocumentNode, ApolloQueryResult } from '@apollo/client'
import { DDO } from '@oceanprotocol/lib'
import { DDO, BestPrice } from '@oceanprotocol/lib'
import { getApolloClientInstance } from '../providers/ApolloClientProvider'
import {
AssetsPoolPrice,
AssetsPoolPrice_pools as AssetsPoolPricePools
} from '../@types/apollo/AssetsPoolPrice'
import {
AssetsFrePrice,
AssetsFrePrice_fixedRateExchanges as AssetsFrePriceFixedRateExchanges
} from '../@types/apollo/AssetsFrePrice'
import { AssetPreviousOrder } from '../@types/apollo/AssetPreviousOrder'
import BigNumber from 'bignumber.js'
export interface PriceList {
[key: string]: string
}
const freQuery = gql`
query AssetFrePrice($datatoken_in: [String!]) {
export interface AssetListPrices {
ddo: DDO
price: BestPrice
}
interface DidAndDatatokenMap {
[name: string]: string
}
const FreQuery = gql`
query AssetsFrePrice($datatoken_in: [String!]) {
fixedRateExchanges(orderBy: id, where: { datatoken_in: $datatoken_in }) {
rate
id
@ -20,18 +38,42 @@ const freQuery = gql`
}
`
const poolQuery = gql`
query AssetPoolPrice($datatokenAddress_in: [String!]) {
pools(where: { datatokenAddress_in: $datatokenAddress_in }) {
spotPrice
consumePrice
const AssetFreQuery = gql`
query AssetFrePrice($datatoken: String) {
fixedRateExchanges(orderBy: id, where: { datatoken: $datatoken }) {
rate
id
datatokenAddress
}
}
`
const previousOrderQuery = gql`
const PoolQuery = gql`
query AssetsPoolPrice($datatokenAddress_in: [String!]) {
pools(where: { datatokenAddress_in: $datatokenAddress_in }) {
id
spotPrice
consumePrice
datatokenAddress
datatokenReserve
oceanReserve
}
}
`
const AssetPoolPriceQuerry = gql`
query AssetPoolPrice($datatokenAddress: String) {
pools(where: { datatokenAddress: $datatokenAddress }) {
id
spotPrice
consumePrice
datatokenAddress
datatokenReserve
oceanReserve
}
}
`
const PreviousOrderQuery = gql`
query AssetPreviousOrder($id: String!, $account: String!) {
tokenOrders(
first: 1
@ -53,7 +95,8 @@ async function fetchData(
const client = getApolloClientInstance()
const response = await client.query({
query: query,
variables: variables
variables: variables,
fetchPolicy: 'no-cache'
})
return response
} catch (error) {
@ -70,19 +113,18 @@ export async function getPreviousOrders(
id: id,
account: account
}
const fetchedPreviousOrders: any = await fetchData(
previousOrderQuery,
const fetchedPreviousOrders: ApolloQueryResult<AssetPreviousOrder> = await fetchData(
PreviousOrderQuery,
variables
)
if (fetchedPreviousOrders.data?.tokenOrders?.length === 0) return null
if (assetTimeout === '0') {
return fetchedPreviousOrders?.data?.tokenOrders[0]?.tx
} else {
const expiry = new BigNumber(
fetchedPreviousOrders?.data?.tokenOrders[0]?.timestamp
).plus(assetTimeout)
const unixTime = new BigNumber(Math.floor(Date.now() / 1000))
if (unixTime.isLessThan(expiry)) {
const expiry =
fetchedPreviousOrders?.data?.tokenOrders[0]?.timestamp * 1000 +
Number(assetTimeout) * 1000
if (Date.now() <= expiry) {
return fetchedPreviousOrders?.data?.tokenOrders[0]?.tx
} else {
return null
@ -90,9 +132,63 @@ export async function getPreviousOrders(
}
}
export async function getAssetPrices(assets: DDO[]): Promise<PriceList> {
const priceList: PriceList = {}
const didDTMap: any = {}
function transformPriceToBestPrice(
frePrice: AssetsFrePriceFixedRateExchanges[],
poolPrice: AssetsPoolPricePools[]
) {
if (poolPrice?.length > 0) {
const price: BestPrice = {
type: 'pool',
address: poolPrice[0]?.id,
value:
poolPrice[0]?.consumePrice === '-1'
? poolPrice[0]?.spotPrice
: poolPrice[0]?.consumePrice,
ocean: poolPrice[0]?.oceanReserve,
datatoken: poolPrice[0]?.datatokenReserve,
pools: [poolPrice[0]?.id],
isConsumable: poolPrice[0]?.consumePrice === '-1' ? 'false' : 'true'
}
return price
} else if (frePrice?.length > 0) {
// TODO Hacky hack, temporary™: set isConsumable to true for fre assets.
// isConsumable: 'true'
const price: BestPrice = {
type: 'exchange',
value: frePrice[0]?.rate,
address: frePrice[0]?.id,
exchange_id: frePrice[0]?.id,
ocean: 0,
datatoken: 0,
pools: [],
isConsumable: 'true'
}
return price
} else {
const price: BestPrice = {
type: '',
value: 0,
address: '',
exchange_id: '',
ocean: 0,
datatoken: 0,
pools: [],
isConsumable: 'false'
}
return price
}
}
async function getAssetsPoolsExchangesAndDatatokenMap(
assets: DDO[]
): Promise<
[
ApolloQueryResult<AssetsPoolPrice>,
ApolloQueryResult<AssetsFrePrice>,
DidAndDatatokenMap
]
> {
const didDTMap: DidAndDatatokenMap = {}
const dataTokenList: string[] = []
for (const ddo of assets) {
@ -105,16 +201,100 @@ export async function getAssetPrices(assets: DDO[]): Promise<PriceList> {
const poolVariables = {
datatokenAddress_in: dataTokenList
}
const poolPriceResponse: any = await fetchData(poolQuery, poolVariables)
const poolPriceResponse: ApolloQueryResult<AssetsPoolPrice> = await fetchData(
PoolQuery,
poolVariables
)
const frePriceResponse: ApolloQueryResult<AssetsFrePrice> = await fetchData(
FreQuery,
freVariables
)
return [poolPriceResponse, frePriceResponse, didDTMap]
}
export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
const priceList: PriceList = {}
const values: [
ApolloQueryResult<AssetsPoolPrice>,
ApolloQueryResult<AssetsFrePrice>,
DidAndDatatokenMap
] = await getAssetsPoolsExchangesAndDatatokenMap(assets)
const poolPriceResponse = values[0]
const frePriceResponse = values[1]
const didDTMap: DidAndDatatokenMap = values[2]
for (const poolPrice of poolPriceResponse.data?.pools) {
priceList[didDTMap[poolPrice.datatokenAddress]] =
poolPrice.consumePrice === '-1'
? poolPrice.spotPrice
: poolPrice.consumePrice
}
const frePriceResponse: any = await fetchData(freQuery, freVariables)
for (const frePrice of frePriceResponse.data?.fixedRateExchanges) {
priceList[didDTMap[frePrice.datatoken?.address]] = frePrice.rate
}
return priceList
}
export async function getPrice(asset: DDO): Promise<BestPrice> {
const freVariables = {
datatoken: asset?.dataToken.toLowerCase()
}
const poolVariables = {
datatokenAddress: asset?.dataToken.toLowerCase()
}
const poolPriceResponse: ApolloQueryResult<AssetsPoolPrice> = await fetchData(
AssetPoolPriceQuerry,
poolVariables
)
const frePriceResponse: ApolloQueryResult<AssetsFrePrice> = await fetchData(
AssetFreQuery,
freVariables
)
const bestPrice: BestPrice = transformPriceToBestPrice(
frePriceResponse.data.fixedRateExchanges,
poolPriceResponse.data.pools
)
return bestPrice
}
export async function getAssetsBestPrices(
assets: DDO[]
): Promise<AssetListPrices[]> {
const assetsWithPrice: AssetListPrices[] = []
const values: [
ApolloQueryResult<AssetsPoolPrice>,
ApolloQueryResult<AssetsFrePrice>,
DidAndDatatokenMap
] = await getAssetsPoolsExchangesAndDatatokenMap(assets)
const poolPriceResponse = values[0]
const frePriceResponse = values[1]
for (const ddo of assets) {
const dataToken = ddo.dataToken.toLowerCase()
const poolPrice: AssetsPoolPricePools[] = []
const frePrice: AssetsFrePriceFixedRateExchanges[] = []
const pool = poolPriceResponse.data?.pools.find(
(pool: any) => pool.datatokenAddress === dataToken
)
pool && poolPrice.push(pool)
const fre = frePriceResponse.data?.fixedRateExchanges.find(
(fre: any) => fre.datatoken.address === dataToken
)
fre && frePrice.push(fre)
const bestPrice = transformPriceToBestPrice(frePrice, poolPrice)
assetsWithPrice.push({
ddo: ddo,
price: bestPrice
})
}
return assetsWithPrice
}

View File

@ -1,4 +1,4 @@
import { Logger, ConfigHelperConfig } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
export interface EthereumListsChain {
name: string
@ -79,32 +79,33 @@ export function addCustomNetwork(
)
}
export function addOceanToWallet(
config: ConfigHelperConfig,
web3Provider: any
): void {
export async function addTokenToWallet(
web3Provider: any,
address: string,
symbol: string,
logo?: string
): Promise<void> {
const image =
logo ||
'https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png'
const tokenMetadata = {
type: 'ERC20',
options: {
address: config.oceanTokenAddress,
symbol: config.oceanTokenSymbol,
decimals: 18,
image:
'https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png'
}
options: { address, symbol, image, decimals: 18 }
}
web3Provider.sendAsync(
{
method: 'wallet_watchAsset',
params: tokenMetadata,
id: Math.round(Math.random() * 100000)
},
(err: string, added: any) => {
(err: { code: number; message: string }, added: any) => {
if (err || 'error' in added) {
Logger.error(
`Couldn't add ${tokenMetadata.options.symbol} (${
tokenMetadata.options.address
}) to MetaMask, error: ${err || added.error}`
}) to MetaMask, error: ${err.message || added.error}`
)
} else {
Logger.log(