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

Merge branch 'main' into issue510-match-providers-algo-dataset

This commit is contained in:
Bogdan Fazakas 2021-06-29 00:08:45 +03:00
commit b0356f559c
161 changed files with 52515 additions and 15897 deletions

View File

@ -1,10 +1,26 @@
# Default network, possible values:
# "development", "ropsten", "rinkeby", "mainnet", "polygon"
# "development", "ropsten", "rinkeby", "mainnet", "polygon", "moonbeamalpha"
GATSBY_NETWORK="rinkeby"
#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_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

@ -1,11 +1,12 @@
{
"parser": "babel-eslint",
"extends": ["eslint:recommended", "prettier"],
"env": { "es6": true, "browser": true, "node": true, "jest": true },
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": { "jsx": true }
},
"env": { "browser": true, "node": true, "es2020": true, "jest": true },
"settings": {
"react": {
"version": "detect"
}
"react": { "version": "detect" }
},
"overrides": [
{
@ -20,9 +21,6 @@
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/react",
"prettier/standard",
"prettier/@typescript-eslint",
"plugin:react-hooks/recommended"
],
"plugins": ["@typescript-eslint", "prettier"],

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16

View File

@ -25,6 +25,8 @@
- [🛳 Production](#-production)
- [⬆️ Deployment](#-deployment)
- [💖 Contributing](#-contributing)
- [🍴 Forking](#-forking)
- [💻 Advanced Features](#-advanced-features)
- [🏛 License](#-license)
## 🏄 Get Started
@ -37,6 +39,9 @@ To start local development:
git clone git@github.com:oceanprotocol/market.git
cd market
# when using nvm to manage Node.js versions
nvm use
npm install
npm start
```
@ -358,6 +363,28 @@ 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!
## 💻 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

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

View File

@ -4,6 +4,7 @@ module.exports = {
// networks in their wallet.
// Ocean Protocol contracts are deployed for: 'mainnet', 'rinkeby', 'development'
network: process.env.GATSBY_NETWORK || 'mainnet',
rbacUrl: process.env.GATSBY_RBAC_URL,
infuraProjectId: process.env.GATSBY_INFURA_PROJECT_ID || 'xxx',
@ -40,8 +41,13 @@ 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'
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',
credentialType: process.env.GATSBY_CREDENTIAL_TYPE || 'address'
}

View File

@ -20,6 +20,15 @@
"rows": 10,
"required": true
},
{
"name": "price",
"label": "New Price",
"type": "number",
"min": "1",
"placeholder": "0",
"help": "Enter a new price.",
"required": true
},
{
"name": "links",
"label": "Sample file",

View File

@ -0,0 +1,31 @@
{
"description": "Update advanced settings of this data set. Updating these settings will create an on-chain transaction you have to approve in your wallet.",
"form": {
"success": "🎉 Successfully updated. 🎉",
"successAction": "Close",
"error": "Updating DDO failed.",
"data": [
{
"name": "allow",
"label": "Allow ETH Address",
"placeholder": "e.g. 0x12345678901234567890abcd",
"help": "Enter ETH address and click ADD button to append the list. Only ETH address in allow list can consume this asset. If the list is empty means anyone can download or compute this asset",
"type": "credentials"
},
{
"name": "deny",
"label": "Deny ETH Address",
"placeholder": "e.g. 0x12345678901234567890abcd",
"help": "Enter ETH address and click ADD button to append the list. If ETH address is fall under deny list, download or compute of this asset is denied",
"type": "credentials"
},
{
"name": "isOrderDisabled",
"label": "Disable Consumption",
"help": "Disable dataset being download or compute when dataset undergoing maintenance.",
"type": "checkbox",
"options": ["Disable"]
}
]
}
}

View File

@ -6,13 +6,6 @@
"successAction": "Close",
"error": "Updating DDO failed.",
"data": [
{
"name": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
},
{
"name": "publisherTrustedAlgorithms",
"label": "Selected Algorithms",
@ -21,6 +14,13 @@
"multiple": true,
"options": [],
"sortOptions": false
},
{
"name": "allowAllPublishedAlgorithms",
"label": "All Algorithms",
"help": "Allow any published algorithm to run on this data set.",
"type": "checkbox",
"options": ["Allow any published algorithm"]
}
]
}

View File

@ -1,4 +1,7 @@
{
"title": "History",
"description": "Find the data sets and jobs that you previously accessed."
"description": "Find the data sets and jobs that you previously accessed.",
"compute": {
"storage": "Results are stored for 30 days."
}
}

View File

@ -19,7 +19,7 @@
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing.",
"help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. Some restrictions apply:\n\n- max. running time: 1 min.\n- [Writing Algorithms for Compute to Data](https://docs.oceanprotocol.com/tutorials/compute-to-data-algorithms/)",
"type": "files",
"required": true
},
@ -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
},
{
@ -56,6 +56,13 @@
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this algorithm will be created with this name & symbol.",
"required": true
},
{
"name": "entrypoint",
"label": "Entrypoint",

View File

@ -19,7 +19,7 @@
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing.",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files",
"required": true
},
@ -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
},

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).

View File

@ -43,7 +43,7 @@ exports.onCreatePage = async ({ page, actions }) => {
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
node: {
// 'fs' fix for squid.js
// 'fs' fix for ocean.js
fs: 'empty'
},
// fix for 'got'/'swarm-js' dependency

62289
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,122 +23,112 @@
"postinstall": "husky install"
},
"dependencies": {
"@apollo/client": "^3.3.11",
"@apollo/client": "^3.3.19",
"@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "^5.14.1",
"@loadable/component": "^5.15.0",
"@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.14.1",
"@oceanprotocol/lib": "^0.15.1",
"@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^3.0.3",
"@sindresorhus/slugify": "^1.0.0",
"@tippyjs/react": "^4.2.0",
"@types/classnames": "^2.2.11",
"@vercel/node": "^1.8.5",
"@walletconnect/web3-provider": "^1.3.4",
"@portis/web3": "^4.0.4",
"@sindresorhus/slugify": "^2.1.0",
"@tippyjs/react": "^4.2.5",
"@walletconnect/web3-provider": "^1.4.1",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
"classnames": "^2.2.6",
"cross-fetch": "^3.0.6",
"date-fns": "^2.16.1",
"classnames": "^2.3.1",
"cross-fetch": "^3.1.4",
"date-fns": "^2.22.1",
"decimal.js": "^10.2.1",
"dom-confetti": "^0.2.2",
"dotenv": "^8.2.0",
"dotenv": "^10.0.0",
"ethereum-address": "0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^6.1.0",
"formik": "^2.2.6",
"gatsby": "^2.30.2",
"filesize": "^6.3.0",
"formik": "^2.2.9",
"gatsby": "^2.32.13",
"gatsby-image": "^2.9.0",
"gatsby-plugin-manifest": "^2.10.0",
"gatsby-plugin-react-helmet": "^3.8.0",
"gatsby-plugin-remove-trailing-slashes": "^2.8.0",
"gatsby-plugin-sharp": "^2.12.1",
"gatsby-plugin-sharp": "^2.14.4",
"gatsby-plugin-svgr": "^2.1.0",
"gatsby-plugin-use-dark-mode": "^1.2.0",
"gatsby-plugin-webpack-size": "^1.0.0",
"gatsby-plugin-use-dark-mode": "^1.3.0",
"gatsby-plugin-webpack-size": "^2.0.1",
"gatsby-source-filesystem": "^2.9.0",
"gatsby-source-graphql": "^2.12.0",
"gatsby-transformer-json": "^2.9.0",
"gatsby-transformer-remark": "^2.14.0",
"gatsby-transformer-sharp": "^2.10.1",
"intersection-observer": "^0.12.0",
"is-url-superb": "^5.0.0",
"gatsby-transformer-remark": "^2.16.1",
"gatsby-transformer-sharp": "^2.12.1",
"graphql": "14.7.0",
"is-url-superb": "^6.0.0",
"jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0",
"query-string": "^6.13.8",
"react": "^17.0.1",
"react-chartjs-2": "^2.11.1",
"react-data-table-component": "^6.11.6",
"react-dom": "^17.0.1",
"query-string": "^7.0.0",
"react": "^17.0.2",
"react-chartjs-2": "^2.11.2",
"react-data-table-component": "^6.11.7",
"react-dom": "^17.0.2",
"react-dotdotdot": "^1.3.1",
"react-dropzone": "^11.2.4",
"react-helmet": "^6.1.0",
"react-intersection-observer": "^8.31.0",
"react-markdown": "^5.0.3",
"react-modal": "^3.12.1",
"react-paginate": "^7.0.0",
"react-spring": "^8.0.27",
"react-tabs": "^3.1.2",
"react-toastify": "^6.2.0",
"react-markdown": "^6.0.2",
"react-modal": "^3.14.2",
"react-paginate": "^7.1.3",
"react-spring": "^9.2.1",
"react-tabs": "^3.2.2",
"react-toastify": "^7.0.4",
"remove-markdown": "^0.3.0",
"shortid": "^2.2.16",
"slugify": "^1.4.6",
"swr": "^0.3.11",
"slugify": "^1.5.3",
"swr": "^0.5.6",
"use-dark-mode": "^2.3.1",
"web3": "^1.3.4",
"web3": "^1.3.6",
"web3modal": "^1.9.3",
"yup": "^0.32.6"
"yup": "^0.32.9"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@storybook/addon-actions": "^6.1.14",
"@storybook/addon-storyshots": "^6.1.14",
"@storybook/react": "^6.1.14",
"@svgr/webpack": "^5.5.0",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.3",
"@types/chart.js": "^2.9.29",
"@types/jest": "^26.0.20",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",
"@types/chart.js": "^2.9.32",
"@types/classnames": "^2.3.1",
"@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6",
"@types/node": "^14.14.20",
"@types/react": "^17.0.0",
"@types/react-helmet": "^6.1.0",
"@types/react-modal": "^3.10.6",
"@types/react-paginate": "^6.2.1",
"@types/node": "^15.6.1",
"@types/react": "^17.0.8",
"@types/react-helmet": "^6.1.1",
"@types/react-modal": "^3.12.0",
"@types/react-paginate": "^7.1.0",
"@types/react-tabs": "^2.3.2",
"@types/remove-markdown": "^0.1.1",
"@types/remove-markdown": "^0.3.0",
"@types/shortid": "0.0.29",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"apollo": "^2.32.1",
"babel-loader": "^8.2.2",
"babel-preset-react-app": "^10.0.0",
"eslint": "^7.17.0",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"apollo": "^2.33.4",
"eslint": "^7.27.0",
"eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^5.0.8",
"husky": "^6.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"prettier": "^2.2.1",
"prettier": "^2.3.0",
"pretty-quick": "^3.1.0",
"serve": "^11.3.2",
"source-map-explorer": "^2.5.2",
"typescript": "^4.1.3"
"typescript": "^4.3.2"
},
"repository": {
"type": "git",
"url": "https://github.com/oceanprotocol/market"
},
"engines": {
"node": ">=12"
"node": ">=14"
},
"browserslist": [
">0.2%",

View File

@ -1,11 +1,6 @@
export interface ComputeJobMetaData {
jobId: string
did: string
dateCreated: string
dateFinished: string
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
export interface ComputeJobMetaData extends ComputeJob {
assetName: string
status: number
statusText: string
algorithmLogUrl: string
resultsUrls: string[]
assetDtSymbol: string
}

View File

@ -49,6 +49,7 @@ export interface MetadataPublishFormAlgorithm {
dockerImage: string
algorithmPrivacy: boolean
timeout: string
dataTokenOptions: DataTokenOptions
termsAndConditions: boolean
// ---- optional fields ----
image: string
@ -61,6 +62,7 @@ export interface MetadataEditForm {
name: string
description: string
timeout: string
price?: number
links?: string | EditableMetadataLinks[]
}

View File

@ -1,5 +1,3 @@
declare module 'intersection-observer'
declare module 'ethereum-blockies' {
export function toDataUrl(address: string): string
}

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,9 +1,9 @@
.icon {
fill: var(--brand-grey-light);
width: 1.1em;
height: 1.1em;
fill: currentColor;
width: 1em;
height: 1em;
vertical-align: baseline;
margin-bottom: -0.2em;
margin-bottom: -0.1em;
display: inline-block;
}

View File

@ -3,6 +3,7 @@ import styles from './AssetType.module.css'
import classNames from 'classnames/bind'
import { ReactComponent as Compute } from '../../images/compute.svg'
import { ReactComponent as Download } from '../../images/download.svg'
import { ReactComponent as Lock } from '../../images/lock.svg'
const cx = classNames.bind(styles)
@ -25,6 +26,8 @@ export default function AssetType({
</div>
{accessType === 'access' ? (
<Download role="img" aria-label="Download" className={styles.icon} />
) : accessType === 'compute' && type === 'algorithm' ? (
<Lock role="img" aria-label="Private" className={styles.icon} />
) : (
<Compute role="img" aria-label="Compute" className={styles.icon} />
)}

View File

@ -5,6 +5,7 @@
margin: 0;
display: inline-block;
width: fit-content;
min-width: 7rem;
padding: calc(var(--spacer) / 3) var(--spacer);
font-size: var(--font-size-base);
font-family: var(--font-family-base);

View File

@ -1,57 +1,57 @@
import React from 'react'
import { action } from '@storybook/addon-actions'
import Button from './Button'
// import React from 'react'
// // import { action } from '@storybook/addon-actions'
// import Button from './Button'
export default {
title: 'Atoms/Button'
}
// export default {
// title: 'Atoms/Button'
// }
export const Default = () => (
<>
<Button onClick={action('clicked')}>Hello Button</Button>
<br />
<br />
<Button size="small" onClick={action('clicked')}>
Hello Button
</Button>
</>
)
// export const Default = () => (
// <>
// <Button onClick={action('clicked')}>Hello Button</Button>
// <br />
// <br />
// <Button size="small" onClick={action('clicked')}>
// Hello Button
// </Button>
// </>
// )
export const Primary = () => (
<>
<Button style="primary" onClick={action('clicked')}>
Hello Button
</Button>
<br />
<br />
<Button style="primary" size="small" onClick={action('clicked')}>
Hello Button
</Button>
</>
)
// export const Primary = () => (
// <>
// <Button style="primary" onClick={action('clicked')}>
// Hello Button
// </Button>
// <br />
// <br />
// <Button style="primary" size="small" onClick={action('clicked')}>
// Hello Button
// </Button>
// </>
// )
export const Ghost = () => (
<>
<Button style="ghost" onClick={action('clicked')}>
Hello Button
</Button>
<br />
<br />
<Button style="ghost" size="small" onClick={action('clicked')}>
Hello Button
</Button>
</>
)
// export const Ghost = () => (
// <>
// <Button style="ghost" onClick={action('clicked')}>
// Hello Button
// </Button>
// <br />
// <br />
// <Button style="ghost" size="small" onClick={action('clicked')}>
// Hello Button
// </Button>
// </>
// )
export const Text = () => (
<>
<Button style="text" onClick={action('clicked')}>
Hello Button
</Button>
<br />
<br />
<Button style="text" size="small" onClick={action('clicked')}>
Hello Button
</Button>
</>
)
// export const Text = () => (
// <>
// <Button style="text" onClick={action('clicked')}>
// Hello Button
// </Button>
// <br />
// <br />
// <Button style="text" size="small" onClick={action('clicked')}>
// Hello Button
// </Button>
// </>
// )

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

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

@ -25,3 +25,7 @@
width: 4.5rem;
padding: calc(var(--spacer) / 2) calc(var(--spacer) / 4);
}
.loaderWrap {
margin-right: calc(var(--spacer) / 6);
}

View File

@ -4,17 +4,28 @@ import filesize from 'filesize'
import classNames from 'classnames/bind'
import cleanupContentType from '../../utils/cleanupContentType'
import styles from './File.module.css'
import Loader from '../atoms/Loader'
const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
<Loader />
</div>
)
}
export default function File({
file,
className,
small
small,
isLoading
}: {
file: FileMetadata
className?: string
small?: boolean
isLoading?: boolean
}): ReactElement {
if (!file) return null
@ -26,17 +37,23 @@ export default function File({
return (
<ul className={styleClasses}>
{file.contentType || file.contentLength ? (
{isLoading === false || isLoading === undefined ? (
<>
<li>{cleanupContentType(file.contentType)}</li>
<li>
{file.contentLength && file.contentLength !== '0'
? filesize(Number(file.contentLength))
: ''}
</li>
{file.contentType || file.contentLength ? (
<>
<li>{cleanupContentType(file.contentType)}</li>
<li>
{file.contentLength && file.contentLength !== '0'
? filesize(Number(file.contentLength))
: ''}
</li>
</>
) : (
<li className={styles.empty}>No file info available</li>
)}
</>
) : (
<li className={styles.empty}>No file info available</li>
<LoaderArea />
)}
</ul>
)

View File

@ -4,11 +4,15 @@ 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, {
AssetSelectionAsset
} from '../../molecules/FormFields/AssetSelection'
import Credentials from '../../molecules/FormFields/Credential'
const cx = classNames.bind(styles)
@ -91,6 +95,7 @@ export default function InputElement({
id={slugify(option)}
type={type}
name={name}
defaultChecked={props.defaultChecked}
{...props}
/>
<label className={styles.radioLabel} htmlFor={slugify(option)}>
@ -103,7 +108,7 @@ export default function InputElement({
case 'assetSelection':
return (
<AssetSelection
assets={(options as unknown) as AssetSelectionAsset[]}
assets={options as unknown as AssetSelectionAsset[]}
{...field}
{...props}
/>
@ -111,7 +116,7 @@ export default function InputElement({
case 'assetSelectionMultiple':
return (
<AssetSelection
assets={(options as unknown) as AssetSelectionAsset[]}
assets={options as unknown as AssetSelectionAsset[]}
multiple
disabled={disabled}
{...field}
@ -124,6 +129,17 @@ 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}
/>
)
case 'credentials':
return <Credentials name={name} {...field} {...props} />
default:
return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -5,10 +5,6 @@
list-style-position: inside;
}
.item span {
color: var(--brand-grey-dark);
}
.ulItem {
list-style-type: square;
}

View File

@ -1,5 +1,7 @@
.loaderWrap {
display: flex;
align-items: center;
justify-content: center;
}
.loader {

View File

@ -13,10 +13,9 @@ const Markdown = ({
// https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313
const textCleaned = text?.replace(/\\n/g, '\n ')
return (
<ReactMarkdown
source={textCleaned}
className={`${styles.markdown} ${className}`}
/>
<ReactMarkdown className={`${styles.markdown} ${className}`}>
{textCleaned}
</ReactMarkdown>
)
}

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}
@ -24,17 +24,18 @@ export default function Price({
conversion={conversion}
type={price.type}
/>
) : !price || !price.address || price.address === '' ? (
) : !price || price?.type === '' ? (
<div className={styles.empty}>
No price set{' '}
<Tooltip content="No pricing mechanism has been set on this asset yet." />
</div>
) : price.isConsumable !== 'true' ? (
<div className={styles.empty}>
Low liquidity{' '}
<Tooltip content="This pool does not have enough liquidity for using this data set." />
</div>
) : (
// TODO: Hacky hack, put back some check for low liquidity
// ) : price.isConsumable !== 'true' ? (
// <div className={styles.empty}>
// Low liquidity{' '}
// <Tooltip content="This pool does not have enough liquidity for using this data set." />
// </div>
<Loader message="Retrieving price..." />
)
}

View File

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

View File

@ -0,0 +1,59 @@
.display {
composes: selection from './FormFields/AssetSelection.module.css';
}
.display [class*='loaderWrap'] {
margin: calc(var(--spacer) / 3);
}
.scroll {
composes: scroll from './FormFields/AssetSelection.module.css';
margin-top: 0;
border-top: none;
width: 100%;
}
.row {
composes: row from './FormFields/AssetSelection.module.css';
}
.row:last-child {
border-bottom: none;
}
.row:first-child {
border-top: none;
}
.row:hover {
background-color: var(--background-content);
}
.info {
display: block;
width: 100%;
}
.title {
composes: title from './FormFields/AssetSelection.module.css';
}
.hover:hover {
color: var(--color-primary);
}
.price {
composes: price from './FormFields/AssetSelection.module.css';
}
.price [class*='symbol'] {
font-size: calc(var(--font-size-small) / 1.2) !important;
}
.did {
composes: did from './FormFields/AssetSelection.module.css';
}
.empty {
composes: empty from './FormFields/AssetSelection.module.css';
}

View File

@ -0,0 +1,49 @@
import React from 'react'
import Dotdotdot from 'react-dotdotdot'
import { Link } from 'gatsby'
import PriceUnit from '../atoms/Price/PriceUnit'
import Loader from '../atoms/Loader'
import styles from './AssetComputeList.module.css'
import { AssetSelectionAsset } from './FormFields/AssetSelection'
function Empty() {
return <div className={styles.empty}>No assets found.</div>
}
export default function AssetComputeSelection({
assets
}: {
assets: AssetSelectionAsset[]
}): JSX.Element {
return (
<div className={styles.display}>
<div className={styles.scroll}>
{!assets ? (
<Loader />
) : assets && !assets.length ? (
<Empty />
) : (
assets.map((asset: AssetSelectionAsset) => (
<Link
to={`/asset/${asset.did}`}
className={styles.row}
key={asset.did}
>
<div className={styles.info}>
<h3 className={styles.title}>
<Dotdotdot clamp={1} tagName="span">
{asset.name}
</Dotdotdot>
</h3>
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
{asset.symbol} | {asset.did}
</Dotdotdot>
</div>
<PriceUnit price={asset.price} small className={styles.price} />
</Link>
))
)}
</div>
</div>
)
}

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

@ -107,7 +107,9 @@
.did {
padding: 0;
font-size: var(--font-size-mini);
/* font-size: var(--font-size-mini); */
/* hack to make DotDotDot clamp work in Safari*/
font-size: 0.63rem;
display: block;
text-align: left;
color: var(--color-secondary);

View File

@ -4,9 +4,9 @@ import slugify from 'slugify'
import classNames from 'classnames/bind'
import PriceUnit from '../../atoms/Price/PriceUnit'
import { ReactComponent as External } from '../../../images/external.svg'
import styles from './AssetSelection.module.css'
import InputElement from '../../atoms/Input/InputElement'
import Loader from '../../atoms/Loader'
import styles from './AssetSelection.module.css'
const cx = classNames.bind(styles)
@ -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

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

@ -0,0 +1,40 @@
.chip {
border: 1px solid var(--border-color);
display: flex;
padding-left: 10px;
}
.buttonWrapper {
width: 100%;
text-align: right;
}
.crossButton {
min-width: 0;
}
.crossButton svg {
display: inline-block;
width: var(--font-size-large);
height: var(--font-size-large);
fill: var(--brand-pink);
vertical-align: middle;
}
.scroll {
border-top: 1px solid var(--border-color);
min-height: fit-content;
max-height: 200px;
position: relative;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.credential {
padding: 0;
border: 1px solid var(--border-color);
background-color: var(--background-highlight);
border-radius: var(--border-radius);
font-size: var(--font-size-small);
min-height: 200px;
}

View File

@ -0,0 +1,81 @@
import { useField } from 'formik'
import { InputProps } from '../../../atoms/Input'
import React, { useState, ChangeEvent, FormEvent, useEffect } from 'react'
import InputGroup from '../../../atoms/Input/InputGroup'
import Button from '../../../atoms/Button'
import styles from './Credential.module.css'
import { isAddress } from 'web3-utils'
import { toast } from 'react-toastify'
import { ReactComponent as Cross } from '../../../../images/cross.svg'
import InputElement from '../../../atoms/Input/InputElement'
export default function Credentials(props: InputProps) {
const [field, meta, helpers] = useField(props.name)
const [arrayInput, setArrayInput] = useState<string[]>(field.value || [])
const [value, setValue] = useState('')
useEffect(() => {
helpers.setValue(arrayInput)
}, [arrayInput])
function handleDeleteChip(value: string) {
const newInput = arrayInput.filter((input) => input !== value)
setArrayInput(newInput)
helpers.setValue(newInput)
}
function handleAddValue(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
if (!isAddress(value)) {
toast.error('Wallet address is invalid')
return
}
if (arrayInput.includes(value)) {
toast.error('Wallet address already added into list')
return
}
setArrayInput((arrayInput) => [...arrayInput, value])
setValue('')
}
return (
<div className={styles.credential}>
<InputGroup>
<InputElement
type="text"
name="address"
size="default"
placeholder={props.placeholder}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
/>
<Button
onClick={(e: FormEvent<HTMLButtonElement>) => handleAddValue(e)}
>
Add
</Button>
</InputGroup>
<div className={styles.scroll}>
{arrayInput &&
arrayInput.map((value) => {
return (
<div className={styles.chip} key={value}>
<code>{value}</code>
<span className={styles.buttonWrapper}>
<Button
className={styles.crossButton}
style="text"
onClick={(even) => handleDeleteChip(value)}
>
<Cross />
</Button>
</span>
</div>
)
})}
</div>
</div>
)
}

View File

@ -1,5 +1,4 @@
import React, { ReactElement } from 'react'
import isUrl from 'is-url-superb'
import Button from '../../../atoms/Button'
import { FieldInputProps, useField } from 'formik'
import Loader from '../../../atoms/Loader'
@ -28,12 +27,8 @@ export default function FileInput({
<Button
style="primary"
size="small"
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)}
disabled={
!field.value ||
// weird static page build fix so is-url-superb won't error
!isUrl(typeof field.value === 'string' ? field.value : '')
}
onClick={(e: React.SyntheticEvent) => e.preventDefault()}
disabled={!field.value}
>
{isLoading ? <Loader /> : 'Add File'}
</Button>

View File

@ -14,7 +14,7 @@ export default function FilesInput(props: InputProps): ReactElement {
const [fileUrl, setFileUrl] = useState<string>()
const { config } = useOcean()
useEffect(() => {
function loadFileInfo() {
const source = axios.CancelToken.source()
async function validateUrl() {
@ -33,11 +33,16 @@ export default function FilesInput(props: InputProps): ReactElement {
setIsLoading(false)
}
}
fileUrl && validateUrl()
return () => {
source.cancel()
}
}
useEffect(() => {
loadFileInfo()
}, [fileUrl, config.providerUri])
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
@ -48,6 +53,11 @@ export default function FilesInput(props: InputProps): ReactElement {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault()
// In the case when the user re-add the same URL after it was removed (by accident or intentionally)
if (fileUrl === url) {
loadFileInfo()
}
setFileUrl(url)
}

View File

@ -14,6 +14,10 @@ const query = graphql`
export default function Terms(props: InputProps): ReactElement {
const data = useStaticQuery(query)
const termsProps: InputProps = {
...props,
defaultChecked: props.value.toString() === 'true'
}
return (
<>
@ -21,7 +25,7 @@ export default function Terms(props: InputProps): ReactElement {
className={styles.terms}
dangerouslySetInnerHTML={{ __html: data.terms.html }}
/>
<InputElement {...props} type="checkbox" />
<InputElement {...termsProps} type="checkbox" />
</>
)
}

View File

@ -135,6 +135,11 @@ export function MetadataAlgorithmPreview({
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.dataTokenOptions?.name && (
<p
className={styles.datatoken}
>{`${values.dataTokenOptions.name}${values.dataTokenOptions.symbol}`}</p>
)}
{values.description && <Description description={values.description} />}
<div className={styles.asset}>

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

@ -17,20 +17,38 @@ export default function SearchBar({
filters?: boolean
size?: 'small' | 'large'
}): ReactElement {
const [value, setValue] = useState(initialValue || '')
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
let [value, setValue] = useState(initialValue || '')
async function startSearch(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
if (value === '') return
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}`)
}
async function emptySearch() {
const searchParams = new URLSearchParams(window.location.href)
const text = searchParams.get('text')
if (text !== ('' || undefined || null)) {
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
navigate(`${url}&text=%20`)
}
}
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
e.target.value === '' && emptySearch()
}
return (
<form className={styles.form}>
<InputGroup>

View File

@ -1,14 +1,13 @@
.buttons {
display: flex;
justify-content: space-between;
display: grid;
gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8);
}
.button {
display: block;
flex: 0 0 48%;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
width: auto;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
border-radius: var(--border-radius);
text-transform: none;
}
@ -31,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,52 +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' },
{ name: 'Polygon/Matic', oceanConfig: 'polygon' }
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>Mainnet</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,24 @@
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 +52,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 +66,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

@ -82,10 +82,8 @@ export default function FormStartCompute({
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const {
isValid,
values
}: FormikContextType<{ algorithm: string }> = useFormikContext()
const { isValid, values }: FormikContextType<{ algorithm: string }> =
useFormikContext()
const { price, ddo } = useAsset()
const [totalPrice, setTotalPrice] = useState(price?.value)
@ -108,17 +106,21 @@ export default function FormStartCompute({
useEffect(() => {
if (!price || !algorithmPrice) return
const priceDataset = hasPreviousOrder ? 0 : Number(price.value)
const priceAlgo = hasPreviousOrderSelectedComputeAsset
? 0
: Number(algorithmPrice.value)
const priceDataset =
hasPreviousOrder || hasDatatoken ? 0 : Number(price.value)
const priceAlgo =
hasPreviousOrderSelectedComputeAsset || hasDatatokenSelectedComputeAsset
? 0
: Number(algorithmPrice.value)
setTotalPrice(priceDataset + priceAlgo)
}, [
price,
algorithmPrice,
hasPreviousOrder,
hasPreviousOrderSelectedComputeAsset
hasDatatoken,
hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset
])
return (
@ -138,7 +140,9 @@ export default function FormStartCompute({
hasPreviousOrderSelectedComputeAsset={
hasPreviousOrderSelectedComputeAsset
}
hasDatatoken={hasDatatoken}
selectedComputeAssetTimeout={selectedComputeAssetTimeout}
hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset}
algorithmPrice={algorithmPrice}
totalPrice={totalPrice}
/>
@ -162,6 +166,8 @@ export default function FormStartCompute({
stepText={stepText}
isLoading={isLoading}
type="submit"
priceType={price?.type}
algorithmPriceType={algorithmPrice?.type}
/>
</Form>
)

View File

@ -8,8 +8,10 @@ import styles from './PriceOutput.module.css'
interface PriceOutputProps {
totalPrice: number
hasPreviousOrder: boolean
hasDatatoken: boolean
assetTimeout: string
hasPreviousOrderSelectedComputeAsset: boolean
hasDatatokenSelectedComputeAsset: boolean
algorithmPrice: BestPrice
selectedComputeAssetTimeout: string
}
@ -17,11 +19,13 @@ interface PriceOutputProps {
function Row({
price,
hasPreviousOrder,
hasDatatoken,
timeout,
sign
}: {
price: number
hasPreviousOrder?: boolean
hasDatatoken?: boolean
timeout?: string
sign?: string
}) {
@ -30,7 +34,7 @@ function Row({
<div className={styles.sign}>{sign}</div>
<div>
<PriceUnit
price={hasPreviousOrder ? '0' : `${price}`}
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
small
className={styles.price}
/>
@ -48,8 +52,10 @@ function Row({
export default function PriceOutput({
totalPrice,
hasPreviousOrder,
hasDatatoken,
assetTimeout,
hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset,
algorithmPrice,
selectedComputeAssetTimeout
}: PriceOutputProps): ReactElement {
@ -63,11 +69,13 @@ export default function PriceOutput({
<div className={styles.calculation}>
<Row
hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken}
price={price?.value}
timeout={assetTimeout}
/>
<Row
hasPreviousOrder={hasPreviousOrderSelectedComputeAsset}
hasDatatoken={hasDatatokenSelectedComputeAsset}
price={algorithmPrice?.value}
timeout={selectedComputeAssetTimeout}
sign="+"

View File

@ -3,7 +3,6 @@ import {
DDO,
File as FileMetadata,
Logger,
ServiceType,
publisherTrustedAlgorithm,
BestPrice
} from '@oceanprotocol/lib'
@ -26,52 +25,39 @@ import {
getInitialValues,
validationSchema
} from '../../../../models/FormStartComputeDataset'
import { ComputeAlgorithm } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import {
ComputeAlgorithm,
ComputeOutput
} from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import axios from 'axios'
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 { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import AlgorithmDatasetsListForCompute from '../../AssetContent/AlgorithmDatasetsListForCompute'
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
const SuccessAction = () => (
<Button style="text" to="/history" size="small">
<Button style="text" to="/history?defaultTab=ComputeJobs" size="small">
Go to history
</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
}
}
`
export default function Compute({
isBalanceSufficient,
dtBalance,
file
file,
fileIsLoading
}: {
isBalanceSufficient: boolean
dtBalance: string
file: FileMetadata
fileIsLoading?: boolean
}): ReactElement {
const { marketFeeAddress } = useSiteMetadata()
const { appConfig } = useSiteMetadata()
const { accountId } = useWeb3()
const { ocean, account, config } = useOcean()
const { price, type, ddo } = useAsset()
@ -86,38 +72,15 @@ export default function Compute({
const [isPublished, setIsPublished] = useState(false)
const [hasPreviousDatasetOrder, setHasPreviousDatasetOrder] = useState(false)
const [previousDatasetOrderId, setPreviousDatasetOrderId] = useState<string>()
const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] = useState(
false
)
const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] =
useState(false)
const [algorithmDTBalance, setalgorithmDTBalance] = useState<string>()
const [algorithmPrice, setAlgorithmPrice] = useState<BestPrice>()
const [variables, setVariables] = useState({})
const [
previousAlgorithmOrderId,
setPreviousAlgorithmOrderId
] = useState<string>()
const [previousAlgorithmOrderId, setPreviousAlgorithmOrderId] =
useState<string>()
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
@ -163,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`
@ -213,37 +176,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].spotPrice
}))
}, [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(() => {
@ -259,26 +195,30 @@ export default function Compute({
}, [ocean, ddo, accountId])
useEffect(() => {
if (!ocean || !accountId || !selectedAlgorithmAsset) return
if (!selectedAlgorithmAsset) return
if (selectedAlgorithmAsset.findServiceByType('access')) {
checkPreviousOrders(selectedAlgorithmAsset).then(() => {
if (
!hasPreviousAlgorithmOrder &&
selectedAlgorithmAsset.findServiceByType('compute')
) {
checkPreviousOrders(selectedAlgorithmAsset)
}
})
} else if (selectedAlgorithmAsset.findServiceByType('compute')) {
checkPreviousOrders(selectedAlgorithmAsset)
}
checkAssetDTBalance(selectedAlgorithmAsset)
initMetadata(selectedAlgorithmAsset)
const { timeout } = (
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
).attributes.main
setAlgorithmTimeout(secondsToString(timeout))
if (accountId) {
if (selectedAlgorithmAsset.findServiceByType('access')) {
checkPreviousOrders(selectedAlgorithmAsset).then(() => {
if (
!hasPreviousAlgorithmOrder &&
selectedAlgorithmAsset.findServiceByType('compute')
) {
checkPreviousOrders(selectedAlgorithmAsset)
}
})
} else if (selectedAlgorithmAsset.findServiceByType('compute')) {
checkPreviousOrders(selectedAlgorithmAsset)
}
}
ocean && checkAssetDTBalance(selectedAlgorithmAsset)
}, [selectedAlgorithmAsset, ocean, accountId, hasPreviousAlgorithmOrder])
// Output errors in toast UI
@ -358,7 +298,9 @@ export default function Compute({
ddo.id,
computeService.index,
computeAlgorithm,
marketFeeAddress
appConfig.marketFeeAddress,
undefined,
false
)
assetOrderId &&
@ -376,7 +318,9 @@ export default function Compute({
serviceAlgo.type,
accountId,
serviceAlgo.index,
marketFeeAddress
appConfig.marketFeeAddress,
undefined,
false
)
algorithmAssetOrderId &&
@ -395,7 +339,10 @@ export default function Compute({
computeAlgorithm.transferTxId = algorithmAssetOrderId
Logger.log('[compute] Starting compute job.')
const output = {}
const output: ComputeOutput = {
publishAlgorithmLog: true,
publishOutput: true
}
const response = await ocean.compute.start(
ddo.id,
assetOrderId,
@ -414,9 +361,12 @@ export default function Compute({
Logger.log('[compute] Starting compute job response: ', response)
setHasPreviousDatasetOrder(true)
await checkPreviousOrders(selectedAlgorithmAsset)
await checkPreviousOrders(ddo)
setIsPublished(true)
} catch (error) {
await checkPreviousOrders(selectedAlgorithmAsset)
await checkPreviousOrders(ddo)
setError('Failed to start job!')
Logger.error('[compute] Failed to start job: ', error.message)
} finally {
@ -427,15 +377,18 @@ export default function Compute({
return (
<>
<div className={styles.info}>
<File file={file} small />
<File file={file} isLoading={fileIsLoading} small />
<Price price={price} conversion />
</div>
{type === 'algorithm' ? (
<Alert
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
state="info"
/>
<>
<Alert
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
state="info"
/>
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
</>
) : (
<Formik
initialValues={getInitialValues()}
@ -475,7 +428,9 @@ export default function Compute({
action={<SuccessAction />}
/>
)}
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
{type !== 'algorithm' && (
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
)}
</footer>
</>
)

View File

@ -5,7 +5,10 @@
.info {
display: flex;
width: 100%;
width: auto;
margin-left: -2rem;
margin-right: -2rem;
padding: 0 calc(var(--spacer)) 0 calc(var(--spacer));
}
.filewrapper {

View File

@ -16,6 +16,7 @@ import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing'
import { useConsume } from '../../../hooks/useConsume'
import ButtonBuy from '../../atoms/ButtonBuy'
import AlgorithmDatasetsListForCompute from '../AssetContent/AlgorithmDatasetsListForCompute'
const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) {
@ -35,31 +36,28 @@ export default function Consume({
ddo,
file,
isBalanceSufficient,
dtBalance
dtBalance,
fileIsLoading
}: {
ddo: DDO
file: FileMetadata
isBalanceSufficient: boolean
dtBalance: string
fileIsLoading?: boolean
}): ReactElement {
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { marketFeeAddress } = useSiteMetadata()
const { appConfig } = useSiteMetadata()
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, price, type } = useAsset()
const {
buyDT,
pricingStepText,
pricingError,
pricingIsLoading
} = usePricing()
const { consumeStepText, consume, consumeError } = useConsume()
const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
usePricing()
const { consumeStepText, consume, consumeError, isLoading } = useConsume()
const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumable, setIsConsumable] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('')
const { data } = useQuery<OrdersData>(previousOrderQuery, {
variables: {
id: ddo.dataToken?.toLowerCase(),
@ -90,7 +88,7 @@ export default function Consume({
useEffect(() => {
const { timeout } = ddo.findServiceByType('access').attributes.main
setAssetTimeout(secondsToString(timeout))
setAssetTimeout(timeout.toString())
}, [ddo])
useEffect(() => {
@ -126,22 +124,28 @@ export default function Consume({
])
async function handleConsume() {
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price, ddo))
await consume(
if (!hasPreviousOrder && !hasDatatoken) {
const tx = await buyDT('1', price, ddo)
if (tx === undefined) return
}
const error = await consume(
ddo.id,
ddo.dataToken,
'access',
marketFeeAddress,
appConfig.marketFeeAddress,
previousOrderId
)
setHasPreviousOrder(true)
error || setHasPreviousOrder(true)
}
// Output errors in UI
useEffect(() => {
consumeError && toast.error(consumeError)
}, [consumeError])
useEffect(() => {
pricingError && toast.error(pricingError)
}, [consumeError, pricingError])
}, [pricingError])
const PurchaseButton = () => (
<ButtonBuy
@ -152,10 +156,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={pricingIsLoading || isLoading}
priceType={price?.type}
/>
)
@ -163,13 +168,16 @@ export default function Consume({
<aside className={styles.consume}>
<div className={styles.info}>
<div className={styles.filewrapper}>
<File file={file} />
<File file={file} isLoading={fileIsLoading} />
</div>
<div className={styles.pricewrapper}>
<Price price={price} conversion />
{!isInPurgatory && <PurchaseButton />}
</div>
</div>
{type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} />
)}
<footer className={styles.feedback}>
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
</footer>

View File

@ -0,0 +1,48 @@
import { DDO, Credentials, CredentialType } from '@oceanprotocol/lib'
import React, { ReactElement, useEffect, useState } from 'react'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
import { useOcean } from '../../../../providers/Ocean'
import DebugOutput from '../../../atoms/DebugOutput'
export interface AdvancedSettings {
credentail: Credentials
isOrderDisabled: boolean
}
export default function DebugEditCredential({
values,
ddo,
credentialType
}: {
values: AdvancedSettingsForm
ddo: DDO
credentialType: CredentialType
}): ReactElement {
const { ocean } = useOcean()
const [advancedSettings, setAdvancedSettings] = useState<AdvancedSettings>()
useEffect(() => {
if (!ocean) return
async function transformValues() {
const newDdo = await ocean.assets.updateCredentials(
ddo,
credentialType,
values.allow,
values.deny
)
setAdvancedSettings({
credentail: newDdo.credentials,
isOrderDisabled: values.isOrderDisabled
})
}
transformValues()
}, [values, ddo, ocean])
return (
<>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed Form Values" output={advancedSettings} />
</>
)
}

View File

@ -13,10 +13,8 @@ export default function DebugEditCompute({
ddo: DDO
}): ReactElement {
const { ocean } = useOcean()
const [
formTransformed,
setFormTransformed
] = useState<ServiceComputePrivacy>()
const [formTransformed, setFormTransformed] =
useState<ServiceComputePrivacy>()
useEffect(() => {
if (!ocean) return

View File

@ -0,0 +1,163 @@
import { Formik } from 'formik'
import React, { ReactElement, useState } from 'react'
import { useAsset } from '../../../../providers/Asset'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import styles from './index.module.css'
import { Logger, CredentialType, DDO } from '@oceanprotocol/lib'
import MetadataFeedback from '../../../molecules/MetadataFeedback'
import { graphql, useStaticQuery } from 'gatsby'
import { useWeb3 } from '../../../../providers/Web3'
import { useOcean } from '../../../../providers/Ocean'
import FormAdvancedSettings from './FormAdvancedSettings'
import {
AdvancedSettingsForm,
getInitialValues,
validationSchema
} from '../../../../models/FormEditCredential'
import DebugEditCredential from './DebugEditAdvancedSettings'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
const contentQuery = graphql`
query EditAvanceSettingsQuery {
content: allFile(
filter: { relativePath: { eq: "pages/editAdvancedSettings.json" } }
) {
edges {
node {
childPagesJson {
description
form {
success
successAction
error
data {
name
placeholder
label
help
type
options
}
}
}
}
}
}
}
`
function getDefaultCredentialType(credentialType: string): CredentialType {
switch (credentialType) {
case 'address':
return CredentialType.address
case 'credential3Box':
return CredentialType.credential3Box
default:
return CredentialType.address
}
}
export default function EditAdvancedSettings({
setShowEdit
}: {
setShowEdit: (show: boolean) => void
}): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { metadata, ddo, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const { appConfig } = useSiteMetadata()
const hasFeedback = error || success
const credentialType = getDefaultCredentialType(appConfig.credentialType)
async function handleSubmit(
values: Partial<AdvancedSettingsForm>,
resetForm: () => void
) {
try {
let newDdo: DDO
newDdo = await ocean.assets.updateCredentials(
ddo,
credentialType,
values.allow,
values.deny
)
newDdo = await ocean.assets.editMetadata(newDdo, {
status: {
isOrderDisabled: values.isOrderDisabled
}
})
const storedddo = await ocean.assets.updateMetadata(newDdo, accountId)
if (!storedddo) {
setError(content.form.error)
Logger.error(content.form.error)
return
} else {
setSuccess(content.form.success)
resetForm()
}
} catch (error) {
Logger.error(error.message)
setError(error.message)
}
}
return (
<Formik
initialValues={getInitialValues(ddo, credentialType)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
await handleSubmit(values, resetForm)
}}
>
{({ isSubmitting, values }) =>
isSubmitting || hasFeedback ? (
<MetadataFeedback
title="Updating Data Set"
error={error}
success={success}
setError={setError}
successAction={{
name: content.form.successAction,
onClick: async () => {
await refreshDdo()
setShowEdit(false)
}
}}
/>
) : (
<>
<p className={styles.description}>{content.description}</p>
<article className={styles.grid}>
<FormAdvancedSettings
data={content.form.data}
setShowEdit={setShowEdit}
/>
</article>
{debug === true && (
<div className={styles.grid}>
<DebugEditCredential
values={values}
ddo={ddo}
credentialType={credentialType}
/>
</div>
)}
</>
)
}
</Formik>
)
}

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

@ -0,0 +1,59 @@
import React, { ChangeEvent, ReactElement } from 'react'
import styles from './FormEditMetadata.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { FormFieldProps } from '../../../../@types/Form'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { AdvancedSettingsForm } from '../../../../models/FormEditCredential'
export default function FormAdvancedSettings({
data,
setShowEdit
}: {
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
}): ReactElement {
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<AdvancedSettingsForm>> = useFormikContext()
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
if (e.target.type === 'checkbox')
setFieldValue(field.name, e.target.checked)
else setFieldValue(field.name, e.target.value)
}
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
</Form>
)
}

View File

@ -29,22 +29,19 @@ export default function FormEditComputeDataset({
const { accountId } = useWeb3()
const { ocean, config } = useOcean()
const { ddo } = useAsset()
const {
isValid,
values
}: FormikContextType<ComputePrivacyForm> = useFormikContext()
const { isValid, values }: FormikContextType<ComputePrivacyForm> =
useFormikContext()
const [allAlgorithms, setAllAlgorithms] = useState<AssetSelectionAsset[]>()
const { publisherTrustedAlgorithms } = ddo?.findServiceByType(
'compute'
).attributes.main.privacy
const { publisherTrustedAlgorithms } =
ddo?.findServiceByType('compute').attributes.main.privacy
async function getAlgorithmList(
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
): 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

@ -47,15 +47,17 @@ export default function FormEditMetadata({
data,
setShowEdit,
setTimeoutStringValue,
values
values,
showPrice
}: {
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishFormDataset>
showPrice: boolean
}): ReactElement {
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { ocean, config } = useOcean()
const {
isValid,
validateField,
@ -79,16 +81,20 @@ export default function FormEditMetadata({
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
{data.map(
(field: FormFieldProps) =>
(!showPrice && field.name === 'price') || (
<Field
key={field.name}
{...field}
component={Input}
prefix={field.name === 'price' && config.oceanTokenSymbol}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
)
)}
<footer className={styles.actions}>
<Button

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 {
@ -36,6 +40,7 @@ const contentQuery = graphql`
label
help
type
min
required
sortOptions
options
@ -60,7 +65,7 @@ export default function Edit({
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 [timeoutStringValue, setTimeoutStringValue] = useState<string>()
@ -70,11 +75,32 @@ export default function Edit({
const hasFeedback = error || success
async function updateFixedPrice(newPrice: number) {
const setPriceResp = await ocean.fixedRateExchange.setRate(
price.address,
newPrice,
accountId
)
if (!setPriceResp) {
setError(content.form.error)
Logger.error(content.form.error)
}
}
async function handleSubmit(
values: Partial<MetadataEditForm>,
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,
@ -82,6 +108,10 @@ export default function Edit({
links: typeof values.links !== 'string' ? values.links : []
})
price.type === 'exchange' &&
values.price !== price.value &&
(await updateFixedPrice(values.price))
if (!ddoEditedMetdata) {
setError(content.form.error)
Logger.error(content.form.error)
@ -115,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()
@ -127,7 +166,7 @@ export default function Edit({
return (
<Formik
initialValues={getInitialValues(metadata, timeout)}
initialValues={getInitialValues(metadata, timeout, price.value)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen
@ -160,6 +199,7 @@ export default function Edit({
setShowEdit={setShowEdit}
setTimeoutStringValue={setTimeoutStringValue}
values={initialValues}
showPrice={price.type === 'exchange'}
/>
<aside>

View File

@ -90,7 +90,7 @@ export default function FormAdd({
amountMax,
coin,
poolAddress,
ocean.pool,
ocean?.pool,
setNewPoolTokens,
setNewPoolShare
])

View File

@ -48,11 +48,8 @@ export default function Output({
coin: string
}): ReactElement {
const data = useStaticQuery(contentQuery)
const {
help,
titleIn,
titleOut
} = data.content.edges[0].node.childContentJson.pool.add.output
const { help, titleIn, titleOut } =
data.content.edges[0].node.childContentJson.pool.add.output
// Connect with form
const { values }: FormikContextType<FormAddLiquidity> = useFormikContext()

View File

@ -174,7 +174,7 @@ export default function Add({
) : (
<Alert
className={styles.warning}
text={content.warning.main}
text={content.warning}
state="info"
action={{
name: 'I understand',

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

@ -21,6 +21,7 @@ import UserLiquidity from '../../../atoms/UserLiquidity'
import InputElement from '../../../atoms/Input/InputElement'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import Decimal from 'decimal.js'
const contentQuery = graphql`
query PoolRemoveQuery {
@ -80,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 {
@ -155,21 +158,19 @@ export default function Remove({
totalPoolTokens
])
async function calculateAmountOfOceansRemoved(amountPoolShares: string) {
const oceanAmount = await ocean.pool.getOceanRemovedforPoolShares(
poolAddress,
amountPoolShares
)
setAmountOcean(oceanAmount)
}
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
@ -177,19 +178,24 @@ export default function Remove({
setAmountPercent(e.target.value)
if (!poolTokens) return
const amountPoolShares = (Number(e.target.value) / 100) * Number(poolTokens)
setAmountPoolShares(`${amountPoolShares}`)
calculateAmountOfOceansRemoved(`${amountPoolShares}`)
const amountPoolShares = new Decimal(e.target.value)
.dividedBy(100)
.mul(new Decimal(poolTokens))
.toString()
setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`)
}
function handleMaxButton(e: ChangeEvent<HTMLInputElement>) {
e.preventDefault()
setAmountPercent(amountMaxPercent)
const amountPoolShares =
(Number(amountMaxPercent) / 100) * Number(poolTokens)
setAmountPoolShares(`${amountPoolShares}`)
calculateAmountOfOceansRemoved(`${amountPoolShares}`)
const amountPoolShares = new Decimal(amountMaxPercent)
.dividedBy(100)
.mul(new Decimal(poolTokens))
.toString()
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

@ -44,6 +44,7 @@ const poolLiquidityQuery = gql`
id
totalShares
swapFee
spotPrice
tokens {
tokenAddress
balance
@ -82,10 +83,8 @@ export default function Pool(): ReactElement {
const [totalUserLiquidityInOcean, setTotalUserLiquidityInOcean] = useState(0)
const [totalLiquidityInOcean, setTotalLiquidityInOcean] = useState(0)
const [
creatorTotalLiquidityInOcean,
setCreatorTotalLiquidityInOcean
] = useState(0)
const [creatorTotalLiquidityInOcean, setCreatorTotalLiquidityInOcean] =
useState(0)
const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>()
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
@ -94,8 +93,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
})
@ -141,7 +140,8 @@ export default function Pool(): ReactElement {
setCreatorLiquidity(creatorLiquidity)
const totalCreatorLiquidityInOcean =
creatorLiquidity?.ocean + creatorLiquidity?.datatoken * price?.value
creatorLiquidity?.ocean +
creatorLiquidity?.datatoken * dataLiquidity.pool.spotPrice
setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean)
const creatorPoolShare =
price?.ocean &&
@ -168,7 +168,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])
@ -250,7 +251,7 @@ export default function Pool(): ReactElement {
<ExplorerLink
networkId={networkId}
path={
networkId === 137
networkId === 137 || networkId === 1287
? `tokens/${ddo.dataToken}`
: `token/${ddo.dataToken}`
}

View File

@ -9,10 +9,11 @@ export async function getMaxPercentRemove(
poolAddress
)
const amountMaxPoolShares = await ocean.pool.getPoolSharesRequiredToRemoveOcean(
poolAddress,
amountMaxOcean
)
const amountMaxPoolShares =
await ocean.pool.getPoolSharesRequiredToRemoveOcean(
poolAddress,
amountMaxOcean
)
let amountMaxPercent = `${Math.floor(
(Number(amountMaxPoolShares) / Number(poolTokens)) * 100

View File

@ -64,7 +64,7 @@ export default function FormTrade({
.required('Required')
.nullable(),
datatoken: Yup.number()
.max(maxDt, `Must be less or equal than ${maximumDt}`)
.max(maximumDt, (param) => `Must be less or equal than ${param.max}`)
.min(0.00001, (param) => `Must be more or equal to ${param.min}`)
.required('Required')
.nullable(),

View File

@ -6,10 +6,8 @@ import styles from './Slippage.module.css'
export default function Slippage(): ReactElement {
// Connect with form
const {
setFieldValue,
values
}: FormikContextType<FormTradeData> = useFormikContext()
const { setFieldValue, values }: FormikContextType<FormTradeData> =
useFormikContext()
function handleChange(e: ChangeEvent<HTMLSelectElement>) {
setFieldValue('slippage', e.target.value)

View File

@ -1,8 +1,9 @@
import React, { ReactElement, useState, useEffect } from 'react'
import Permission from '../Permission'
import styles from './index.module.css'
import Compute from './Compute'
import Consume from './Consume'
import { Logger } from '@oceanprotocol/lib'
import { Logger, File as FileMetadata, DID } from '@oceanprotocol/lib'
import Tabs from '../../atoms/Tabs'
import compareAsBN from '../../../utils/compareAsBN'
import Pool from './Pool'
@ -10,21 +11,47 @@ import Trade from './Trade'
import { useAsset } from '../../../providers/Asset'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import { fileinfo, getFileInfo } from '../../../utils/provider'
import axios from 'axios'
export default function AssetActions(): ReactElement {
const { accountId } = useWeb3()
const { ocean, balance, account } = useOcean()
const { price, ddo, metadata } = useAsset()
const { config, ocean, balance, account } = useOcean()
const { price, ddo } = useAsset()
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
const [fileMetadata, setFileMetadata] = useState<FileMetadata>(Object)
const [fileIsLoading, setFileIsLoading] = useState<boolean>(false)
const isCompute = Boolean(ddo?.findServiceByType('compute'))
useEffect(() => {
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
useEffect(() => {
if (!ocean || !accountId) return
async function init() {
try {
const dtBalance = await ocean.datatokens.balance(
@ -41,6 +68,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(
@ -56,14 +84,16 @@ export default function AssetActions(): ReactElement {
<Compute
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
file={metadata?.main.files[0]}
file={fileMetadata}
fileIsLoading={fileIsLoading}
/>
) : (
<Consume
ddo={ddo}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
file={metadata?.main.files[0]}
file={fileMetadata}
fileIsLoading={fileIsLoading}
/>
)
@ -74,10 +104,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',
@ -89,5 +116,9 @@ export default function AssetActions(): ReactElement {
}
)
return <Tabs items={tabs} className={styles.actions} />
return (
<Permission eventType="consume">
<Tabs items={tabs} className={styles.actions} />
</Permission>
)
}

View File

@ -0,0 +1,29 @@
.datasetsContainer {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
width: auto;
margin-left: -2rem;
margin-right: -2rem;
border-top: 1px solid var(--border-color);
margin-top: calc(var(--spacer) / 2);
}
.datasetsContainer div[class*='AssetSelection-module--selection'] {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-left: 0;
border-right: 0;
padding: 0;
}
.datasetsContainer .text {
margin-bottom: calc(var(--spacer) / 2);
margin-top: calc(var(--spacer) / 2);
text-align: center;
color: var(--font-color-text);
font-size: var(--font-size-base);
font-family: var(--font-family-heading);
}

View File

@ -0,0 +1,36 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './AlgorithmDatasetsListForCompute.module.css'
import { getAlgorithmDatasetsForCompute } from '../../../utils/aquarius'
import { AssetSelectionAsset } from '../../molecules/FormFields/AssetSelection'
import AssetComputeList from '../../molecules/AssetComputeList'
import { useOcean } from '../../../providers/Ocean'
import { useAsset } from '../../../providers/Asset'
export default function AlgorithmDatasetsListForCompute({
algorithmDid
}: {
algorithmDid: string
}): ReactElement {
const { config } = useOcean()
const { type } = useAsset()
const [datasetsForCompute, setDatasetsForCompute] =
useState<AssetSelectionAsset[]>()
useEffect(() => {
async function getDatasetsAllowedForCompute() {
const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid,
config.metadataCacheUri
)
setDatasetsForCompute(datasets)
}
type === 'algorithm' && getDatasetsAllowedForCompute()
}, [type])
return (
<div className={styles.datasetsContainer}>
<h3 className={styles.text}>Datasets algorithm is allowed to run on</h3>
<AssetComputeList assets={datasetsForCompute} />
</div>
)
}

View File

@ -1,7 +1,7 @@
.bookmark {
position: absolute;
top: -10px;
right: calc(var(--spacer) / 4);
right: calc(var(--spacer) / 8);
appearance: none;
background: none;
border: none;

View File

@ -6,12 +6,10 @@ import { useAsset } from '../../../providers/Asset'
export default function MetaFull(): ReactElement {
const { ddo, metadata, isInPurgatory, type } = useAsset()
const { algorithm } = ddo.findServiceByType('metadata').attributes.main
function DockerImage() {
const algorithmContainer = ddo.findServiceByType('metadata').attributes.main
.algorithm.container
const { image } = algorithmContainer
const { tag } = algorithmContainer
const { image, tag } = algorithm.container
return <span>{`${image}:${tag}`}</span>
}
@ -25,7 +23,7 @@ export default function MetaFull(): ReactElement {
content={<Publisher account={ddo?.publicKey[0].owner} />}
/>
{type === 'algorithm' && (
{type === 'algorithm' && algorithm && (
<MetaItem title="Docker Image" content={<DockerImage />} />
)}
<MetaItem title="DID" content={<code>{ddo?.id}</code>} />

View File

@ -1,25 +1,51 @@
.meta {
margin-bottom: calc(var(--spacer) / 1.5);
color: var(--color-secondary);
}
.meta p {
margin-bottom: 0;
}
.date {
font-size: var(--font-size-small);
}
.typeAndDate {
margin-bottom: calc(var(--spacer) / 12);
display: flex;
.asset {
margin-left: -2rem;
margin-right: -2rem;
padding-left: 2rem;
padding-right: 3rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: calc(var(--spacer) / 1.5);
padding-bottom: calc(var(--spacer) / 1.75);
}
.typeDetails {
@media (min-width: 40rem) {
.asset {
margin-top: -0.65rem;
}
}
.assetType {
display: inline-block;
border-right: 1px solid var(--border-color);
padding-right: calc(var(--spacer) / 3.5);
margin-right: calc(var(--spacer) / 4);
width: auto;
}
.datatoken {
white-space: pre;
margin-right: calc(var(--spacer) / 3);
}
.byline {
font-size: var(--font-size-small);
}
.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,47 +3,65 @@ 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'
return (
<aside className={styles.meta}>
<div className={styles.typeAndDate}>
<header className={styles.asset}>
<AssetType
type={type}
accessType={accessType}
className={styles.typeDetails}
className={styles.assetType}
/>
<p className={styles.date}>
<Time date={ddo?.created} relative />
{ddo?.created !== ddo?.updated && (
<>
{' — '}
updated <Time date={ddo?.updated} relative />
</>
)}
</p>
</div>
<p>
<ExplorerLink
className={styles.datatoken}
networkId={networkId}
path={
networkId === 137
networkId === 137 || networkId === 1287
? `tokens/${ddo?.dataToken}`
: `token/${ddo?.dataToken}`
}
>
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink>
</p>
Published By <Publisher account={owner} />
{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}>
Published By <Publisher account={owner} />
<p>
<Time date={ddo?.created} relative />
{ddo?.created !== ddo?.updated && (
<>
{' — '}
<span className={styles.updated}>
updated <Time date={ddo?.updated} relative />
</span>
</>
)}
</p>
</div>
</aside>
)
}

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'
@ -25,19 +26,15 @@ export default function FormPricing({
// Connect with form
const { values, setFieldValue, submitForm } = useFormikContext()
const {
price,
oceanAmount,
weightOnOcean,
weightOnDataToken,
type
} = values as PriceOptionsMarket
const { price, oceanAmount, weightOnOcean, weightOnDataToken, type } =
values as PriceOptionsMarket
// Switch type value upon tab change
function handleTabChange(tabName: string) {
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
@ -62,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
}
}
}
}
@ -57,12 +61,8 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
const [showPricing, setShowPricing] = useState(false)
const [success, setSuccess] = useState<string>()
const {
createPricing,
pricingIsLoading,
pricingError,
pricingStepText
} = usePricing()
const { createPricing, pricingIsLoading, pricingError, pricingStepText } =
usePricing()
const hasFeedback = pricingIsLoading || typeof success !== 'undefined'

View File

@ -17,6 +17,8 @@ import MetaMain from './MetaMain'
import EditHistory from './EditHistory'
import { useWeb3 } from '../../../providers/Web3'
import styles from './index.module.css'
import EditAdvancedSettings from '../AssetActions/Edit/EditAdvancedSettings'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export interface AssetContentProps {
path?: string
@ -48,14 +50,19 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>()
const [showEditCompute, setShowEditCompute] = useState<boolean>()
const { ddo, price, metadata } = useAsset()
const isOwner = accountId === owner
const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
useState<boolean>()
const [isOwner, setIsOwner] = useState(false)
const { ddo, price, metadata, type } = useAsset()
const { appConfig } = useSiteMetadata()
useEffect(() => {
if (!price) return
if (!accountId || !owner) return
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner)
setShowPricing(isOwner && price.type === '')
}, [isOwner, price])
}, [accountId, price, owner])
function handleEditButton() {
// move user's focus to top of screen
@ -68,10 +75,17 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
setShowEditCompute(true)
}
function handleEditAdvancedSettingsButton() {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setShowEditAdvancedSettings(true)
}
return showEdit ? (
<Edit setShowEdit={setShowEdit} />
) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} />
) : showEditAdvancedSettings ? (
<EditAdvancedSettings setShowEdit={setShowEditAdvancedSettings} />
) : (
<article className={styles.grid}>
<div>
@ -101,7 +115,19 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
<Button style="text" size="small" onClick={handleEditButton}>
Edit Metadata
</Button>
{ddo.findServiceByType('compute') && (
{appConfig.allowAdvancedSettings === 'true' && (
<>
<span className={styles.separator}>|</span>
<Button
style="text"
size="small"
onClick={handleEditAdvancedSettingsButton}
>
Edit Advanced Settings
</Button>
</>
)}
{ddo.findServiceByType('compute') && type === 'dataset' && (
<>
<span className={styles.separator}>|</span>
<Button

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

@ -0,0 +1,62 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '../../providers/Web3'
import rbacRequest from '../../utils/rbac'
import Alert from '../atoms/Alert'
import Loader from '../atoms/Loader'
import appConfig from '../../../app.config'
export default function Permission({
eventType,
children
}: {
eventType: string
children: ReactElement
}): ReactElement {
const url = appConfig.rbacUrl
const [data, updateData] = useState<boolean | 'ERROR'>()
const [errorMessage, updateError] = useState<string>()
const [messageState, updateMessageState] =
useState<'error' | 'warning' | 'info' | 'success'>()
const { accountId } = useWeb3()
useEffect(() => {
if (url === undefined) return
const getData = async () => {
if (accountId === undefined) {
updateError('Please make sure your wallet is connected to proceed.')
updateMessageState('info')
} else {
const data = await rbacRequest(eventType, accountId)
updateData(data)
if (data === 'ERROR') {
updateError(
'There was an error verifying your permissions. Please refresh the page or conntact your network administrator'
)
updateMessageState('error')
} else if (data === false) {
updateError(
`Sorry, you don't have permission to ${eventType}. Please make sure you have connected your registered address.`
)
updateMessageState('warning')
} else if (data !== true) {
updateError(
'An unkown error occured. Please conntact your network administrator'
)
updateMessageState('error')
}
}
}
getData()
}, [eventType, accountId, url])
if (url === undefined || data === true) {
return <>{children}</>
}
return (
<>
<Alert text={errorMessage} state={messageState} />
<br />
<Loader />
</>
)
}

View File

@ -1,3 +0,0 @@
.title {
margin-bottom: calc(var(--spacer) / 4);
}

Some files were not shown because too many files have changed in this diff Show More