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

Merge branch 'feature/v4-c2d' into fix/issue-1069-c2d-unsupported-networks

This commit is contained in:
Enzo Vezzaro 2022-06-21 08:32:20 -04:00
commit 56583c5e7a
68 changed files with 5006 additions and 4407 deletions

8
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
time: '03:00'
timezone: Europe/Berlin

View File

@ -5,6 +5,7 @@ on:
branches: branches:
- main - main
- v4 - v4
- v3
tags: tags:
- '**' - '**'
pull_request: pull_request:
@ -36,7 +37,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-
- run: npm ci - run: npm ci --legacy-peer-deps
- run: npm run build - run: npm run build
test: test:
@ -63,7 +64,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-
- run: npm ci - run: npm ci --legacy-peer-deps
- run: npm test - run: npm test
- name: Upload coverage artifact - name: Upload coverage artifact
@ -95,7 +96,7 @@ jobs:
with: with:
name: coverage-${{ runner.os }} name: coverage-${{ runner.os }}
- run: npm ci - run: npm ci --legacy-peer-deps
- run: npm run codegen:apollo - run: npm run codegen:apollo
- uses: paambaati/codeclimate-action@v3.0.0 - uses: paambaati/codeclimate-action@v3.0.0
@ -126,6 +127,6 @@ jobs:
key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}- restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-
- run: npm ci - run: npm ci --legacy-peer-deps
- run: npm run pregenerate - run: npm run pregenerate
- run: npm run storybook:build - run: npm run storybook:build

View File

@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
- run: npm ci - run: npm ci --legacy-peer-deps
- run: npm run build - run: npm run build
env: env:

View File

@ -9,12 +9,20 @@ module.exports = {
process.env.NEXT_PUBLIC_METADATACACHE_URI || process.env.NEXT_PUBLIC_METADATACACHE_URI ||
'https://v4.aquarius.oceanprotocol.com', 'https://v4.aquarius.oceanprotocol.com',
v3MetadataCacheUri:
process.env.NEXT_PUBLIC_V3_METADATACACHE_URI ||
'https://aquarius.oceanprotocol.com',
v3MarketUri:
process.env.NEXT_PUBLIC_V3_MARKET_URI ||
'https://v3.market.oceanprotocol.com',
// List of chainIds which metadata cache queries will return by default. // List of chainIds which metadata cache queries will return by default.
// This preselects the Chains user preferences. // This preselects the Chains user preferences.
chainIds: [3, 4, 80001, 1287], chainIds: [1, 137, 56, 246, 1285],
// List of all supported chainIds. Used to populate the Chains user preferences list. // List of all supported chainIds. Used to populate the Chains user preferences list.
chainIdsSupported: [3, 4, 80001, 1287], chainIdsSupported: [1, 137, 56, 246, 1285, 3, 4, 80001, 1287],
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',

View File

@ -12,5 +12,8 @@
"name": "Discord", "name": "Discord",
"url": "https://discord.gg/TnXjkR5" "url": "https://discord.gg/TnXjkR5"
} }
] ],
"stats": {
"note": "Counted on-chain from our NFT and pool factories. Includes assets in all Ocean Market forks and [purgatory](https://github.com/oceanprotocol/list-purgatory)."
}
} }

View File

@ -30,13 +30,22 @@
"required": true "required": true
}, },
{ {
"name": "links", "name": "files",
"label": "Sample file", "label": "New file",
"placeholder": "e.g. https://file.com/samplefile.json", "placeholder": "e.g. https://file.com/file.json",
"help": "Please provide a URL to a sample of your data set file. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**", "help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** 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. Leaving this field empty will not remove the current value.",
"prominentHelp": true, "prominentHelp": true,
"type": "files" "type": "files"
}, },
{
"name": "links",
"label": "New sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please provide a URL to a sample of your data set file. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** Leaving this field empty will not remove the current value.",
"prominentHelp": true,
"type": "files"
},
{ {
"name": "timeout", "name": "timeout",
"label": "Timeout", "label": "Timeout",

View File

@ -1,7 +1,7 @@
{ {
"siteTitle": "Ocean Market", "siteTitle": "Ocean Market",
"siteTagline": "A marketplace to find, publish and trade data sets in the Ocean Network.", "siteTagline": "A marketplace to find, publish and trade data sets in the Ocean Network.",
"siteUrl": "https://v4.market.oceanprotocol.com", "siteUrl": "https://market.oceanprotocol.com",
"siteImage": "/share.png", "siteImage": "/share.png",
"copyright": "All Rights Reserved. Powered by ", "copyright": "All Rights Reserved. Powered by ",
"menu": [ "menu": [
@ -14,12 +14,8 @@
"link": "/profile" "link": "/profile"
} }
], ],
"announcement": "Data NFTs, One-Sided Staking and more. [Explore OceanONDA V4](https://blog.oceanprotocol.com/oceanonda-v4-production-has-arrived-cb4fe8faaf39).",
"warning": { "warning": {
"main": "", "ctd": "Compute-to-Data is still in a testing phase, please use it only on test networks."
"polygonPublish": "Only republish data sets with a pool from ETH Mainnet into Polygon/Matic if the liquidity is **less than or equal to 1000 OCEAN in the original pool**. Doing otherwise will lead to [purgatory](https://github.com/oceanprotocol/list-purgatory) for the data set in Polygon/Matic."
},
"announcement": {
"main": "Ocean Market is [available on Polygon](https://blog.oceanprotocol.com/ocean-on-polygon-network-8abad19cbf47).",
"polygon": "Polygon/Matic EVM support is in early stages. [Use the Polygon Bridge](https://docs.oceanprotocol.com/tutorials/polygon-bridge/) to get mOCEAN."
} }
} }

8221
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,28 +17,28 @@
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh", "deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install", "postinstall": "husky install",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/", "codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet", "storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook" "storybook:build": "cross-env NODE_ENV=test build-storybook"
}, },
"dependencies": { "dependencies": {
"@coingecko/cryptoformat": "^0.4.4", "@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0", "@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^1.0.0-next.44", "@oceanprotocol/lib": "^1.1.2",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^4.0.7", "@portis/web3": "^4.0.7",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@urql/exchange-refocus": "^0.2.5", "@urql/exchange-refocus": "^0.2.5",
"@walletconnect/web3-provider": "^1.7.8", "@walletconnect/web3-provider": "^1.7.8",
"axios": "^0.27.2", "axios": "^0.27.2",
"chart.js": "^3.7.1", "chart.js": "^3.8.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"decimal.js": "^10.3.1", "decimal.js": "^10.3.1",
"dom-confetti": "^0.2.2", "dom-confetti": "^0.2.2",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"filesize": "^8.0.7", "filesize": "^9.0.1",
"formik": "^2.2.9", "formik": "^2.2.9",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
@ -50,7 +50,7 @@
"next": "^12.1.6", "next": "^12.1.6",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"react": "^18.1.0", "react": "^18.1.0",
"react-chartjs-2": "^4.1.0", "react-chartjs-2": "^4.2.0",
"react-clipboard.js": "^2.0.16", "react-clipboard.js": "^2.0.16",
"react-data-table-component": "^6.11.7", "react-data-table-component": "^6.11.7",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
@ -59,66 +59,66 @@
"react-paginate": "^8.1.3", "react-paginate": "^8.1.3",
"react-spring": "^9.4.5", "react-spring": "^9.4.5",
"react-tabs": "^5.1.0", "react-tabs": "^5.1.0",
"react-toastify": "^8.2.0", "react-toastify": "^9.0.4",
"remark": "^13.0.0", "remark": "^13.0.0",
"remark-gfm": "^1.0.0", "remark-gfm": "^1.0.0",
"remark-html": "^13.0.1", "remark-html": "^13.0.1",
"remove-markdown": "^0.3.0", "remove-markdown": "^0.5.0",
"slugify": "^1.6.5", "slugify": "^1.6.5",
"swr": "^1.3.0", "swr": "^1.3.0",
"urql": "^2.2.0", "urql": "^2.2.1",
"use-dark-mode": "^2.3.1", "use-dark-mode": "^2.3.1",
"web3": "^1.7.3", "web3": "^1.7.3",
"web3modal": "^1.9.7", "web3modal": "^1.9.7",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^6.5.4", "@storybook/addon-essentials": "^6.5.7",
"@storybook/addon-storyshots": "^6.5.4", "@storybook/addon-storyshots": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.4", "@storybook/builder-webpack5": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.4", "@storybook/manager-webpack5": "^6.5.7",
"@storybook/react": "^6.5.4", "@storybook/react": "^6.5.7",
"@storybook/testing-library": "^0.0.11", "@storybook/testing-library": "^0.0.11",
"@storybook/testing-react": "^1.3.0", "@storybook/testing-react": "^1.3.0",
"@svgr/webpack": "^6.2.1", "@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.2.0", "@testing-library/react": "^13.3.0",
"@types/chart.js": "^2.9.37", "@types/chart.js": "^2.9.37",
"@types/js-cookie": "^3.0.1", "@types/js-cookie": "^3.0.2",
"@types/loadable__component": "^5.13.1", "@types/loadable__component": "^5.13.4",
"@types/lodash.debounce": "^4.0.3", "@types/lodash.debounce": "^4.0.7",
"@types/lodash.omit": "^4.5.6", "@types/lodash.omit": "^4.5.7",
"@types/node": "^17.0.35", "@types/node": "^17.0.41",
"@types/react": "^18.0.9", "@types/react": "^18.0.12",
"@types/react-dom": "^18.0.4", "@types/react-dom": "^18.0.5",
"@types/react-modal": "^3.13.1", "@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1", "@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1", "@types/remove-markdown": "^0.3.1",
"@types/yup": "^0.29.13", "@types/yup": "^0.29.14",
"@typescript-eslint/eslint-plugin": "^5.25.0", "@typescript-eslint/eslint-plugin": "^5.27.1",
"@typescript-eslint/parser": "^5.25.0", "@typescript-eslint/parser": "^5.27.1",
"apollo": "^2.33.9", "apollo": "^2.34.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.16.0", "eslint": "^8.17.0",
"eslint-config-oceanprotocol": "^2.0.1", "eslint-config-oceanprotocol": "^2.0.1",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.30.0", "eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-testing-library": "^5.5.0", "eslint-plugin-testing-library": "^5.5.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"jest": "^28.1.0", "jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.0", "jest-environment-jsdom": "^28.1.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"process": "^0.11.10", "process": "^0.11.10",
"serve": "^13.0.2", "serve": "^13.0.2",
"stream-http": "^3.2.0", "stream-http": "^3.2.0",
"tsconfig-paths-webpack-plugin": "^3.5.2", "tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.4" "typescript": "^4.7.3"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -9,7 +9,7 @@ import React, {
} from 'react' } from 'react'
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib' import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
import { CancelToken } from 'axios' import { CancelToken } from 'axios'
import { retrieveAsset } from '@utils/aquarius' import { checkV3Asset, retrieveAsset } from '@utils/aquarius'
import { useWeb3 } from './Web3' import { useWeb3 } from './Web3'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean' import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
@ -18,7 +18,7 @@ import { getAccessDetails } from '@utils/accessDetailsAndPricing'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { useMarketMetadata } from './MarketMetadata' import { useMarketMetadata } from './MarketMetadata'
interface AssetProviderValue { export interface AssetProviderValue {
isInPurgatory: boolean isInPurgatory: boolean
purgatoryData: Purgatory purgatoryData: Purgatory
asset: AssetExtended asset: AssetExtended
@ -26,6 +26,7 @@ interface AssetProviderValue {
owner: string owner: string
error?: string error?: string
isAssetNetwork: boolean isAssetNetwork: boolean
isV3Asset: boolean
oceanConfig: Config oceanConfig: Config
loading: boolean loading: boolean
fetchAsset: (token?: CancelToken) => Promise<void> fetchAsset: (token?: CancelToken) => Promise<void>
@ -51,6 +52,7 @@ function AssetProvider({
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>() const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
const [isV3Asset, setIsV3Asset] = useState<boolean>()
const [oceanConfig, setOceanConfig] = useState<Config>() const [oceanConfig, setOceanConfig] = useState<Config>()
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
@ -68,6 +70,7 @@ function AssetProvider({
const asset = await retrieveAsset(did, token) const asset = await retrieveAsset(did, token)
if (!asset) { if (!asset) {
setIsV3Asset(await checkV3Asset(did, token))
setError( setError(
`\`${did}\`` + `\`${did}\`` +
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.' '\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
@ -168,6 +171,7 @@ function AssetProvider({
loading, loading,
fetchAsset, fetchAsset,
isAssetNetwork, isAssetNetwork,
isV3Asset,
oceanConfig oceanConfig
} as AssetProviderValue } as AssetProviderValue
} }

View File

@ -29,6 +29,8 @@ export interface AppConfig {
classNameLight: string classNameLight: string
storageKey: string storageKey: string
} }
v3MetadataCacheUri: string
v3MarketUri: string
} }
export interface SiteContent { export interface SiteContent {
siteTitle: string siteTitle: string
@ -40,13 +42,9 @@ export interface SiteContent {
name: string name: string
link: string link: string
}[] }[]
announcement: string
warning: { warning: {
main: string ctd: string
polygonPublish: string
}
announcement: {
main: string
polygon: string
} }
} }

View File

@ -36,7 +36,7 @@ function MarketMetadataProvider({
opcData.push({ opcData.push({
chainId: appConfig.chainIdsSupported[i], chainId: appConfig.chainIdsSupported[i],
approvedTokens: response.data?.opc.approvedTokens, approvedTokens: response.data?.opc.approvedTokens?.map((x) => x.id),
swapApprovedFee: response.data?.opc.swapOceanFee, swapApprovedFee: response.data?.opc.swapOceanFee,
swapNotApprovedFee: response.data?.opc.swapNonOceanFee swapNotApprovedFee: response.data?.opc.swapNonOceanFee
} as unknown as OpcFee) } as unknown as OpcFee)

View File

@ -31,7 +31,7 @@ interface UserPreferencesValue {
const UserPreferencesContext = createContext(null) const UserPreferencesContext = createContext(null)
const localStorageKey = 'ocean-user-preferences' const localStorageKey = 'ocean-user-preferences-v4'
function getLocalStorage(): UserPreferencesValue { function getLocalStorage(): UserPreferencesValue {
const storageParsed = const storageParsed =

View File

@ -319,15 +319,6 @@ export async function getOrderPriceAndFees(
orderPriceAndFee.price = new Decimal(+orderPriceAndFee.price || 0) orderPriceAndFee.price = new Decimal(+orderPriceAndFee.price || 0)
.add(new Decimal(+orderPriceAndFee?.consumeMarketOrderFee || 0)) .add(new Decimal(+orderPriceAndFee?.consumeMarketOrderFee || 0))
.add(new Decimal(+orderPriceAndFee?.publisherMarketOrderFee || 0)) .add(new Decimal(+orderPriceAndFee?.publisherMarketOrderFee || 0))
.add(
new Decimal(
(await unitsToAmount(
web3,
orderPriceAndFee?.providerFee?.providerFeeToken,
orderPriceAndFee?.providerFee?.providerFeeAmount.toString()
)) || 0
)
)
.toString() .toString()
return orderPriceAndFee return orderPriceAndFee
} }

View File

@ -2,7 +2,7 @@ import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../app.config' import { metadataCacheUri, v3MetadataCacheUri } from '../../app.config'
import { import {
SortDirectionOptions, SortDirectionOptions,
SortTermOptions SortTermOptions
@ -133,6 +133,28 @@ export async function retrieveAsset(
} }
} }
export async function checkV3Asset(
did: string,
cancelToken: CancelToken
): Promise<boolean> {
try {
const response: AxiosResponse<Asset> = await axios.get(
`${v3MetadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return false
return true
} catch (error) {
if (axios.isCancel(error)) {
LoggerInstance.log(error.message)
} else {
LoggerInstance.error(error.message)
}
return false
}
}
export async function getAssetsNames( export async function getAssetsNames(
didList: string[], didList: string[],
cancelToken: CancelToken cancelToken: CancelToken

View File

@ -1,13 +1,13 @@
import { import {
approve, approve,
approveWei,
Datatoken, Datatoken,
FreOrderParams, FreOrderParams,
LoggerInstance, LoggerInstance,
OrderParams, OrderParams,
ProviderComputeInitialize, ProviderComputeInitialize,
ProviderFees, ProviderFees,
ProviderInstance, ProviderInstance
unitsToAmount
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { AssetExtended } from 'src/@types/AssetExtended' import { AssetExtended } from 'src/@types/AssetExtended'
import Web3 from 'web3' import Web3 from 'web3'
@ -53,17 +53,10 @@ export async function order(
asset.services[0].serviceEndpoint asset.services[0].serviceEndpoint
)) ))
const providerFeesSanitized = providerFees || initializeData.providerFee
providerFeesSanitized.providerFeeAmount = await unitsToAmount(
web3,
providerFeesSanitized.providerFeeToken,
providerFeesSanitized.providerFeeAmount.toString()
)
const orderParams = { const orderParams = {
consumer: computeConsumerAddress || accountId, consumer: computeConsumerAddress || accountId,
serviceIndex: 0, serviceIndex: 0,
_providerFee: providerFeesSanitized, _providerFee: providerFees || initializeData.providerFee,
_consumeMarketFee: { _consumeMarketFee: {
consumeMarketFeeAddress: marketFeeAddress, consumeMarketFeeAddress: marketFeeAddress,
consumeMarketFeeAmount: consumeMarketOrderFee, consumeMarketFeeAmount: consumeMarketOrderFee,
@ -71,7 +64,6 @@ export async function order(
} }
} as OrderParams } as OrderParams
// TODO: we need to approve provider fee
switch (asset.accessDetails?.type) { switch (asset.accessDetails?.type) {
case 'fixed': { case 'fixed': {
// this assumes all fees are in ocean // this assumes all fees are in ocean
@ -94,8 +86,6 @@ export async function order(
swapMarketFee: consumeMarketFixedSwapFee, swapMarketFee: consumeMarketFixedSwapFee,
marketFeeAddress marketFeeAddress
} as FreOrderParams } as FreOrderParams
console.log('freParams', freParams)
console.log('orderParams', orderParams)
const tx = await datatoken.buyFromFreAndOrder( const tx = await datatoken.buyFromFreAndOrder(
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,
accountId, accountId,
@ -155,30 +145,6 @@ export async function reuseOrder(
asset.services[0].serviceEndpoint asset.services[0].serviceEndpoint
)) ))
if (
providerFees?.providerFeeAmount ||
initializeData?.providerFee?.providerFeeAmount
) {
const txApprove = await approve(
web3,
accountId,
providerFees.providerFeeToken ||
initializeData.providerFee.providerFeeToken,
asset.accessDetails.datatoken.address,
await unitsToAmount(
web3,
providerFees.providerFeeToken ||
initializeData.providerFee.providerFeeToken,
providerFees.providerFeeAmount ||
initializeData.providerFee.providerFeeAmount
),
false
)
if (!txApprove) {
return
}
}
const tx = await datatoken.reuseOrder( const tx = await datatoken.reuseOrder(
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,
accountId, accountId,
@ -215,6 +181,23 @@ export async function handleComputeOrder(
'[compute] Handle compute order for asset type: ', '[compute] Handle compute order for asset type: ',
asset.metadata.type asset.metadata.type
) )
if (
initializeData.providerFee &&
initializeData.providerFee.providerFeeAmount !== '0'
) {
const txApproveWei = await approveWei(
web3,
accountId,
asset.accessDetails.baseToken.address,
asset.accessDetails.datatoken.address,
initializeData.providerFee.providerFeeAmount
)
if (!txApproveWei) {
toast.error('Failed to approve provider fees!')
return
}
}
if (initializeData.validOrder && !initializeData.providerFee) { if (initializeData.validOrder && !initializeData.providerFee) {
LoggerInstance.log('[compute] Has valid order: ', initializeData.validOrder) LoggerInstance.log('[compute] Has valid order: ', initializeData.validOrder)
return initializeData.validOrder return initializeData.validOrder

View File

@ -3,7 +3,7 @@ import {
ComputeAsset, ComputeAsset,
ComputeEnvironment, ComputeEnvironment,
downloadFileBrowser, downloadFileBrowser,
FileMetadata, FileInfo,
LoggerInstance, LoggerInstance,
ProviderComputeInitializeResults, ProviderComputeInitializeResults,
ProviderInstance ProviderInstance
@ -53,7 +53,7 @@ export async function initializeProviderForCompute(
// TODO: Why do we have these one line functions ?!?!?! // TODO: Why do we have these one line functions ?!?!?!
export async function getEncryptedFiles( export async function getEncryptedFiles(
files: FileMetadata[], files: any,
providerUrl: string providerUrl: string
): Promise<string> { ): Promise<string> {
try { try {
@ -69,7 +69,7 @@ export async function getFileDidInfo(
did: string, did: string,
serviceId: string, serviceId: string,
providerUrl: string providerUrl: string
): Promise<FileMetadata[]> { ): Promise<FileInfo[]> {
try { try {
const response = await ProviderInstance.checkDidFiles( const response = await ProviderInstance.checkDidFiles(
did, did,
@ -85,7 +85,7 @@ export async function getFileDidInfo(
export async function getFileUrlInfo( export async function getFileUrlInfo(
url: string, url: string,
providerUrl: string providerUrl: string
): Promise<FileMetadata[]> { ): Promise<FileInfo[]> {
try { try {
const response = await ProviderInstance.checkFileUrl(url, providerUrl) const response = await ProviderInstance.checkFileUrl(url, providerUrl)
return response return response

View File

@ -3,6 +3,11 @@
margin-top: calc(var(--spacer) / 2); margin-top: calc(var(--spacer) / 2);
} }
.actionsCenter {
margin: auto !important;
display: block;
}
.help { .help {
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
color: var(--color-secondary); color: var(--color-secondary);

View File

@ -31,6 +31,7 @@ interface ButtonBuyProps {
algorithmPriceType?: string algorithmPriceType?: string
isAlgorithmConsumable?: boolean isAlgorithmConsumable?: boolean
isSupportedOceanNetwork?: boolean isSupportedOceanNetwork?: boolean
hasProviderFee?: boolean
} }
// TODO: we need to take a look at these messages // TODO: we need to take a look at these messages
@ -109,7 +110,8 @@ function getComputeAssetHelpText(
selectedComputeAssetType?: string, selectedComputeAssetType?: string,
isAlgorithmConsumable?: boolean, isAlgorithmConsumable?: boolean,
isSupportedOceanNetwork?: boolean, isSupportedOceanNetwork?: boolean,
web3?: any web3?: any,
hasProviderFee?: boolean
) { ) {
const computeAssetHelpText = getConsumeHelpText( const computeAssetHelpText = getConsumeHelpText(
dtBalance, dtBalance,
@ -138,11 +140,15 @@ function getComputeAssetHelpText(
isSupportedOceanNetwork isSupportedOceanNetwork
) )
const providerFeeHelpText = hasProviderFee
? 'In order to start the job you also need to pay the fees for renting the c2d resources.'
: 'C2D resources required to start the job are available, no payment required for those fees.'
const computeHelpText = selectedComputeAssettLowPoolLiquidity const computeHelpText = selectedComputeAssettLowPoolLiquidity
? computeAlgoHelpText ? computeAlgoHelpText
: lowPoolLiquidity : lowPoolLiquidity
? computeAssetHelpText ? computeAssetHelpText
: `${computeAssetHelpText} ${computeAlgoHelpText}` : `${computeAssetHelpText} ${computeAlgoHelpText} ${providerFeeHelpText}`
return computeHelpText return computeHelpText
} }
@ -172,7 +178,8 @@ export default function ButtonBuy({
priceType, priceType,
algorithmPriceType, algorithmPriceType,
isAlgorithmConsumable, isAlgorithmConsumable,
isSupportedOceanNetwork isSupportedOceanNetwork,
hasProviderFee
}: ButtonBuyProps): ReactElement { }: ButtonBuyProps): ReactElement {
const { web3 } = useWeb3() const { web3 } = useWeb3()
const buttonText = const buttonText =
@ -182,7 +189,9 @@ export default function ButtonBuy({
: priceType === 'free' : priceType === 'free'
? 'Get' ? 'Get'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}` : `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
: hasPreviousOrder && hasPreviousOrderSelectedComputeAsset : hasPreviousOrder &&
hasPreviousOrderSelectedComputeAsset &&
!hasProviderFee
? 'Start Compute Job' ? 'Start Compute Job'
: priceType === 'free' && algorithmPriceType === 'free' : priceType === 'free' && algorithmPriceType === 'free'
? 'Order Compute Job' ? 'Order Compute Job'
@ -199,7 +208,7 @@ export default function ButtonBuy({
type={type} type={type}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className="center" className={action === 'compute' ? styles.actionsCenter : ''}
> >
{buttonText} {buttonText}
</Button> </Button>
@ -236,7 +245,8 @@ export default function ButtonBuy({
selectedComputeAssetType, selectedComputeAssetType,
isAlgorithmConsumable, isAlgorithmConsumable,
isSupportedOceanNetwork, isSupportedOceanNetwork,
web3 web3,
hasProviderFee
)} )}
</div> </div>
</> </>

View File

@ -4,7 +4,7 @@ import classNames from 'classnames/bind'
import cleanupContentType from '@utils/cleanupContentType' import cleanupContentType from '@utils/cleanupContentType'
import styles from './index.module.css' import styles from './index.module.css'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { FileMetadata } from '@oceanprotocol/lib' import { FileInfo } from '@oceanprotocol/lib'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -22,7 +22,7 @@ export default function FileIcon({
small, small,
isLoading isLoading
}: { }: {
file: FileMetadata file: FileInfo
className?: string className?: string
small?: boolean small?: boolean
isLoading?: boolean isLoading?: boolean

View File

@ -2,13 +2,13 @@ import React, { ReactElement } from 'react'
import { prettySize } from './utils' import { prettySize } from './utils'
import cleanupContentType from '@utils/cleanupContentType' import cleanupContentType from '@utils/cleanupContentType'
import styles from './Info.module.css' import styles from './Info.module.css'
import { FileMetadata } from '@oceanprotocol/lib' import { FileInfo as FileInfoData } from '@oceanprotocol/lib'
export default function FileInfo({ export default function FileInfo({
file, file,
handleClose handleClose
}: { }: {
file: FileMetadata file: FileInfoData
handleClose(): void handleClose(): void
}): ReactElement { }): ReactElement {
const contentTypeCleaned = file.contentType const contentTypeCleaned = file.contentType

View File

@ -39,15 +39,10 @@ function getTitle(row: PoolTransaction, locale: string) {
case 'SETUP': { case 'SETUP': {
const firstToken = row.baseToken const firstToken = row.baseToken
const firstTokenSymbol = firstToken?.symbol const firstTokenSymbol = firstToken?.symbol
const secondToken = row.datatoken
const secondTokenSymbol = secondToken?.symbol
title += `Create pool with ${formatPrice( title += `Create pool with ${formatPrice(
Math.abs(row.baseTokenValue).toString(), Math.abs(row.baseTokenValue).toString(),
locale locale
)}${firstTokenSymbol} and ${formatPrice( )}${firstTokenSymbol}`
Math.abs(row.datatokenValue).toString(),
locale
)}${secondTokenSymbol}`
break break
} }
case 'JOIN': case 'JOIN':

View File

@ -187,7 +187,7 @@ export default function PoolTransactions({
poolTransactions.push({ poolTransactions.push({
...data[i], ...data[i],
networkId: !minimal networkId: !minimal
? getAsset(ddoList, data[i].pool.datatoken.id).chainId ? getAsset(ddoList, data[i].pool.datatoken.id)?.chainId
: poolChainId, : poolChainId,
asset: !minimal ? getAsset(ddoList, data[i].pool.datatoken.id) : null asset: !minimal ? getAsset(ddoList, data[i].pool.datatoken.id) : null
}) })

View File

@ -1,11 +1,11 @@
import React, { ReactElement } from 'react' import React, { ReactElement, ReactNode } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
export interface BadgeProps { export interface BadgeProps {
label: string label: string | ReactNode
className?: string className?: string
} }

View File

@ -20,15 +20,6 @@
text-align: center; text-align: center;
} }
.button:first-child {
margin-left: 0;
}
.button:last-child {
margin-right: 0;
min-width: auto;
}
.button:hover, .button:hover,
.button:focus { .button:focus {
color: var(--brand-white); color: var(--brand-white);
@ -82,11 +73,6 @@
min-width: auto; min-width: auto;
} }
.center {
margin: auto !important;
display: block;
}
/* Size Modifiers */ /* Size Modifiers */
.small { .small {
font-size: var(--font-size-small); font-size: var(--font-size-small);

View File

@ -23,6 +23,11 @@
margin-left: calc(var(--spacer) / 4); margin-left: calc(var(--spacer) / 4);
} }
.loader.white {
border-color: rgba(255 255 255 / 0.3);
border-top-color: var(--brand-white);
}
@keyframes loader { @keyframes loader {
to { to {
transform: rotate(360deg); transform: rotate(360deg);

View File

@ -3,12 +3,13 @@ import styles from './index.module.css'
export interface LoaderProps { export interface LoaderProps {
message?: string message?: string
white?: boolean
} }
export default function Loader({ message }: LoaderProps): ReactElement { export default function Loader({ message, white }: LoaderProps): ReactElement {
return ( return (
<div className={styles.loaderWrap}> <div className={styles.loaderWrap}>
<span className={styles.loader} /> <span className={`${styles.loader} ${white ? styles.white : ''}`} />
{message && <span className={styles.message}>{message}</span>} {message && <span className={styles.message}>{message}</span>}
</div> </div>
) )

View File

@ -8,8 +8,7 @@ import AnnouncementBanner from '@shared/AnnouncementBanner'
import PrivacyPreferenceCenter from '../Privacy/PrivacyPreferenceCenter' import PrivacyPreferenceCenter from '../Privacy/PrivacyPreferenceCenter'
import styles from './index.module.css' import styles from './index.module.css'
import { ToastContainer } from 'react-toastify' import { ToastContainer } from 'react-toastify'
import { useRouter } from 'next/router' import contentPurgatory from '../../../content/purgatory.json'
import content from '../../../content/purgatory.json'
import { useMarketMetadata } from '@context/MarketMetadata' import { useMarketMetadata } from '@context/MarketMetadata'
export default function App({ export default function App({
@ -17,24 +16,22 @@ export default function App({
}: { }: {
children: ReactElement children: ReactElement
}): ReactElement { }): ReactElement {
const router = useRouter()
const { siteContent, appConfig } = useMarketMetadata() const { siteContent, appConfig } = useMarketMetadata()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId) const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
return ( return (
<div className={styles.app}> <div className={styles.app}>
{router.pathname === '/' && siteContent?.warning.main !== '' && ( {siteContent?.announcement !== '' && (
<AnnouncementBanner text={siteContent?.warning.main} /> <AnnouncementBanner text={siteContent?.announcement} />
)} )}
<Header /> <Header />
{isInPurgatory && ( {isInPurgatory && (
<Alert <Alert
title={content.account.title} title={contentPurgatory.account.title}
badge={`Reason: ${purgatoryData?.reason}`} badge={`Reason: ${purgatoryData?.reason}`}
text={content.account.description} text={contentPurgatory.account.description}
state="error" state="error"
/> />
)} )}

View File

@ -22,3 +22,7 @@
border-right: 0; border-right: 0;
padding: 0; padding: 0;
} }
.warning {
margin-bottom: var(--spacer);
}

View File

@ -18,6 +18,8 @@ import {
import { AssetExtended } from 'src/@types/AssetExtended' import { AssetExtended } from 'src/@types/AssetExtended'
import Decimal from 'decimal.js' import Decimal from 'decimal.js'
import { MAX_DECIMALS } from '@utils/constants' import { MAX_DECIMALS } from '@utils/constants'
import { useMarketMetadata } from '@context/MarketMetadata'
import Alert from '@shared/atoms/Alert'
import { getDummyWeb3 } from '@utils/web3' import { getDummyWeb3 } from '@utils/web3'
export default function FormStartCompute({ export default function FormStartCompute({
@ -45,7 +47,9 @@ export default function FormStartCompute({
isConsumable, isConsumable,
consumableFeedback, consumableFeedback,
datasetOrderPriceAndFees, datasetOrderPriceAndFees,
algoOrderPriceAndFees algoOrderPriceAndFees,
providerFeeAmount,
validUntil
}: { }: {
algorithms: AssetSelectionAsset[] algorithms: AssetSelectionAsset[]
ddoListAlgorithms: Asset[] ddoListAlgorithms: Asset[]
@ -72,7 +76,10 @@ export default function FormStartCompute({
consumableFeedback: string consumableFeedback: string
datasetOrderPriceAndFees?: OrderPriceAndFees datasetOrderPriceAndFees?: OrderPriceAndFees
algoOrderPriceAndFees?: OrderPriceAndFees algoOrderPriceAndFees?: OrderPriceAndFees
providerFeeAmount?: string
validUntil?: string
}): ReactElement { }): ReactElement {
const { siteContent } = useMarketMetadata()
const { isValid, values }: FormikContextType<{ algorithm: string }> = const { isValid, values }: FormikContextType<{ algorithm: string }> =
useFormikContext() useFormikContext()
const { asset, isAssetNetwork } = useAsset() const { asset, isAssetNetwork } = useAsset()
@ -140,8 +147,12 @@ export default function FormStartCompute({
algoOrderPriceAndFees?.price || algoOrderPriceAndFees?.price ||
selectedAlgorithmAsset.accessDetails.price selectedAlgorithmAsset.accessDetails.price
).toDecimalPlaces(MAX_DECIMALS) ).toDecimalPlaces(MAX_DECIMALS)
const providerFees = providerFeeAmount
? new Decimal(providerFeeAmount).toDecimalPlaces(MAX_DECIMALS)
: new Decimal(0)
const totalPrice = priceDataset const totalPrice = priceDataset
.plus(priceAlgo) .plus(priceAlgo)
.plus(providerFees)
.toDecimalPlaces(MAX_DECIMALS) .toDecimalPlaces(MAX_DECIMALS)
.toString() .toString()
@ -166,6 +177,11 @@ export default function FormStartCompute({
return ( return (
<Form className={styles.form}> <Form className={styles.form}>
<Alert
className={styles.warning}
state="info"
text={siteContent.warning.ctd}
/>
{content.form.data.map((field: FormFieldContent) => ( {content.form.data.map((field: FormFieldContent) => (
<Field <Field
key={field.name} key={field.name}
@ -190,6 +206,8 @@ export default function FormStartCompute({
totalPrice={totalPrice} totalPrice={totalPrice}
datasetOrderPrice={datasetOrderPrice} datasetOrderPrice={datasetOrderPrice}
algoOrderPrice={algoOrderPrice} algoOrderPrice={algoOrderPrice}
providerFeeAmount={providerFeeAmount}
validUntil={validUntil}
/> />
<ButtonBuy <ButtonBuy
@ -230,6 +248,7 @@ export default function FormStartCompute({
selectedAlgorithmAsset?.accessDetails?.isPurchasable selectedAlgorithmAsset?.accessDetails?.isPurchasable
} }
isSupportedOceanNetwork={isSupportedOceanNetwork} isSupportedOceanNetwork={isSupportedOceanNetwork}
hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'}
/> />
</Form> </Form>
) )

View File

@ -36,8 +36,10 @@
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding-top: calc(var(--spacer) / 7); padding-top: calc(var(--spacer) / 7);
padding-bottom: calc(var(--spacer) / 7); padding-bottom: calc(var(--spacer) / 7);
display: flex; display: grid;
justify-content: space-between; grid-template-columns: 5% 1fr auto;
column-gap: calc(var(--spacer) / 10);
/* justify-content: space-between; */
} }
.priceRow:last-child { .priceRow:last-child {
@ -47,8 +49,14 @@
.sign { .sign {
display: inline-block; display: inline-block;
width: 5%;
text-align: left; text-align: left;
color: var(--color-secondary); color: var(--color-secondary);
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
.type {
display: inline-block;
text-align: left;
color: var(--color-secondary);
font-size: var(--font-size-mini);
}

View File

@ -19,6 +19,8 @@ interface PriceOutputProps {
selectedComputeAssetTimeout: string selectedComputeAssetTimeout: string
datasetOrderPrice?: number datasetOrderPrice?: number
algoOrderPrice?: number algoOrderPrice?: number
providerFeeAmount?: string
validUntil?: string
} }
function Row({ function Row({
@ -27,7 +29,8 @@ function Row({
hasDatatoken, hasDatatoken,
symbol, symbol,
timeout, timeout,
sign sign,
type
}: { }: {
price: string price: string
hasPreviousOrder?: boolean hasPreviousOrder?: boolean
@ -35,10 +38,12 @@ function Row({
symbol?: string symbol?: string
timeout?: string timeout?: string
sign?: string sign?: string
type?: string
}) { }) {
return ( return (
<div className={styles.priceRow}> <div className={styles.priceRow}>
<div className={styles.sign}>{sign}</div> <div className={styles.sign}>{sign}</div>
<div className={styles.type}>{type}</div>
<div> <div>
<PriceUnit <PriceUnit
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`} price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
@ -68,7 +73,9 @@ export default function PriceOutput({
algorithmConsumeDetails, algorithmConsumeDetails,
selectedComputeAssetTimeout, selectedComputeAssetTimeout,
datasetOrderPrice, datasetOrderPrice,
algoOrderPrice algoOrderPrice,
providerFeeAmount,
validUntil
}: PriceOutputProps): ReactElement { }: PriceOutputProps): ReactElement {
const { asset } = useAsset() const { asset } = useAsset()
@ -89,6 +96,7 @@ export default function PriceOutput({
.toString()} .toString()}
timeout={assetTimeout} timeout={assetTimeout}
symbol={symbol} symbol={symbol}
type="DATASET"
/> />
<Row <Row
hasPreviousOrder={hasPreviousOrderSelectedComputeAsset} hasPreviousOrder={hasPreviousOrderSelectedComputeAsset}
@ -101,6 +109,14 @@ export default function PriceOutput({
timeout={selectedComputeAssetTimeout} timeout={selectedComputeAssetTimeout}
symbol={symbol} symbol={symbol}
sign="+" sign="+"
type="ALGORITHM"
/>
<Row
price={providerFeeAmount} // initializeCompute.provider fee amount
timeout={`${validUntil} seconds`} // valid until value
symbol={symbol}
sign="+"
type="C2D RESOURCES"
/> />
<Row price={totalPrice} symbol={symbol} sign="=" /> <Row price={totalPrice} symbol={symbol} sign="=" />
</div> </div>

View File

@ -2,7 +2,7 @@ import React, { useState, ReactElement, useEffect, useCallback } from 'react'
import { import {
Asset, Asset,
DDO, DDO,
FileMetadata, FileInfo,
Datatoken, Datatoken,
ProviderInstance, ProviderInstance,
ComputeAsset, ComputeAsset,
@ -11,7 +11,8 @@ import {
LoggerInstance, LoggerInstance,
ComputeAlgorithm, ComputeAlgorithm,
ComputeOutput, ComputeOutput,
ProviderComputeInitializeResults ProviderComputeInitializeResults,
unitsToAmount
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import Price from '@shared/Price' import Price from '@shared/Price'
@ -59,7 +60,7 @@ export default function Compute({
}: { }: {
asset: AssetExtended asset: AssetExtended
dtBalance: string dtBalance: string
file: FileMetadata file: FileInfo
fileIsLoading?: boolean fileIsLoading?: boolean
isConsumable?: boolean isConsumable?: boolean
consumableFeedback?: string consumableFeedback?: string
@ -94,7 +95,8 @@ export default function Compute({
const [computeEnv, setComputeEnv] = useState<ComputeEnvironment>() const [computeEnv, setComputeEnv] = useState<ComputeEnvironment>()
const [initializedProviderResponse, setInitializedProviderResponse] = const [initializedProviderResponse, setInitializedProviderResponse] =
useState<ProviderComputeInitializeResults>() useState<ProviderComputeInitializeResults>()
// const [computeValidUntil, setComputeValidUntil] = useState<number>() const [providerFeeAmount, setProviderFeeAmount] = useState<string>('0')
const [computeValidUntil, setComputeValidUntil] = useState<string>('0')
const [datasetOrderPriceAndFees, setDatasetOrderPriceAndFees] = const [datasetOrderPriceAndFees, setDatasetOrderPriceAndFees] =
useState<OrderPriceAndFees>() useState<OrderPriceAndFees>()
const [isRequestingDataseOrderPrice, setIsRequestingDataseOrderPrice] = const [isRequestingDataseOrderPrice, setIsRequestingDataseOrderPrice] =
@ -147,7 +149,18 @@ export default function Compute({
return return
} }
setInitializedProviderResponse(initializedProvider) setInitializedProviderResponse(initializedProvider)
setProviderFeeAmount(
await unitsToAmount(
web3,
initializedProvider?.datasets?.[0]?.providerFee?.providerFeeToken,
initializedProvider?.datasets?.[0]?.providerFee?.providerFeeAmount
)
)
const computeDuration = (
parseInt(initializedProvider?.datasets?.[0]?.providerFee?.validUntil) -
Math.floor(Date.now() / 1000)
).toString()
setComputeValidUntil(computeDuration)
if ( if (
asset?.accessDetails?.addressOrId !== ZERO_ADDRESS && asset?.accessDetails?.addressOrId !== ZERO_ADDRESS &&
asset?.accessDetails?.type !== 'free' && asset?.accessDetails?.type !== 'free' &&
@ -258,6 +271,7 @@ export default function Compute({
setValidAlgorithmOrderTx( setValidAlgorithmOrderTx(
selectedAlgorithmAsset?.accessDetails?.validOrderTx selectedAlgorithmAsset?.accessDetails?.validOrderTx
) )
setAlgoOrderPriceAndFees(null)
async function initSelectedAlgo() { async function initSelectedAlgo() {
await checkAssetDTBalance(selectedAlgorithmAsset) await checkAssetDTBalance(selectedAlgorithmAsset)
@ -479,6 +493,8 @@ export default function Compute({
consumableFeedback={consumableFeedback} consumableFeedback={consumableFeedback}
datasetOrderPriceAndFees={datasetOrderPriceAndFees} datasetOrderPriceAndFees={datasetOrderPriceAndFees}
algoOrderPriceAndFees={algoOrderPriceAndFees} algoOrderPriceAndFees={algoOrderPriceAndFees}
providerFeeAmount={providerFeeAmount}
validUntil={computeValidUntil}
/> />
</Formik> </Formik>
)} )}

View File

@ -7,7 +7,7 @@ import ButtonBuy from '@shared/ButtonBuy'
import { secondsToString } from '@utils/ddo' import { secondsToString } from '@utils/ddo'
import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute' import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute'
import styles from './Download.module.css' import styles from './Download.module.css'
import { FileMetadata, LoggerInstance, ZERO_ADDRESS } from '@oceanprotocol/lib' import { FileInfo, LoggerInstance, ZERO_ADDRESS } from '@oceanprotocol/lib'
import { order } from '@utils/order' import { order } from '@utils/order'
import { AssetExtended } from 'src/@types/AssetExtended' import { AssetExtended } from 'src/@types/AssetExtended'
import { buyDtFromPool } from '@utils/pool' import { buyDtFromPool } from '@utils/pool'
@ -29,7 +29,7 @@ export default function Download({
consumableFeedback consumableFeedback
}: { }: {
asset: AssetExtended asset: AssetExtended
file: FileMetadata file: FileInfo
isBalanceSufficient: boolean isBalanceSufficient: boolean
dtBalance: string dtBalance: string
fileIsLoading?: boolean fileIsLoading?: boolean

View File

@ -149,7 +149,7 @@ export default function Add({
/> />
</div> </div>
<Output newPoolTokens={newPoolTokens} newPoolShare={newPoolShare} /> {/* TODO: will be fixed in #1481 <Output newPoolTokens={newPoolTokens} newPoolShare={newPoolShare} /> */}
<Actions <Actions
isDisabled={!isValid || !values.amount || values.amount === 0} isDisabled={!isValid || !values.amount || values.amount === 0}

View File

@ -4,8 +4,8 @@
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
color: var(--color-secondary); color: var(--color-secondary);
text-align: center; text-align: center;
padding-top: calc(var(--spacer) / 4); padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 3); padding-bottom: calc(var(--spacer) / 2);
} }
.update:before { .update:before {

View File

@ -18,3 +18,18 @@
margin-left: calc(var(--spacer) / 3); margin-left: calc(var(--spacer) / 3);
margin-right: calc(var(--spacer) / 3); margin-right: calc(var(--spacer) / 3);
} }
.fees {
border-top: none;
padding-top: 0;
margin-top: -0.5rem;
}
.fees > div {
grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
text-align: center;
}
.fees figure {
display: none;
}

View File

@ -110,17 +110,9 @@ export default function PoolSections() {
titlePostfixTitle={`Weight of ${poolInfo?.weightBaseToken}% ${poolInfo?.baseTokenSymbol} & ${poolInfo?.weightDt}% ${poolInfo?.datatokenSymbol}`} titlePostfixTitle={`Weight of ${poolInfo?.weightBaseToken}% ${poolInfo?.baseTokenSymbol} & ${poolInfo?.weightDt}% ${poolInfo?.datatokenSymbol}`}
> >
<Graph /> <Graph />
<Token </PoolSection>
symbol={poolInfo?.baseTokenSymbol}
balance={`${poolData?.baseTokenLiquidity}`}
size="mini"
/>
<Token
symbol={poolInfo?.datatokenSymbol}
balance={`${poolData?.datatokenLiquidity}`}
size="mini"
/>
<PoolSection className={styles.fees}>
<Token <Token
symbol="% swap fee" symbol="% swap fee"
balance={poolInfo?.liquidityProviderSwapFee} balance={poolInfo?.liquidityProviderSwapFee}

View File

@ -1,7 +1,7 @@
import React, { ReactElement, useState, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import Compute from './Compute' import Compute from './Compute'
import Consume from './Download' import Consume from './Download'
import { FileMetadata, LoggerInstance, Datatoken } from '@oceanprotocol/lib' import { FileInfo, LoggerInstance, Datatoken } from '@oceanprotocol/lib'
import Tabs, { TabsItem } from '@shared/atoms/Tabs' import Tabs, { TabsItem } from '@shared/atoms/Tabs'
import { compareAsBN } from '@utils/numbers' import { compareAsBN } from '@utils/numbers'
import Pool from './Pool' import Pool from './Pool'
@ -37,7 +37,7 @@ export default function AssetActions({
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>() const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>() const [dtBalance, setDtBalance] = useState<string>()
const [fileMetadata, setFileMetadata] = useState<FileMetadata>() const [fileMetadata, setFileMetadata] = useState<FileInfo>()
const [fileIsLoading, setFileIsLoading] = useState<boolean>(false) const [fileIsLoading, setFileIsLoading] = useState<boolean>(false)
const isCompute = Boolean( const isCompute = Boolean(
asset?.services.filter((service) => service.type === 'compute')[0] asset?.services.filter((service) => service.type === 'compute')[0]

View File

@ -5,13 +5,14 @@ import AddToken from '@shared/AddToken'
import ExplorerLink from '@shared/ExplorerLink' import ExplorerLink from '@shared/ExplorerLink'
import Publisher from '@shared/Publisher' import Publisher from '@shared/Publisher'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { AssetExtended } from 'src/@types/AssetExtended'
import styles from './MetaAsset.module.css' import styles from './MetaAsset.module.css'
export default function MetaAsset({ export default function MetaAsset({
asset, asset,
isBlockscoutExplorer isBlockscoutExplorer
}: { }: {
asset: Asset asset: AssetExtended
isBlockscoutExplorer: boolean isBlockscoutExplorer: boolean
}): ReactElement { }): ReactElement {
const { isAssetNetwork } = useAsset() const { isAssetNetwork } = useAsset()

View File

@ -1,16 +1,17 @@
import { Asset } from '@oceanprotocol/lib' import { useAsset } from '@context/Asset'
import AssetType from '@shared/AssetType' import AssetType from '@shared/AssetType'
import Time from '@shared/atoms/Time' import Time from '@shared/atoms/Time'
import Publisher from '@shared/Publisher' import Publisher from '@shared/Publisher'
import { getServiceByName } from '@utils/ddo' import { getServiceByName } from '@utils/ddo'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { AssetExtended } from 'src/@types/AssetExtended'
import styles from './MetaInfo.module.css' import styles from './MetaInfo.module.css'
export default function MetaInfo({ export default function MetaInfo({
asset, asset,
nftPublisher nftPublisher
}: { }: {
asset: Asset asset: AssetExtended
nftPublisher: string nftPublisher: string
}): ReactElement { }): ReactElement {
const isCompute = Boolean(getServiceByName(asset, 'compute')) const isCompute = Boolean(getServiceByName(asset, 'compute'))

View File

@ -13,32 +13,3 @@
height: calc(var(--spacer) * 2); height: calc(var(--spacer) * 2);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.nftImage {
position: relative;
margin: 0;
border-right: 1px solid var(--border-color);
width: calc(var(--spacer) * 2);
height: calc(var(--spacer) * 2);
}
.nftImage img,
.nftImage > svg:first-of-type {
width: 100%;
height: 100%;
}
.nftImage > svg:first-of-type {
transform: scale(0.7);
}
.nftImage .tooltip {
position: absolute;
padding: 0;
left: 0;
bottom: 0;
}
.nftImage .tooltip svg {
fill: var(--font-color-text);
}

View File

@ -1,14 +1,11 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
import { AssetExtended } from 'src/@types/AssetExtended'
import { decodeTokenURI } from '@utils/nft'
import MetaAsset from './MetaAsset' import MetaAsset from './MetaAsset'
import MetaInfo from './MetaInfo' import MetaInfo from './MetaInfo'
import Tooltip from '@shared/atoms/Tooltip' import Nft from '../Nft'
import NftTooltip from './NftTooltip' import { AssetExtended } from 'src/@types/AssetExtended'
import Logo from '@shared/atoms/Logo'
import { FormPublishData } from '../../../Publish/_types' const blockscoutNetworks = [1287, 2021000, 2021001, 44787, 246, 1285]
import { useFormikContext } from 'formik'
export default function MetaMain({ export default function MetaMain({
asset, asset,
@ -17,51 +14,12 @@ export default function MetaMain({
asset: AssetExtended asset: AssetExtended
nftPublisher: string nftPublisher: string
}): ReactElement { }): ReactElement {
const nftMetadata = decodeTokenURI(asset?.nft?.tokenURI)
const blockscoutNetworks = [1287, 2021000, 2021001, 44787, 246, 1285]
const isBlockscoutExplorer = blockscoutNetworks.includes(asset?.chainId) const isBlockscoutExplorer = blockscoutNetworks.includes(asset?.chainId)
// TODO: using this for the publish preview works fine, but produces a console warning
// on asset details page as there is no formik context there:
// Warning: Formik context is undefined, please verify you are calling useFormikContext()
// as child of a <Formik> component.
const formikState = useFormikContext<FormPublishData>()
// checking if the NFT has an image associated (tokenURI)
// if tokenURI is undefined, then we are in Preview
// for Preview we need to show accessDetails.dataImage
// as this is where the NFT's SVG (during publish) is stored
const nftImage = nftMetadata?.image_data
? nftMetadata.image_data
: formikState?.values?.metadata?.nft?.image_data
? formikState.values.metadata.nft.image_data
: null
return ( return (
<aside className={styles.meta}> <aside className={styles.meta}>
<header className={styles.asset}> <header className={styles.asset}>
<div className={styles.nftImage}> <Nft isBlockscoutExplorer={isBlockscoutExplorer} />
{nftImage ? (
<img src={nftImage} alt={asset?.nft?.name} />
) : (
<Logo noWordmark />
)}
{(nftMetadata || asset?.nftAddress) && (
<Tooltip
className={styles.tooltip}
content={
<NftTooltip
nft={nftMetadata}
address={asset?.nftAddress}
chainId={asset?.chainId}
isBlockscoutExplorer={isBlockscoutExplorer}
/>
}
/>
)}
</div>
<MetaAsset asset={asset} isBlockscoutExplorer={isBlockscoutExplorer} /> <MetaAsset asset={asset} isBlockscoutExplorer={isBlockscoutExplorer} />
</header> </header>

View File

@ -7,6 +7,11 @@ import styles from './NftTooltip.module.css'
import explorerLinkStyles from '@shared/ExplorerLink/index.module.css' import explorerLinkStyles from '@shared/ExplorerLink/index.module.css'
import { accountTruncate } from '@utils/web3' import { accountTruncate } from '@utils/web3'
// Supported OpenSea networks:
// https://support.opensea.io/hc/en-us/articles/4404027708051-Which-blockchains-does-OpenSea-support-
const openSeaNetworks = [1, 137]
const openSeaTestNetworks = [4]
export default function NftTooltip({ export default function NftTooltip({
nft, nft,
address, address,
@ -18,26 +23,23 @@ export default function NftTooltip({
chainId: number chainId: number
isBlockscoutExplorer: boolean isBlockscoutExplorer: boolean
}): ReactElement { }): ReactElement {
// Currently Ocean NFTs are not displayed correctly on OpenSea const openSeaSupported = openSeaNetworks
// Code prepared to easily integrate this feature once this is fixed .concat(openSeaTestNetworks)
//
// Supported OpeanSea networks:
// https://support.opensea.io/hc/en-us/articles/4404027708051-Which-blockchains-does-OpenSea-support-
const openseaNetworks = [1, 137]
const openseaTestNetworks = [4]
const openSeaSupported = openseaNetworks
.concat(openseaTestNetworks)
.includes(chainId) .includes(chainId)
const openSeaBaseUri = openSeaSupported const openSeaBaseUri = openSeaSupported
? openseaTestNetworks.includes(chainId) ? openSeaTestNetworks.includes(chainId)
? 'https://testnets.opensea.io' ? 'https://testnets.opensea.io'
: 'https://opensea.io' : 'https://opensea.io'
: undefined : undefined
const openSeaUrl = `${openSeaBaseUri}/assets/${
chainId === 137 ? 'matic' : ''
}/${address}/1`
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{nft && <img src={nft.image_data} alt={nft?.name} />} {nft && <img src={nft.image_data || nft.image} alt={nft?.name} />}
<div className={styles.info}> <div className={styles.info}>
{nft && <h5>{nft.name}</h5>} {nft && <h5>{nft.name}</h5>}
{address && ( {address && (
@ -53,25 +55,23 @@ export default function NftTooltip({
isBlockscoutExplorer ? `tokens/${address}` : `token/${address}` isBlockscoutExplorer ? `tokens/${address}` : `token/${address}`
} }
> >
View on explorer View on Explorer
</ExplorerLink> </ExplorerLink>
)} )}
{openSeaSupported && nft && address && ( {openSeaSupported && address && (
<a <a
href={`${openSeaBaseUri}/assets/${address}/1`} href={openSeaUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={explorerLinkStyles.link} className={explorerLinkStyles.link}
> >
View on OpeanSea <External /> View on OpenSea <External />
</a> </a>
)} )}
</div> </div>
{!nft?.image_data && ( {!nft?.image_data && !nft?.image && (
<p className={styles.fallback}> <p className={styles.fallback}>This Data NFT has no image set.</p>
This Data NFT was not created on Ocean Market
</p>
)} )}
</div> </div>
</div> </div>

View File

@ -0,0 +1,28 @@
.nftImage {
position: relative;
margin: 0;
border-right: 1px solid var(--border-color);
width: calc(var(--spacer) * 2);
height: calc(var(--spacer) * 2);
}
.nftImage img,
.nftImage > svg:first-of-type {
width: 100%;
height: 100%;
}
.nftImage > svg:first-of-type {
transform: scale(0.7);
}
.nftImage .tooltip {
position: absolute;
padding: 0;
left: 0;
bottom: 0;
}
.nftImage .tooltip svg {
fill: var(--font-color-text);
}

View File

@ -0,0 +1,60 @@
import { useAsset } from '@context/Asset'
import Tooltip from '@shared/atoms/Tooltip'
import { decodeTokenURI } from '@utils/nft'
import { useFormikContext } from 'formik'
import React from 'react'
import { FormPublishData } from 'src/components/Publish/_types'
import Logo from '@shared/atoms/Logo'
import NftTooltip from './NftTooltip'
import styles from './index.module.css'
export default function Nft({
isBlockscoutExplorer
}: {
isBlockscoutExplorer: boolean
}) {
const { asset } = useAsset()
const nftMetadata = decodeTokenURI(asset?.nft?.tokenURI)
// TODO: using this for the publish preview works fine, but produces a console warning
// on asset details page as there is no formik context there:
// Warning: Formik context is undefined, please verify you are calling useFormikContext()
// as child of a <Formik> component.
const formikState = useFormikContext<FormPublishData>()
// checking if the NFT has an image associated (tokenURI)
// if tokenURI is undefined, then we are in Preview
// for Preview we need to show accessDetails.dataImage
// as this is where the NFT's SVG (during publish) is stored
const nftImage = nftMetadata?.image_data
? nftMetadata.image_data
: nftMetadata?.image
? nftMetadata.image
: formikState?.values?.metadata?.nft?.image_data
? formikState.values.metadata.nft.image_data
: null
return (
<div className={styles.nftImage}>
{nftImage ? (
<img src={nftImage} alt={asset?.nft?.name} />
) : (
<Logo noWordmark />
)}
{(nftMetadata || asset?.nftAddress) && (
<Tooltip
className={styles.tooltip}
content={
<NftTooltip
nft={nftMetadata}
address={asset?.nftAddress}
chainId={asset?.chainId}
isBlockscoutExplorer={isBlockscoutExplorer}
/>
}
/>
)}
</div>
)
}

View File

@ -24,6 +24,7 @@ import EditFeedback from './EditFeedback'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { setNftMetadata } from '@utils/nft' import { setNftMetadata } from '@utils/nft'
import { sanitizeUrl } from '@utils/url' import { sanitizeUrl } from '@utils/url'
import { getEncryptedFiles } from '@utils/provider'
export default function Edit({ export default function Edit({
asset asset
@ -64,6 +65,7 @@ export default function Edit({
resetForm: () => void resetForm: () => void
) { ) {
try { try {
let updatedFiles = asset.services[0].files
const linksTransformed = values.links?.length && const linksTransformed = values.links?.length &&
values.links[0].valid && [sanitizeUrl(values.links[0].url)] values.links[0].valid && [sanitizeUrl(values.links[0].url)]
const updatedMetadata: Metadata = { const updatedMetadata: Metadata = {
@ -78,17 +80,43 @@ export default function Edit({
values.price !== asset.accessDetails.price && values.price !== asset.accessDetails.price &&
(await updateFixedPrice(values.price)) (await updateFixedPrice(values.price))
if (values.files[0]?.url) {
const file = {
nftAddress: asset.nftAddress,
datatokenAddress: asset.services[0].datatokenAddress,
files: [
{
type: 'url',
index: 0,
url: values.files[0].url,
method: 'GET'
}
]
}
const filesEncrypted = await getEncryptedFiles(
file,
asset.services[0].serviceEndpoint
)
updatedFiles = filesEncrypted
}
const updatedService: Service = { const updatedService: Service = {
...asset.services[0], ...asset.services[0],
timeout: mapTimeoutStringToSeconds(values.timeout) timeout: mapTimeoutStringToSeconds(values.timeout),
files: updatedFiles
} }
// TODO: remove version update at a later time
const updatedAsset: Asset = { const updatedAsset: Asset = {
...asset, ...(asset as Asset),
version: '4.1.0',
metadata: updatedMetadata, metadata: updatedMetadata,
services: [updatedService] services: [updatedService]
} }
// delete custom helper properties injected in the market so we don't write them on chain
delete (updatedAsset as AssetExtended).accessDetails
delete (updatedAsset as AssetExtended).datatokens
delete (updatedAsset as AssetExtended).stats
const setMetadataTx = await setNftMetadata( const setMetadataTx = await setNftMetadata(
updatedAsset, updatedAsset,
accountId, accountId,

View File

@ -1,9 +1,8 @@
import React, { ChangeEvent, ReactElement, useState } from 'react' import React, { ChangeEvent, ReactElement } from 'react'
import { Field, Form, FormikContextType, useFormikContext } from 'formik' import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Input, { InputProps } from '@shared/FormInput' import Input, { InputProps } from '@shared/FormInput'
import FormActions from './FormActions' import FormActions from './FormActions'
import styles from './FormEdit.module.css' import styles from './FormEdit.module.css'
import { FormPublishData } from '../../Publish/_types'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { MetadataEditForm } from './_types' import { MetadataEditForm } from './_types'

View File

@ -1,4 +1,4 @@
import { Metadata, ServiceComputeOptions } from '@oceanprotocol/lib' import { FileInfo, Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
import { secondsToString } from '@utils/ddo' import { secondsToString } from '@utils/ddo'
import * as Yup from 'yup' import * as Yup from 'yup'
import { MetadataEditForm } from './_types' import { MetadataEditForm } from './_types'
@ -10,6 +10,7 @@ export const validationSchema = Yup.object().shape({
description: Yup.string().required('Required').min(10), description: Yup.string().required('Required').min(10),
price: Yup.number().required('Required'), price: Yup.number().required('Required'),
links: Yup.array<any[]>().nullable(), links: Yup.array<any[]>().nullable(),
files: Yup.array<FileInfo[]>().nullable(),
timeout: Yup.string().required('Required'), timeout: Yup.string().required('Required'),
author: Yup.string().nullable() author: Yup.string().nullable()
}) })
@ -24,6 +25,7 @@ export function getInitialValues(
description: metadata?.description, description: metadata?.description,
price, price,
links: metadata?.links, links: metadata?.links,
files: '',
timeout: secondsToString(timeout), timeout: secondsToString(timeout),
author: metadata?.author author: metadata?.author
} }

View File

@ -6,5 +6,6 @@ export interface MetadataEditForm {
timeout: string timeout: string
price?: string price?: string
links?: string | any[] links?: string | any[]
files: string | any[]
author?: string author?: string
} }

View File

@ -1,27 +1,33 @@
import React, { useState, useEffect, ReactElement } from 'react' import React, { useState, useEffect, ReactElement } from 'react'
import { useRouter } from 'next/router'
import Page from '@shared/Page' import Page from '@shared/Page'
import Alert from '@shared/atoms/Alert' import Alert from '@shared/atoms/Alert'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import AssetContent from './AssetContent' import AssetContent from './AssetContent'
import { v3MarketUri } from 'app.config'
export default function AssetDetails({ uri }: { uri: string }): ReactElement { export default function AssetDetails({ uri }: { uri: string }): ReactElement {
const { asset, title, error, isInPurgatory, loading } = useAsset() const router = useRouter()
const { asset, title, error, isInPurgatory, loading, isV3Asset } = useAsset()
const [pageTitle, setPageTitle] = useState<string>() const [pageTitle, setPageTitle] = useState<string>()
useEffect(() => { useEffect(() => {
if (isV3Asset) {
router.push(`${v3MarketUri}${uri}`)
}
if (!asset || error) { if (!asset || error) {
setPageTitle('Could not retrieve asset') setPageTitle('Could not retrieve asset')
return return
} }
setPageTitle(isInPurgatory ? '' : title) setPageTitle(isInPurgatory ? '' : title)
}, [asset, error, isInPurgatory, title]) }, [asset, error, isInPurgatory, isV3Asset, router, title, uri])
return asset && pageTitle !== undefined && !loading ? ( return asset && pageTitle !== undefined && !loading ? (
<Page title={pageTitle} uri={uri}> <Page title={pageTitle} uri={uri}>
<AssetContent asset={asset} /> <AssetContent asset={asset} />
</Page> </Page>
) : error ? ( ) : error && isV3Asset === false ? (
<Page title={pageTitle} noPageHeader uri={uri}> <Page title={pageTitle} noPageHeader uri={uri}>
<Alert title={pageTitle} text={error} state="error" /> <Alert title={pageTitle} text={error} state="error" />
</Page> </Page>

View File

@ -4,6 +4,8 @@ import PriceUnit from '@shared/Price/PriceUnit'
import NetworkName from '@shared/NetworkName' import NetworkName from '@shared/NetworkName'
import styles from './Tooltip.module.css' import styles from './Tooltip.module.css'
import { StatsValue } from './_types' import { StatsValue } from './_types'
import content from '../../../../content/footer.json'
import Markdown from '@shared/Markdown'
export default function MarketStatsTooltip({ export default function MarketStatsTooltip({
totalValueLockedInOcean, totalValueLockedInOcean,
@ -39,13 +41,7 @@ export default function MarketStatsTooltip({
</li> </li>
))} ))}
</ul> </ul>
<p className={styles.note}> <Markdown className={styles.note} text={content.stats.note} />
Counted on-chain from our NFT and pool factories. Does not filter out
assets in{' '}
<a href="https://github.com/oceanprotocol/list-purgatory">
list-purgatory
</a>
</p>
</> </>
) )
} }

View File

@ -61,7 +61,9 @@ export default function MarketStats(): ReactElement {
// //
const getMarketStats = useCallback(async () => { const getMarketStats = useCallback(async () => {
if (!mainChainIds?.length) return if (!mainChainIds?.length) return
const newData: {
[chainId: number]: FooterStatsValuesGlobalStatistics
} = {}
for (const chainId of mainChainIds) { for (const chainId of mainChainIds) {
const context: OperationContext = { const context: OperationContext = {
url: `${getSubgraphUri( url: `${getSubgraphUri(
@ -73,15 +75,12 @@ export default function MarketStats(): ReactElement {
try { try {
const response = await fetchData(queryGlobalStatistics, null, context) const response = await fetchData(queryGlobalStatistics, null, context)
if (!response?.data?.globalStatistics) return if (!response?.data?.globalStatistics) return
newData[chainId] = response.data.globalStatistics[0]
setData((prevState) => ({
...prevState,
[chainId]: response.data.globalStatistics[0]
}))
} catch (error) { } catch (error) {
LoggerInstance.error('Error fetching global stats: ', error.message) LoggerInstance.error('Error fetching global stats: ', error.message)
} }
} }
setData(newData)
}, [mainChainIds]) }, [mainChainIds])
// //
@ -96,10 +95,12 @@ export default function MarketStats(): ReactElement {
// //
useEffect(() => { useEffect(() => {
if (!data || !mainChainIds?.length) return if (!data || !mainChainIds?.length) return
const newTotal: StatsTotal = { const newTotal: StatsTotal = {
...initialTotal // always start calculating beginning from initial 0 values ...initialTotal // always start calculating beginning from initial 0 values
} }
const newTVLInOcean: StatsValue = {}
const newTotalLiquidity: StatsValue = {}
const newPoolCount: StatsValue = {}
for (const chainId of mainChainIds) { for (const chainId of mainChainIds) {
const baseTokenValue = data[chainId]?.totalLiquidity[0]?.value const baseTokenValue = data[chainId]?.totalLiquidity[0]?.value
@ -109,25 +110,15 @@ export default function MarketStats(): ReactElement {
? new Decimal(baseTokenValue).mul(2) ? new Decimal(baseTokenValue).mul(2)
: new Decimal(0) : new Decimal(0)
setTotalValueLockedInOcean((prevState) => ({ newTVLInOcean[chainId] = `${totalValueLockedInOcean}`
...prevState,
[chainId]: `${totalValueLockedInOcean}`
}))
const totalOceanLiquidity = Number(baseTokenValue) || 0 const totalOceanLiquidity = Number(baseTokenValue) || 0
setTotalOceanLiquidity((prevState) => ({ newTotalLiquidity[chainId] = `${totalOceanLiquidity}`
...prevState,
[chainId]: `${totalOceanLiquidity}`
}))
const poolCount = data[chainId]?.poolCount || 0 const poolCount = data[chainId]?.poolCount || 0
setPoolCount((prevState) => ({ newPoolCount[chainId] = `${poolCount}`
...prevState,
[chainId]: `${poolCount}`
}))
const nftCount = data[chainId]?.nftCount || 0 const nftCount = data[chainId]?.nftCount || 0
const datatokenCount = data[chainId]?.datatokenCount || 0 const datatokenCount = data[chainId]?.datatokenCount || 0
const orderCount = data[chainId]?.orderCount || 0 const orderCount = data[chainId]?.orderCount || 0
@ -142,7 +133,9 @@ export default function MarketStats(): ReactElement {
LoggerInstance.error('Error data manipulation: ', error.message) LoggerInstance.error('Error data manipulation: ', error.message)
} }
} }
setTotalValueLockedInOcean(newTVLInOcean)
setTotalOceanLiquidity(newTotalLiquidity)
setPoolCount(newPoolCount)
setTotal(newTotal) setTotal(newTotal)
}, [data, mainChainIds, prices, currency]) }, [data, mainChainIds, prices, currency])

View File

@ -7,13 +7,24 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.logo { .logo,
order: 1; .badge {
white-space: nowrap; white-space: nowrap;
display: flex; display: flex;
}
.logo {
order: 0;
align-items: center; align-items: center;
} }
.badge {
cursor: pointer;
margin-top: -0.25rem;
position: relative;
padding: calc(var(--spacer) / 5) calc(var(--spacer) / 4);
}
.navigation { .navigation {
order: 3; order: 3;
margin-top: calc(var(--spacer) / 2); margin-top: calc(var(--spacer) / 2);
@ -60,10 +71,10 @@
.title { .title {
margin: 0; margin: 0;
margin-right: var(--spacer);
display: block; display: block;
color: var(--color-secondary); color: var(--color-secondary);
font-size: var(--font-size-h4); font-size: var(--font-size-h4);
margin-right: calc(var(--spacer) / 6);
} }
} }
@ -115,6 +126,10 @@
padding-right: 0; padding-right: 0;
} }
.link[aria-disabled] {
pointer-events: none;
}
.logo svg { .logo svg {
margin-right: calc(var(--spacer) / 3); margin-right: calc(var(--spacer) / 3);
} }
@ -122,3 +137,36 @@
.actions button { .actions button {
text-transform: none; text-transform: none;
} }
.tooltip[aria-expanded='true'] svg {
transform: rotate(180deg);
}
.caret,
svg.caret {
width: var(--font-size-mini);
height: var(--font-size-mini);
fill: var(--brand-white);
opacity: 0.7;
transition: transform 0.2s ease-out;
vertical-align: middle;
margin-left: calc(var(--spacer) / 12);
}
@media screen and (min-width: 42rem) {
.caret,
svg.caret {
display: inline-block;
}
}
.versions {
padding: calc(var(--spacer) / 4);
}
.versions .link {
display: inline-block;
margin: 0 calc(var(--spacer) / 4);
padding: 0;
font-size: var(--font-size-small);
}

View File

@ -9,7 +9,8 @@ import SearchBar from './SearchBar'
import styles from './Menu.module.css' import styles from './Menu.module.css'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useMarketMetadata } from '@context/MarketMetadata' import { useMarketMetadata } from '@context/MarketMetadata'
import Tooltip from '@shared/atoms/Tooltip'
import Caret from '@images/caret.svg'
const Wallet = loadable(() => import('./Wallet')) const Wallet = loadable(() => import('./Wallet'))
declare type MenuItem = { declare type MenuItem = {
@ -33,19 +34,41 @@ function MenuLink({ item }: { item: MenuItem }) {
} }
export default function Menu(): ReactElement { export default function Menu(): ReactElement {
const { siteContent } = useMarketMetadata() const { appConfig, siteContent } = useMarketMetadata()
return ( return (
<nav className={styles.menu}> <nav className={styles.menu}>
<Link href="/"> <Link href="/">
<a className={styles.logo}> <a className={styles.logo}>
<Logo noWordmark /> <Logo noWordmark />
<h1 className={styles.title}> <h1 className={styles.title}>{siteContent?.siteTitle}</h1>
{siteContent?.siteTitle} <Badge label="v4" />
</h1>
</a> </a>
</Link> </Link>
<Tooltip
className={styles.tooltip}
content={
<div className={styles.versions}>
<a className={styles.link} href={appConfig.v3MarketUri}>
v3
</a>
<a className={styles.link} href="" aria-current aria-disabled>
v4
</a>
</div>
}
trigger="click focus"
placement="bottom"
>
<Badge
className={styles.badge}
label={
<>
v4 <Caret aria-hidden="true" className={styles.caret} />
</>
}
/>
</Tooltip>
<ul className={styles.navigation}> <ul className={styles.navigation}>
{siteContent?.menu.map((item: MenuItem) => ( {siteContent?.menu.map((item: MenuItem) => (
<li key={item.name}> <li key={item.name}>

View File

@ -39,7 +39,7 @@ export async function getAssetsFromPoolShares(
assetList.push({ assetList.push({
poolShare: data[i], poolShare: data[i],
userLiquidity, userLiquidity,
networkId: getAsset(ddoList, data[i].pool.datatoken.address).chainId, networkId: getAsset(ddoList, data[i].pool.datatoken.address)?.chainId,
createTime: data[i].pool.createdTimestamp, createTime: data[i].pool.createdTimestamp,
asset: getAsset(ddoList, data[i].pool.datatoken.address) asset: getAsset(ddoList, data[i].pool.datatoken.address)
}) })

View File

@ -7,6 +7,7 @@
.actions button { .actions button {
margin: 0 calc(var(--spacer) / 2); margin: 0 calc(var(--spacer) / 2);
min-height: 40px;
} }
.infoIcon { .infoIcon {

View File

@ -10,6 +10,7 @@ import { useRouter } from 'next/router'
import Tooltip from '@shared/atoms/Tooltip' import Tooltip from '@shared/atoms/Tooltip'
import AvailableNetworks from 'src/components/Publish/AvailableNetworks' import AvailableNetworks from 'src/components/Publish/AvailableNetworks'
import Info from '@images/info.svg' import Info from '@images/info.svg'
import Loader from '@shared/atoms/Loader'
export default function Actions({ export default function Actions({
scrollToRef, scrollToRef,
@ -24,8 +25,7 @@ export default function Actions({
values, values,
errors, errors,
isValid, isValid,
isSubmitting, isSubmitting
setFieldValue
}: FormikContextType<FormPublishData> = useFormikContext() }: FormikContextType<FormPublishData> = useFormikContext()
const { connect, accountId } = useWeb3() const { connect, accountId } = useWeb3()
@ -60,6 +60,11 @@ export default function Actions({
(values.user.stepCurrent === 2 && errors.services !== undefined) || (values.user.stepCurrent === 2 && errors.services !== undefined) ||
(values.user.stepCurrent === 3 && errors.pricing !== undefined) (values.user.stepCurrent === 3 && errors.pricing !== undefined)
const hasSubmitError =
values.feedback?.[1].status === 'error' ||
values.feedback?.[2].status === 'error' ||
values.feedback?.[3].status === 'error'
return ( return (
<footer className={styles.actions}> <footer className={styles.actions}>
{did ? ( {did ? (
@ -108,7 +113,13 @@ export default function Actions({
style="primary" style="primary"
disabled={isSubmitting || !isValid} disabled={isSubmitting || !isValid}
> >
Submit {isSubmitting ? (
<Loader white />
) : hasSubmitError ? (
'Retry'
) : (
'Submit'
)}
</Button> </Button>
)} )}
</> </>

View File

@ -4,3 +4,7 @@
margin-left: 1.75rem; margin-left: 1.75rem;
top: -3.2rem; top: -3.2rem;
} }
.fieldWarning {
composes: fieldWarning from '../index.module.css';
}

View File

@ -9,6 +9,8 @@ import IconDataset from '@images/dataset.svg'
import IconAlgorithm from '@images/algorithm.svg' import IconAlgorithm from '@images/algorithm.svg'
import styles from './index.module.css' import styles from './index.module.css'
import { algorithmContainerPresets } from '../_constants' import { algorithmContainerPresets } from '../_constants'
import Alert from '@shared/atoms/Alert'
import { useMarketMetadata } from '@context/MarketMetadata'
const assetTypeOptionsTitles = getFieldContent( const assetTypeOptionsTitles = getFieldContent(
'type', 'type',
@ -16,6 +18,8 @@ const assetTypeOptionsTitles = getFieldContent(
).options ).options
export default function MetadataFields(): ReactElement { export default function MetadataFields(): ReactElement {
const { siteContent } = useMarketMetadata()
// connect with Form state, use for conditional field rendering // connect with Form state, use for conditional field rendering
const { values, setFieldValue } = useFormikContext<FormPublishData>() const { values, setFieldValue } = useFormikContext<FormPublishData>()
@ -71,6 +75,13 @@ export default function MetadataFields(): ReactElement {
name="metadata.type" name="metadata.type"
options={assetTypeOptions} options={assetTypeOptions}
/> />
{values.services[0].access === 'compute' && (
<Alert
className={styles.fieldWarning}
state="info"
text={siteContent.warning.ctd}
/>
)}
<Field <Field
{...getFieldContent('name', content.metadata.fields)} {...getFieldContent('name', content.metadata.fields)}
component={Input} component={Input}

View File

@ -6,6 +6,9 @@ import IconCompute from '@images/compute.svg'
import content from '../../../../content/publish/form.json' import content from '../../../../content/publish/form.json'
import { getFieldContent } from '../_utils' import { getFieldContent } from '../_utils'
import { FormPublishData } from '../_types' import { FormPublishData } from '../_types'
import Alert from '@shared/atoms/Alert'
import { useMarketMetadata } from '@context/MarketMetadata'
import styles from '../index.module.css'
const accessTypeOptionsTitles = getFieldContent( const accessTypeOptionsTitles = getFieldContent(
'access', 'access',
@ -13,6 +16,8 @@ const accessTypeOptionsTitles = getFieldContent(
).options ).options
export default function ServicesFields(): ReactElement { export default function ServicesFields(): ReactElement {
const { siteContent } = useMarketMetadata()
// connect with Form state, use for conditional field rendering // connect with Form state, use for conditional field rendering
const { values, setFieldValue } = useFormikContext<FormPublishData>() const { values, setFieldValue } = useFormikContext<FormPublishData>()
@ -67,12 +72,21 @@ export default function ServicesFields(): ReactElement {
name="services[0].algorithmPrivacy" name="services[0].algorithmPrivacy"
/> />
) : ( ) : (
<Field <>
{...getFieldContent('access', content.services.fields)} <Field
component={Input} {...getFieldContent('access', content.services.fields)}
name="services[0].access" component={Input}
options={accessTypeOptions} name="services[0].access"
/> options={accessTypeOptions}
/>
{values.services[0].access === 'compute' && (
<Alert
className={styles.fieldWarning}
state="info"
text={siteContent.warning.ctd}
/>
)}
</>
)} )}
<Field <Field
{...getFieldContent('providerUrl', content.services.fields)} {...getFieldContent('providerUrl', content.services.fields)}

View File

@ -1,12 +1,8 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
import { FormPublishData } from '../_types'
import { useFormikContext } from 'formik'
import { Feedback } from './Feedback' import { Feedback } from './Feedback'
export default function Submission(): ReactElement { export default function Submission(): ReactElement {
const { values, handleSubmit } = useFormikContext<FormPublishData>()
return ( return (
<div className={styles.submission}> <div className={styles.submission}>
<Feedback /> <Feedback />

View File

@ -3,7 +3,7 @@ import { NftMetadata } from '@utils/nft'
import { ReactElement } from 'react' import { ReactElement } from 'react'
import { PriceOptions } from 'src/@types/Price' import { PriceOptions } from 'src/@types/Price'
interface FileMetadata { interface FileInfo {
url: string url: string
valid?: boolean valid?: boolean
contentLength?: string contentLength?: string
@ -11,8 +11,8 @@ interface FileMetadata {
} }
export interface FormPublishService { export interface FormPublishService {
files: FileMetadata[] files: FileInfo[]
links?: FileMetadata[] links?: FileInfo[]
timeout: string timeout: string
dataTokenOptions: { name: string; symbol: string } dataTokenOptions: { name: string; symbol: string }
access: 'Download' | 'Compute' | string access: 'Download' | 'Compute' | string

View File

@ -144,13 +144,18 @@ export async function transformPublishFormToDdo(
} }
// this is the default format hardcoded // this is the default format hardcoded
const file = [ const file = {
{ nftAddress,
type: 'url', datatokenAddress,
url: files[0].url, files: [
method: 'GET' {
} type: 'url',
] index: 0,
url: files[0].url,
method: 'GET'
}
]
}
const filesEncrypted = const filesEncrypted =
!isPreview && !isPreview &&
files?.length && files?.length &&
@ -173,7 +178,7 @@ export async function transformPublishFormToDdo(
'@context': ['https://w3id.org/did/v1'], '@context': ['https://w3id.org/did/v1'],
id: did, id: did,
nftAddress, nftAddress,
version: '4.0.0', version: '4.1.0',
chainId, chainId,
metadata: newMetadata, metadata: newMetadata,
services: [newService], services: [newService],

View File

@ -146,7 +146,7 @@ export const validationSchema: Yup.SchemaOf<any> = Yup.object().shape({
// .min(4, (param) => `Title must be at least ${param.min} characters`) // .min(4, (param) => `Title must be at least ${param.min} characters`)
// .required('Required'), // .required('Required'),
// description: Yup.string().min(10).required('Required'), // description: Yup.string().min(10).required('Required'),
// files: Yup.array<FileMetadata>().required('Required').nullable(), // files: Yup.array<FileInfo>().required('Required').nullable(),
// timeout: Yup.string().required('Required'), // timeout: Yup.string().required('Required'),
// dataTokenOptions: Yup.object() // dataTokenOptions: Yup.object()
// .shape({ // .shape({
@ -167,6 +167,6 @@ export const validationSchema: Yup.SchemaOf<any> = Yup.object().shape({
// // ---- optional fields ---- // // ---- optional fields ----
// algorithmPrivacy: Yup.boolean().nullable(), // algorithmPrivacy: Yup.boolean().nullable(),
// tags: Yup.string().nullable(), // tags: Yup.string().nullable(),
// links: Yup.array<FileMetadata[]>().nullable() // links: Yup.array<FileInfo[]>().nullable()
// }) // })
// .defined() // .defined()

View File

@ -16,3 +16,8 @@
padding-right: calc(var(--spacer) * 4); padding-right: calc(var(--spacer) * 4);
} }
} }
.fieldWarning {
margin-top: -2rem;
margin-bottom: var(--spacer);
}

View File

@ -11,7 +11,7 @@ import Actions from './Actions'
import Debug from './Debug' import Debug from './Debug'
import Navigation from './Navigation' import Navigation from './Navigation'
import { Steps } from './Steps' import { Steps } from './Steps'
import { FormPublishData, PublishFeedback } from './_types' import { FormPublishData } from './_types'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import useNftFactory from '@hooks/contracts/useNftFactory' import useNftFactory from '@hooks/contracts/useNftFactory'
import { ProviderInstance, LoggerInstance, DDO } from '@oceanprotocol/lib' import { ProviderInstance, LoggerInstance, DDO } from '@oceanprotocol/lib'
@ -35,34 +35,34 @@ export default function PublishPage({
const nftFactory = useNftFactory() const nftFactory = useNftFactory()
const newAbortController = useAbortController() const newAbortController = useAbortController()
const [feedback, setFeedback] = useState<PublishFeedback>( // This `feedback` state is auto-synced into Formik context under `values.feedback`
initialPublishFeedback // for use in other components. Syncing defined in ./Steps.tsx child component.
) const [feedback, setFeedback] = useState(initialPublishFeedback)
// Collecting output of each publish step, enabling retry of failed steps
const [erc721Address, setErc721Address] = useState<string>()
const [datatokenAddress, setDatatokenAddress] = useState<string>()
const [ddo, setDdo] = useState<DDO>()
const [ddoEncrypted, setDdoEncrypted] = useState<string>()
const [did, setDid] = useState<string>() const [did, setDid] = useState<string>()
async function handleSubmit(values: FormPublishData) { // --------------------------------------------------
let _erc721Address: string, // 1. Create NFT & datatokens & create pricing schema
_datatokenAddress: string, // --------------------------------------------------
_ddo: DDO, async function create(values: FormPublishData): Promise<{
_encryptedDdo: string erc721Address: string
datatokenAddress: string
}> {
setFeedback((prevState) => ({
...prevState,
'1': {
...prevState['1'],
status: 'active',
errorMessage: null
}
}))
// reset all feedback state
setFeedback(initialPublishFeedback)
// --------------------------------------------------
// 1. Create NFT & datatokens & create pricing schema
// --------------------------------------------------
try { try {
setFeedback((prevState) => ({
...prevState,
'1': {
...prevState['1'],
status: 'active',
txCount: values.pricing.type === 'dynamic' ? 2 : 1,
description: prevState['1'].description
}
}))
const config = getOceanConfig(chainId) const config = getOceanConfig(chainId)
LoggerInstance.log('[publish] using config: ', config) LoggerInstance.log('[publish] using config: ', config)
@ -76,8 +76,7 @@ export default function PublishPage({
) )
const isSuccess = Boolean(erc721Address && datatokenAddress && txHash) const isSuccess = Boolean(erc721Address && datatokenAddress && txHash)
_erc721Address = erc721Address if (!isSuccess) throw new Error('No Token created. Please try again.')
_datatokenAddress = datatokenAddress
LoggerInstance.log('[publish] createTokensAndPricing tx', txHash) LoggerInstance.log('[publish] createTokensAndPricing tx', txHash)
LoggerInstance.log('[publish] erc721Address', erc721Address) LoggerInstance.log('[publish] erc721Address', erc721Address)
@ -87,14 +86,16 @@ export default function PublishPage({
...prevState, ...prevState,
'1': { '1': {
...prevState['1'], ...prevState['1'],
status: isSuccess ? 'success' : 'error', status: 'success',
txHash txHash
} }
})) }))
return { erc721Address, datatokenAddress }
} catch (error) { } catch (error) {
LoggerInstance.error('[publish] error', error.message) LoggerInstance.error('[publish] error', error.message)
if (error.message.length > 65) { if (error.message.length > 65) {
error.message = 'No Token created.' error.message = 'No Token created. Please try again.'
} }
setFeedback((prevState) => ({ setFeedback((prevState) => ({
@ -102,58 +103,65 @@ export default function PublishPage({
'1': { '1': {
...prevState['1'], ...prevState['1'],
status: 'error', status: 'error',
errorMessage: error.message, errorMessage: error.message
description:
values.pricing.type === 'dynamic'
? prevState['1'].description.replace(
'a single transaction',
'a single transaction, after an initial approve transaction'
)
: prevState['1'].description
} }
})) }))
} }
}
// --------------------------------------------------
// 2. Construct and encrypt DDO
// --------------------------------------------------
async function encrypt(
values: FormPublishData,
erc721Address: string,
datatokenAddress: string
): Promise<{ ddo: DDO; ddoEncrypted: string }> {
setFeedback((prevState) => ({
...prevState,
'2': {
...prevState['2'],
status: 'active',
errorMessage: null
}
}))
// --------------------------------------------------
// 2. Construct and encrypt DDO
// --------------------------------------------------
try { try {
setFeedback((prevState) => ({ if (!datatokenAddress || !erc721Address)
...prevState, throw new Error('No NFT or Datatoken received. Please try again.')
'2': {
...prevState['2'],
status: 'active'
}
}))
if (!_datatokenAddress || !_erc721Address)
throw new Error('No NFT or Datatoken received.')
const ddo = await transformPublishFormToDdo( const ddo = await transformPublishFormToDdo(
values, values,
_datatokenAddress, datatokenAddress,
_erc721Address erc721Address
) )
_ddo = ddo if (!ddo) throw new Error('No DDO received. Please try again.')
setDdo(ddo)
LoggerInstance.log('[publish] Got new DDO', ddo) LoggerInstance.log('[publish] Got new DDO', ddo)
const encryptedResponse = await ProviderInstance.encrypt( const ddoEncrypted = await ProviderInstance.encrypt(
ddo, ddo,
values.services[0].providerUrl.url, values.services[0].providerUrl.url,
newAbortController() newAbortController()
) )
const encryptedDdo = encryptedResponse
_encryptedDdo = encryptedDdo if (!ddoEncrypted)
LoggerInstance.log('[publish] Got encrypted DDO', encryptedDdo) throw new Error('No encrypted DDO received. Please try again.')
setDdoEncrypted(ddoEncrypted)
LoggerInstance.log('[publish] Got encrypted DDO', ddoEncrypted)
setFeedback((prevState) => ({ setFeedback((prevState) => ({
...prevState, ...prevState,
'2': { '2': {
...prevState['2'], ...prevState['2'],
status: encryptedDdo ? 'success' : 'error' status: 'success'
} }
})) }))
return { ddo, ddoEncrypted }
} catch (error) { } catch (error) {
LoggerInstance.error('[publish] error', error.message) LoggerInstance.error('[publish] error', error.message)
setFeedback((prevState) => ({ setFeedback((prevState) => ({
@ -165,43 +173,53 @@ export default function PublishPage({
} }
})) }))
} }
}
// --------------------------------------------------
// 3. Write DDO into NFT metadata
// --------------------------------------------------
async function publish(
values: FormPublishData,
ddo: DDO,
ddoEncrypted: string
): Promise<{ did: string }> {
setFeedback((prevState) => ({
...prevState,
'3': {
...prevState['3'],
status: 'active',
errorMessage: null
}
}))
// --------------------------------------------------
// 3. Write DDO into NFT metadata
// --------------------------------------------------
try { try {
setFeedback((prevState) => ({ if (!ddo || !ddoEncrypted)
...prevState, throw new Error('No DDO received. Please try again.')
'3': {
...prevState['3'],
status: 'active'
}
}))
if (!_ddo || !_encryptedDdo) throw new Error('No DDO received.')
const res = await setNFTMetadataAndTokenURI( const res = await setNFTMetadataAndTokenURI(
_ddo, ddo,
accountId, accountId,
web3, web3,
values.metadata.nft, values.metadata.nft,
newAbortController() newAbortController()
) )
if (!res?.transactionHash)
throw new Error(
'Metadata could not be written into the NFT. Please try again.'
)
LoggerInstance.log('[publish] setMetadata result', res) LoggerInstance.log('[publish] setMetadata result', res)
const txHash = res.transactionHash
setFeedback((prevState) => ({ setFeedback((prevState) => ({
...prevState, ...prevState,
'3': { '3': {
...prevState['3'], ...prevState['3'],
status: res ? 'success' : 'error', status: res ? 'success' : 'error',
txHash txHash: res?.transactionHash
} }
})) }))
setDid(_ddo.id) return { did: ddo.id }
} catch (error) { } catch (error) {
LoggerInstance.error('[publish] error', error.message) LoggerInstance.error('[publish] error', error.message)
setFeedback((prevState) => ({ setFeedback((prevState) => ({
@ -215,6 +233,44 @@ export default function PublishPage({
} }
} }
// --------------------------------------------------
// Orchestrate publishing
// --------------------------------------------------
async function handleSubmit(values: FormPublishData) {
// Syncing variables with state, enabling retry of failed steps
let _erc721Address = erc721Address
let _datatokenAddress = datatokenAddress
let _ddo = ddo
let _ddoEncrypted = ddoEncrypted
let _did = did
if (!_erc721Address || !_datatokenAddress) {
const { erc721Address, datatokenAddress } = await create(values)
_erc721Address = erc721Address
_datatokenAddress = datatokenAddress
setErc721Address(erc721Address)
setDatatokenAddress(datatokenAddress)
}
if (!_ddo || !_ddoEncrypted) {
const { ddo, ddoEncrypted } = await encrypt(
values,
_erc721Address,
_datatokenAddress
)
_ddo = ddo
_ddoEncrypted = ddoEncrypted
setDdo(ddo)
setDdoEncrypted(ddoEncrypted)
}
if (!_did) {
const { did } = await publish(values, _ddo, _ddoEncrypted)
_did = did
setDid(did)
}
}
return isInPurgatory && purgatoryData ? null : ( return isInPurgatory && purgatoryData ? null : (
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
@ -224,22 +280,20 @@ export default function PublishPage({
await handleSubmit(values) await handleSubmit(values)
}} }}
> >
{({ values }) => { {({ values }) => (
return ( <>
<> <PageHeader
<PageHeader title={<Title networkId={values.user.chainId} />}
title={<Title networkId={values.user.chainId} />} description={content.description}
description={content.description} />
/> <Form className={styles.form} ref={scrollToRef}>
<Form className={styles.form} ref={scrollToRef}> <Navigation />
<Navigation /> <Steps feedback={feedback} />
<Steps feedback={feedback} /> <Actions scrollToRef={scrollToRef} did={did} />
<Actions scrollToRef={scrollToRef} did={did} /> </Form>
</Form> {debug && <Debug />}
{debug && <Debug />} </>
</> )}
)
}}
</Formik> </Formik>
) )
} }