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

Merge branch 'main' into feature/issue-1657-shacl-schema

This commit is contained in:
EnzoVezzaro 2022-11-28 10:39:56 -04:00
commit 7ed9298530
77 changed files with 2232 additions and 46941 deletions

View File

@ -5,7 +5,7 @@ export const assetAquarius: Asset = {
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630', id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d', nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0', version: '4.1.0',
chainId: 5, chainId: 1,
metadata: { metadata: {
created: '2022-09-29T11:30:26Z', created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z', updated: '2022-09-29T11:30:26Z',
@ -57,12 +57,12 @@ export const assetAquarius: Asset = {
} }
], ],
stats: { stats: {
orders: 22 orders: 22,
// price: { price: {
// value: 3231343254, value: 3231343254,
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN' tokenSymbol: 'OCEAN'
// } }
}, },
purgatory: { purgatory: {
state: false, state: false,

View File

@ -3,6 +3,7 @@ import { assetAquarius } from './assetAquarius'
export const asset: AssetExtended = { export const asset: AssetExtended = {
...assetAquarius, ...assetAquarius,
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:

View File

@ -68,11 +68,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 0 orders: 0,
// price: {} price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
type: 'NOT_SUPPORTED' type: 'NOT_SUPPORTED'
} as any } as any
}, },
@ -150,15 +155,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 45554.69921875, allocated: 45554.69921875,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0x282d8efCe846A88B159800bd4130ad77443Fa1A1', value: 3231343254,
// tokenSymbol: 'mOCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 100 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -243,15 +249,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 7 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -336,15 +343,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 50.2051887512, allocated: 50.2051887512,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 10 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -435,15 +443,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 6 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -468,6 +477,7 @@ export const assets: AssetExtended[] = [
{ {
'@context': ['https://w3id.org/did/v1'], '@context': ['https://w3id.org/did/v1'],
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -563,12 +573,12 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 5 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0' version: '4.1.0'
}, },
@ -651,15 +661,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 5 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -744,15 +755,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 5 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -836,13 +848,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 114.1658859253, allocated: 114.1658859253,
orders: 1 orders: 1,
// price: { price: {
// value: 0 value: 3231343254,
// } tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'free', type: 'free',
addressOrId: '0x0a81f1c69e5428067e6124817c7affe8bc0adf9f', addressOrId: '0x0a81f1c69e5428067e6124817c7affe8bc0adf9f',
@ -918,15 +933,16 @@ export const assets: AssetExtended[] = [
} }
], ],
stats: { stats: {
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 2 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -1008,13 +1024,16 @@ export const assets: AssetExtended[] = [
} }
], ],
stats: { stats: {
orders: 1 orders: 1,
// price: { price: {
// value: 0 value: 3231343254,
// } tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'free', type: 'free',
addressOrId: '0x772224c2c2bddb88a55b3905aaaf8c7188b02ce3', addressOrId: '0x772224c2c2bddb88a55b3905aaaf8c7188b02ce3',
@ -1092,13 +1111,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 11159.279296875, allocated: 11159.279296875,
orders: 1 orders: 1,
// price: { price: {
// value: 0 value: 3231343254,
// } tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'free', type: 'free',
addressOrId: '0x89a0170556bb80438081d69f43d8c07a90e9aa24', addressOrId: '0x89a0170556bb80438081d69f43d8c07a90e9aa24',
@ -1174,13 +1196,16 @@ export const assets: AssetExtended[] = [
} }
], ],
stats: { stats: {
orders: 0 orders: 0,
// price: { price: {
// value: 0 value: 3231343254,
// } tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'free', type: 'free',
addressOrId: '0xad42c7afee47140b5cd87f05d5846c418145f43a', addressOrId: '0xad42c7afee47140b5cd87f05d5846c418145f43a',
@ -1257,8 +1282,12 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 0, allocated: 0,
orders: 0 orders: 0,
// price: {} price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
@ -1326,15 +1355,16 @@ export const assets: AssetExtended[] = [
} }
], ],
stats: { stats: {
orders: 1 orders: 1,
// price: { price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', value: 3231343254,
// tokenSymbol: 'OCEAN', tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// value: 1 tokenSymbol: 'OCEAN'
// } }
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:
@ -1416,13 +1446,16 @@ export const assets: AssetExtended[] = [
} }
], ],
stats: { stats: {
orders: 0 orders: 0,
// price: { price: {
// value: 0 value: 3231343254,
// } tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'free', type: 'free',
addressOrId: '0x23c1fd10dadcaf558fb7173b79cfd0d867568a3d', addressOrId: '0x23c1fd10dadcaf558fb7173b79cfd0d867568a3d',
@ -1510,10 +1543,16 @@ export const assets: AssetExtended[] = [
], ],
stats: { stats: {
allocated: 422.9883117676, allocated: 422.9883117676,
orders: 0 orders: 0,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
}, },
version: '4.1.0', version: '4.1.0',
accessDetails: { accessDetails: {
templateId: 2,
publisherMarketOrderFee: '0', publisherMarketOrderFee: '0',
type: 'fixed', type: 'fixed',
addressOrId: addressOrId:

View File

@ -0,0 +1 @@
export default true

View File

@ -1 +1,3 @@
export default true export default function isUrl() {
return true
}

View File

@ -18,6 +18,7 @@ module.exports = {
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
defaultDatatokenTemplateIndex: 2,
// The ETH address the marketplace fee will be sent to. // The ETH address the marketplace fee will be sent to.
marketFeeAddress: marketFeeAddress:
process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS || process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS ||

View File

@ -29,19 +29,62 @@
}, },
{ {
"name": "files", "name": "files",
"label": "New file", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "prominentHelp": false,
"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 dataset, 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.", "type": "tabs",
"prominentHelp": true, "fields": [{
"type": "files" "value": "ipfs",
}, "title": "IPFS",
{ "label": "CID",
"placeholder": "e.g. bafkreidgvpkjawlxz6sffxzwgooowe5yt7i6wsyg236mfoks77nywkptdq",
"help": "This CID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "arweave",
"title": "Arweave",
"label": "Transaction ID",
"placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd",
"help": "This Transaction ID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"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.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
}],
"sortOptions": false,
"required": true
},{
"name": "links", "name": "links",
"label": "New sample file", "label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "prominentHelp": false,
"help": "Please provide a URL to a sample of your dataset file. This file should reveal the data structure of your dataset, 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.", "type": "tabs",
"prominentHelp": true, "fields": [
"type": "files" {
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"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.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": false
}],
"required": false
}, },
{ {
@ -66,6 +109,13 @@
"type": "tags", "type": "tags",
"placeholder": "e.g. logistics", "placeholder": "e.g. logistics",
"required": false "required": false
},
{
"name": "paymentCollector",
"label": "Payment Collector Address",
"placeholder": "e.g. 0X123ABC...",
"help": "This address will receive the revenue from all sales. More info available in our [docs](https://docs.oceanprotocol.com/core-concepts/datanft-and-datatoken#revenue).",
"required": false
} }
] ]
} }

View File

@ -104,19 +104,61 @@
{ {
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "prominentHelp": false,
"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 dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", "type": "tabs",
"prominentHelp": true, "fields": [{
"type": "files", "value": "ipfs",
"title": "IPFS",
"label": "CID",
"placeholder": "e.g. bafkreidgvpkjawlxz6sffxzwgooowe5yt7i6wsyg236mfoks77nywkptdq",
"help": "This CID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "arweave",
"title": "Arweave",
"label": "Transaction ID",
"placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd",
"help": "This Transaction ID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"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.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
}],
"sortOptions": false,
"required": true "required": true
}, },{
{
"name": "links", "name": "links",
"label": "Sample file", "label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "prominentHelp": false,
"help": "This file should reveal the data structure of your dataset, 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.**", "type": "tabs",
"prominentHelp": true, "fields": [
"type": "files" {
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"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.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": false
}],
"required": false
}, },
{ {
"name": "algorithmPrivacy", "name": "algorithmPrivacy",

47079
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@
"@coingecko/cryptoformat": "^0.5.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": "^2.4.0", "@oceanprotocol/lib": "^2.5.2",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3", "@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
@ -41,6 +41,7 @@
"filesize": "^10.0.5", "filesize": "^10.0.5",
"formik": "^2.2.9", "formik": "^2.2.9",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-ipfs": "^7.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"match-sorter": "^6.3.1", "match-sorter": "^6.3.1",

View File

@ -10,6 +10,7 @@ export interface AppConfig {
infuraProjectId: string infuraProjectId: string
chainIds: number[] chainIds: number[]
chainIdsSupported: number[] chainIdsSupported: number[]
defaultDatatokenTemplateIndex: number
marketFeeAddress: string marketFeeAddress: string
publisherMarketOrderFee: string publisherMarketOrderFee: string
publisherMarketFixedSwapFee: string publisherMarketFixedSwapFee: string

View File

@ -1,10 +1,12 @@
import { useRef, useEffect, useCallback } from 'react' import { useRef, useEffect, useCallback } from 'react'
import axios, { CancelToken } from 'axios' import axios, { CancelToken } from 'axios'
export const useCancelToken = (): (() => CancelToken) => { export const useCancelToken = (): (() => CancelToken) => {
const axiosSource = useRef(null) const axiosSource = useRef(null)
const newCancelToken = useCallback(() => { const newCancelToken = useCallback(() => {
axiosSource.current = axios.CancelToken.source() axiosSource.current = axios.CancelToken.source()
return axiosSource.current.token return axiosSource?.current?.token
}, []) }, [])
useEffect( useEffect(

4
src/@types/Analytics.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface PageViews {
count: number
did: string
}

View File

@ -5,5 +5,6 @@ import { Asset } from '@oceanprotocol/lib'
declare global { declare global {
interface AssetExtended extends Asset { interface AssetExtended extends Asset {
accessDetails?: AccessDetails accessDetails?: AccessDetails
views?: number
} }
} }

View File

@ -39,6 +39,7 @@ declare global {
interface AccessDetails { interface AccessDetails {
type: 'fixed' | 'free' | 'NOT_SUPPORTED' type: 'fixed' | 'free' | 'NOT_SUPPORTED'
price: string price: string
templateId: number
addressOrId: string addressOrId: string
baseToken: TokenInfo baseToken: TokenInfo
datatoken: TokenInfo datatoken: TokenInfo

View File

@ -30,6 +30,7 @@ const tokensPriceQuery = gql`
publishMarketFeeAddress publishMarketFeeAddress
publishMarketFeeToken publishMarketFeeToken
publishMarketFeeAmount publishMarketFeeAmount
templateId
orders( orders(
where: { payer: $account } where: { payer: $account }
orderBy: createdTimestamp orderBy: createdTimestamp
@ -84,6 +85,7 @@ const tokenPriceQuery = gql`
id id
symbol symbol
name name
templateId
publishMarketFeeAddress publishMarketFeeAddress
publishMarketFeeToken publishMarketFeeToken
publishMarketFeeAmount publishMarketFeeAmount
@ -160,7 +162,7 @@ function getAccessDetailsFromTokenPrice(
// the last valid order should be the last reuse order tx id if there is one // the last valid order should be the last reuse order tx id if there is one
accessDetails.validOrderTx = reusedOrder?.tx || order?.tx accessDetails.validOrderTx = reusedOrder?.tx || order?.tx
} }
accessDetails.templateId = tokenPrice.templateId
// TODO: fetch order fee from sub query // TODO: fetch order fee from sub query
accessDetails.publisherMarketOrderFee = tokenPrice?.publishMarketFeeAmount accessDetails.publisherMarketOrderFee = tokenPrice?.publishMarketFeeAmount
@ -169,6 +171,7 @@ function getAccessDetailsFromTokenPrice(
const dispenser = tokenPrice.dispensers[0] const dispenser = tokenPrice.dispensers[0]
accessDetails.type = 'free' accessDetails.type = 'free'
accessDetails.addressOrId = dispenser.token.id accessDetails.addressOrId = dispenser.token.id
accessDetails.price = '0' accessDetails.price = '0'
accessDetails.isPurchasable = dispenser.active accessDetails.isPurchasable = dispenser.active
accessDetails.datatoken = { accessDetails.datatoken = {

View File

@ -61,13 +61,13 @@ export function generateBaseQuery(
...(baseQueryParams.filters || []), ...(baseQueryParams.filters || []),
baseQueryParams.chainIds baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds) ? getFilterTerm('chainId', baseQueryParams.chainIds)
: [], : '',
getFilterTerm('_index', 'aquarius'), getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory ...(baseQueryParams.ignorePurgatory
? [] ? ''
: [getFilterTerm('purgatory.state', false)]), : [getFilterTerm('purgatory.state', false)]),
...(baseQueryParams.ignoreState ...(baseQueryParams.ignoreState
? [] ? ''
: [ : [
{ {
bool: { bool: {

View File

@ -1,3 +1,5 @@
import { Asset } from '@oceanprotocol/lib'
// Boolean value that will be true if we are inside a browser, false otherwise // Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined' export const isBrowser = typeof window !== 'undefined'
@ -14,3 +16,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
} }
return arr return arr
} }
export function sortAssets(items: Asset[], sorted: string[]) {
items.sort(function (a, b) {
return sorted?.indexOf(a.id) - sorted?.indexOf(b.id)
})
return items
}

5
src/@utils/ipfs.ts Normal file
View File

@ -0,0 +1,5 @@
import * as isIPFS from 'is-ipfs'
export function isCID(value: string) {
return isIPFS.cid(value)
}

View File

@ -82,10 +82,13 @@ export function generateNftCreateData(
export function decodeTokenURI(tokenURI: string): NftMetadata { export function decodeTokenURI(tokenURI: string): NftMetadata {
if (!tokenURI) return undefined if (!tokenURI) return undefined
try { try {
const nftMeta = JSON.parse( const nftMeta = tokenURI.includes('data:application/json')
Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString() ? (JSON.parse(
) as NftMetadata Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
) as NftMetadata)
: ({ image: tokenURI } as NftMetadata)
return nftMeta return nftMeta
} catch (error) { } catch (error) {

View File

@ -1,5 +1,20 @@
import { formatCurrency } from '@coingecko/cryptoformat'
import { Decimal } from 'decimal.js' import { Decimal } from 'decimal.js'
export function formatNumber(
price: number,
locale: string,
decimals?: string
): string {
return formatCurrency(price, '', locale, false, {
// Not exactly clear what `significant figures` are for this library,
// but setting this seems to give us the formatting we want.
// See https://github.com/oceanprotocol/market/issues/70
significantFigures: 4,
...(decimals && { decimalPlaces: Number(decimals) })
})
}
// Run decimal.js comparison // Run decimal.js comparison
// http://mikemcl.github.io/decimal.js/#cmp // http://mikemcl.github.io/decimal.js/#cmp
export function compareAsBN(balance: string, price: string): boolean { export function compareAsBN(balance: string, price: string): boolean {

View File

@ -3,12 +3,15 @@ import {
approve, approve,
approveWei, approveWei,
Datatoken, Datatoken,
Dispenser,
FixedRateExchange,
FreOrderParams, FreOrderParams,
LoggerInstance, LoggerInstance,
OrderParams, OrderParams,
ProviderComputeInitialize, ProviderComputeInitialize,
ProviderFees, ProviderFees,
ProviderInstance ProviderInstance,
ProviderInitialize
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import Web3 from 'web3' import Web3 from 'web3'
import { getOceanConfig } from './ocean' import { getOceanConfig } from './ocean'
@ -20,6 +23,26 @@ import {
} from '../../app.config' } from '../../app.config'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
async function initializeProvider(
asset: AssetExtended,
accountId: string,
providerFees?: ProviderFees
): Promise<ProviderInitialize> {
if (providerFees) return
try {
const provider = await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
)
return provider
} catch (error) {
LoggerInstance.log('[Initialize Provider] Error:', error)
}
}
/** /**
* @param web3 * @param web3
* @param asset * @param asset
@ -40,15 +63,11 @@ export async function order(
const datatoken = new Datatoken(web3) const datatoken = new Datatoken(web3)
const config = getOceanConfig(asset.chainId) const config = getOceanConfig(asset.chainId)
const initializeData = const initializeData = await initializeProvider(
!providerFees && asset,
(await ProviderInstance.initialize( accountId,
asset.id, providerFees
asset.services[0].id, )
0,
accountId,
asset.services[0].serviceEndpoint
))
const orderParams = { const orderParams = {
consumer: computeConsumerAddress || accountId, consumer: computeConsumerAddress || accountId,
@ -66,22 +85,6 @@ export async function order(
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
const txApprove = await approve(
web3,
config,
accountId,
asset.accessDetails.baseToken.address,
asset.accessDetails.datatoken.address,
await amountToUnits(
web3,
asset?.accessDetails?.baseToken?.address,
orderPriceAndFees.price
),
false
)
if (!txApprove) {
return
}
const freParams = { const freParams = {
exchangeContract: config.fixedRateExchangeAddress, exchangeContract: config.fixedRateExchangeAddress,
@ -92,23 +95,96 @@ export async function order(
swapMarketFee: consumeMarketFixedSwapFee, swapMarketFee: consumeMarketFixedSwapFee,
marketFeeAddress marketFeeAddress
} as FreOrderParams } as FreOrderParams
const tx = await datatoken.buyFromFreAndOrder(
asset.accessDetails.datatoken.address,
accountId,
orderParams,
freParams
)
return tx if (asset.accessDetails.templateId === 1) {
// buy datatoken
const txApprove = await approve(
web3,
config,
accountId,
asset.accessDetails.baseToken.address,
config.fixedRateExchangeAddress,
await amountToUnits(
web3,
asset?.accessDetails?.baseToken?.address,
orderPriceAndFees.price
),
false
)
if (!txApprove) {
return
}
const fre = new FixedRateExchange(config.fixedRateExchangeAddress, web3)
const freTx = await fre.buyDatatokens(
accountId,
asset.accessDetails?.addressOrId,
'1',
orderPriceAndFees.price,
marketFeeAddress,
consumeMarketFixedSwapFee
)
return await datatoken.startOrder(
asset.accessDetails.datatoken.address,
accountId,
orderParams.consumer,
orderParams.serviceIndex,
orderParams._providerFee,
orderParams._consumeMarketFee
)
}
if (asset.accessDetails.templateId === 2) {
const txApprove = await approve(
web3,
config,
accountId,
asset.accessDetails.baseToken.address,
asset.accessDetails.datatoken.address,
await amountToUnits(
web3,
asset?.accessDetails?.baseToken?.address,
orderPriceAndFees.price
),
false
)
if (!txApprove) {
return
}
return await datatoken.buyFromFreAndOrder(
asset.accessDetails.datatoken.address,
accountId,
orderParams,
freParams
)
}
break
} }
case 'free': { case 'free': {
const tx = await datatoken.buyFromDispenserAndOrder( if (asset.accessDetails.templateId === 1) {
asset.services[0].datatokenAddress, const dispenser = new Dispenser(config.dispenserAddress, web3)
accountId, const dispenserTx = await dispenser.dispense(
orderParams, asset.accessDetails?.datatoken.address,
config.dispenserAddress accountId,
) '1',
return tx accountId
)
return await datatoken.startOrder(
asset.accessDetails.datatoken.address,
accountId,
orderParams.consumer,
orderParams.serviceIndex,
orderParams._providerFee,
orderParams._consumeMarketFee
)
}
if (asset.accessDetails.templateId === 2) {
return await datatoken.buyFromDispenserAndOrder(
asset.services[0].datatokenAddress,
accountId,
orderParams,
config.dispenserAddress
)
}
} }
} }
} }
@ -130,15 +206,11 @@ export async function reuseOrder(
providerFees?: ProviderFees providerFees?: ProviderFees
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const datatoken = new Datatoken(web3) const datatoken = new Datatoken(web3)
const initializeData = const initializeData = await initializeProvider(
!providerFees && asset,
(await ProviderInstance.initialize( accountId,
asset.id, providerFees
asset.services[0].id, )
0,
accountId,
asset.services[0].serviceEndpoint
))
const tx = await datatoken.reuseOrder( const tx = await datatoken.reuseOrder(
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,

View File

@ -1,9 +1,11 @@
import { import {
Arweave,
ComputeAlgorithm, ComputeAlgorithm,
ComputeAsset, ComputeAsset,
ComputeEnvironment, ComputeEnvironment,
downloadFileBrowser, downloadFileBrowser,
FileInfo, FileInfo,
Ipfs,
LoggerInstance, LoggerInstance,
ProviderComputeInitializeResults, ProviderComputeInitializeResults,
ProviderInstance, ProviderInstance,
@ -83,18 +85,45 @@ export async function getFileDidInfo(
} }
} }
export async function getFileUrlInfo( export async function getFileInfo(
url: string, file: string,
providerUrl: string providerUrl: string,
storageType: string
): Promise<FileInfo[]> { ): Promise<FileInfo[]> {
try { try {
const fileUrl: UrlFile = { let response
type: 'url', switch (storageType) {
index: 0, case 'ipfs': {
url, const fileIPFS: Ipfs = {
method: 'get' type: 'ipfs',
hash: file
}
response = await ProviderInstance.getFileInfo(fileIPFS, providerUrl)
break
}
case 'arweave': {
const fileArweave: Arweave = {
type: 'arweave',
transactionId: file
}
response = await ProviderInstance.getFileInfo(fileArweave, providerUrl)
break
}
default: {
const fileUrl: UrlFile = {
type: 'url',
index: 0,
url: file,
method: 'get'
}
response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
break
}
} }
const response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
return response return response
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(error.message)

View File

@ -2,23 +2,9 @@ import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import { getUrqlClientInstance } from '@context/UrqlProvider' import { getUrqlClientInstance } from '@context/UrqlProvider'
import { getOceanConfig } from './ocean' import { getOceanConfig } from './ocean'
import { AssetPreviousOrder } from '../@types/subgraph/AssetPreviousOrder'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery' import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery'
import appConfig from '../../app.config'
const PreviousOrderQuery = gql`
query AssetPreviousOrder($id: String!, $account: String!) {
orders(
first: 1
where: { datatoken: $id, payer: $account }
orderBy: createdTimestamp
orderDirection: desc
) {
createdTimestamp
tx
}
}
`
const UserTokenOrders = gql` const UserTokenOrders = gql`
query OrdersData($user: String!) { query OrdersData($user: String!) {
@ -76,6 +62,11 @@ export function getSubgraphUri(chainId: number): string {
export function getQueryContext(chainId: number): OperationContext { export function getQueryContext(chainId: number): OperationContext {
try { try {
if (!appConfig.chainIdsSupported.includes(chainId))
throw Object.assign(
new Error('network not supported, query context cancelled')
)
const queryContext: OperationContext = { const queryContext: OperationContext = {
url: `${getSubgraphUri( url: `${getSubgraphUri(
Number(chainId) Number(chainId)

View File

@ -1,7 +1,7 @@
import { AllLocked } from 'src/@types/subgraph/AllLocked' import { AllLockedQuery } from 'src/@types/subgraph/AllLockedQuery'
import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations' import { OwnAllocationsQuery } from 'src/@types/subgraph/OwnAllocationsQuery'
import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation' import { NftOwnAllocationQuery } from 'src/@types/subgraph/NftOwnAllocationQuery'
import { OceanLocked } from 'src/@types/subgraph/OceanLocked' import { OceanLockedQuery } from 'src/@types/subgraph/OceanLockedQuery'
import { gql, OperationResult } from 'urql' import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph' import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios' import axios from 'axios'
@ -12,11 +12,11 @@ import {
NetworkType NetworkType
} from '@hooks/useNetworkMetadata' } from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius' import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from 'app.config' import { chainIdsSupported } from '../../app.config'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
const AllLocked = gql` const AllLocked = gql`
query AllLocked { query AllLockedQuery {
veOCEANs(first: 1000) { veOCEANs(first: 1000) {
lockedAmount lockedAmount
} }
@ -24,7 +24,7 @@ const AllLocked = gql`
` `
const OwnAllocations = gql` const OwnAllocations = gql`
query OwnAllocations($address: String) { query OwnAllocationsQuery($address: String) {
veAllocations(where: { allocationUser: $address }) { veAllocations(where: { allocationUser: $address }) {
id id
nftAddress nftAddress
@ -33,7 +33,7 @@ const OwnAllocations = gql`
} }
` `
const NftOwnAllocation = gql` const NftOwnAllocation = gql`
query NftOwnAllocation($address: String, $nftAddress: String) { query NftOwnAllocationQuery($address: String, $nftAddress: String) {
veAllocations( veAllocations(
where: { allocationUser: $address, nftAddress: $nftAddress } where: { allocationUser: $address, nftAddress: $nftAddress }
) { ) {
@ -42,7 +42,7 @@ const NftOwnAllocation = gql`
} }
` `
const OceanLocked = gql` const OceanLocked = gql`
query OceanLocked($address: ID!) { query OceanLockedQuery($address: ID!) {
veOCEAN(id: $address) { veOCEAN(id: $address) {
id id
lockedAmount lockedAmount
@ -87,7 +87,7 @@ export async function getNftOwnAllocation(
): Promise<number> { ): Promise<number> {
const veNetworkId = getVeChainNetworkId(networkId) const veNetworkId = getVeChainNetworkId(networkId)
const queryContext = getQueryContext(veNetworkId) const queryContext = getQueryContext(veNetworkId)
const fetchedAllocation: OperationResult<NftOwnAllocation, any> = const fetchedAllocation: OperationResult<NftOwnAllocationQuery, any> =
await fetchData( await fetchData(
NftOwnAllocation, NftOwnAllocation,
{ {
@ -115,7 +115,7 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
0 0
) )
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData( const fetchedLocked: OperationResult<AllLockedQuery, any> = await fetchData(
AllLocked, AllLocked,
null, null,
queryContext queryContext
@ -136,11 +136,12 @@ export async function getLocked(
const veNetworkIds = getVeChainNetworkIds(networkIds) const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) { for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i]) const queryContext = getQueryContext(veNetworkIds[i])
const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData( const fetchedLocked: OperationResult<OceanLockedQuery, any> =
OceanLocked, await fetchData(
{ address: userAddress.toLowerCase() }, OceanLocked,
queryContext { address: userAddress.toLowerCase() },
) queryContext
)
fetchedLocked.data?.veOCEAN?.lockedAmount && fetchedLocked.data?.veOCEAN?.lockedAmount &&
(total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount)) (total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
@ -157,7 +158,7 @@ export async function getOwnAllocations(
const veNetworkIds = getVeChainNetworkIds(networkIds) const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) { for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i]) const queryContext = getQueryContext(veNetworkIds[i])
const fetchedAllocations: OperationResult<OwnAllocations, any> = const fetchedAllocations: OperationResult<OwnAllocationsQuery, any> =
await fetchData( await fetchData(
OwnAllocations, OwnAllocations,
{ address: userAddress.toLowerCase() }, { address: userAddress.toLowerCase() },

57
src/@utils/yup.ts Normal file
View File

@ -0,0 +1,57 @@
import { isCID } from '@utils/ipfs'
import isUrl from 'is-url-superb'
import * as Yup from 'yup'
export function testLinks(isEdit?: boolean) {
return Yup.string().test((value, context) => {
const { type } = context.parent
let validField
let errorMessage
switch (type) {
// we allow submit if the type input is hidden as will be ignore
case 'hidden':
validField = true
break
case 'url':
validField = isUrl(value?.toString() || '')
// if we're in publish, the field must be valid
if (!validField) {
validField = false
errorMessage = 'Must be a valid url.'
}
// we allow submit if we're in the edit page and the field is empty
if (
(!value?.toString() && isEdit) ||
(!value?.toString() && context.path === 'services[0].links[0].url')
) {
validField = true
}
// if the url has google drive, we need to block the user from submit
if (value?.toString().includes('drive.google')) {
validField = false
errorMessage =
'Google Drive is not a supported hosting service. Please use an alternative.'
}
break
case 'ipfs':
validField = isCID(value?.toString())
errorMessage = !value?.toString() ? 'CID required.' : 'CID not valid.'
break
case 'arweave':
validField = !value?.toString().includes('http')
errorMessage = !value?.toString()
? 'Transaction ID required.'
: 'Transaction ID not valid.'
break
}
if (!validField) {
return context.createError({
message: errorMessage
})
}
return true
})
}

View File

@ -69,11 +69,13 @@ export default function AssetList({
const styleClasses = `${styles.assetList} ${className || ''}` const styleClasses = `${styles.assetList} ${className || ''}`
return assetsWithPrices && !loading ? ( return loading ? (
<LoaderArea />
) : (
<> <>
<div className={styleClasses}> <div className={styleClasses}>
{assetsWithPrices.length > 0 ? ( {assetsWithPrices?.length > 0 ? (
assetsWithPrices.map((assetWithPrice) => ( assetsWithPrices?.map((assetWithPrice) => (
<AssetTeaser <AssetTeaser
asset={assetWithPrice} asset={assetWithPrice}
key={assetWithPrice.id} key={assetWithPrice.id}
@ -95,7 +97,5 @@ export default function AssetList({
/> />
)} )}
</> </>
) : (
<LoaderArea />
) )
} }

View File

@ -48,7 +48,7 @@
} }
.footer { .footer {
margin-top: calc(var(--spacer) / 12); margin-top: calc(var(--spacer) / 24);
} }
.typeLabel { .typeLabel {

View File

@ -8,8 +8,8 @@ import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName' import NetworkName from '@shared/NetworkName'
import styles from './index.module.css' import styles from './index.module.css'
import { getServiceByName } from '@utils/ddo' import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { formatNumber } from '@utils/numbers'
export declare type AssetTeaserProps = { export declare type AssetTeaserProps = {
asset: AssetExtended asset: AssetExtended
@ -77,16 +77,37 @@ export default function AssetTeaser({
<footer className={styles.footer}> <footer className={styles.footer}>
{allocated && allocated > 0 ? ( {allocated && allocated > 0 ? (
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{allocated < 0 {allocated < 0 ? (
? '' ''
: `${formatPrice(allocated, locale)} veOCEAN`} ) : (
<>
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
veOCEAN
</>
)}
</span> </span>
) : null} ) : null}
{orders && orders > 0 ? ( {orders && orders > 0 ? (
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{orders < 0 {orders < 0 ? (
? 'N/A' 'N/A'
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`} ) : (
<>
<strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
</>
)}
</span>
) : null}
{asset.views && asset.views > 0 ? (
<span className={styles.typeLabel}>
{asset.views < 0 ? (
'N/A'
) : (
<>
<strong>{asset.views}</strong>{' '}
{asset.views === 1 ? 'view' : 'views'}
</>
)}
</span> </span>
) : null} ) : null}
</footer> </footer>

View File

@ -75,6 +75,7 @@ export default function ContainerInput(props: InputProps): ReactElement {
name={`${field.name}[0].url`} name={`${field.name}[0].url`}
checkUrl={false} checkUrl={false}
isLoading={isLoading} isLoading={isLoading}
storageType={'url'}
handleButtonClick={handleValidation} handleButtonClick={handleValidation}
/> />
)} )}

View File

@ -23,7 +23,7 @@ export default function FileInfo({
{hideUrl ? 'https://oceanprotocol/placeholder' : file.url} {hideUrl ? 'https://oceanprotocol/placeholder' : file.url}
</h3> </h3>
<ul> <ul>
<li className={styles.success}> URL confirmed</li> <li className={styles.success}> File confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>} {file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
{contentTypeCleaned && <li>{contentTypeCleaned}</li>} {contentTypeCleaned && <li>{contentTypeCleaned}</li>}
</ul> </ul>

View File

@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react' import React from 'react'
import FilesInput from './index' import FilesInput from './index'
import { useField } from 'formik' import { useField } from 'formik'
import { getFileUrlInfo } from '@utils/provider' import { getFileInfo } from '@utils/provider'
jest.mock('formik') jest.mock('formik')
jest.mock('@utils/provider') jest.mock('@utils/provider')
@ -48,16 +48,17 @@ const mockForm = {
describe('@shared/FormInput/InputElement/FilesInput', () => { describe('@shared/FormInput/InputElement/FilesInput', () => {
it('renders without crashing', async () => { it('renders without crashing', async () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers]) ;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
;(getFileUrlInfo as jest.Mock).mockReturnValue([ ;(getFileInfo as jest.Mock).mockReturnValue([
{ {
valid: true, valid: true,
url: 'https://hello.com', url: 'https://hello.com',
type: 'url',
contentType: 'text/html', contentType: 'text/html',
contentLength: 100 contentLength: 100
} }
]) ])
render(<FilesInput form={mockForm} {...props} />) render(<FilesInput form={mockForm} field={mockField} {...props} />)
expect(screen.getByText('Validate')).toBeInTheDocument() expect(screen.getByText('Validate')).toBeInTheDocument()
fireEvent.click(screen.getByText('Validate')) fireEvent.click(screen.getByText('Validate'))
@ -67,13 +68,14 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
expect(mockHelpers.setValue).toHaveBeenCalled() expect(mockHelpers.setValue).toHaveBeenCalled()
}) })
it('renders fileinfo when file is valid', () => { it('renders fileinfo when file url is valid', () => {
;(useField as jest.Mock).mockReturnValue([ ;(useField as jest.Mock).mockReturnValue([
{ {
value: [ value: [
{ {
valid: true, valid: true,
url: 'https://hello.com', url: 'https://hello.com',
type: 'url',
contentType: 'text/html', contentType: 'text/html',
contentLength: 100 contentLength: 100
} }
@ -82,10 +84,52 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta, mockMeta,
mockHelpers mockHelpers
]) ])
render(<FilesInput {...props} />) render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('https://hello.com')).toBeInTheDocument() expect(screen.getByText('https://hello.com')).toBeInTheDocument()
}) })
it('renders fileinfo when ipfs is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
hash: 'bafkreicxccbk4blsx5qtovqfgsuutxjxom47dvyzyz3asi2ggjg5ipwlc4',
type: 'ipfs',
contentLength: '40492',
contentType: 'text/csv',
index: 0
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('✓ File confirmed')).toBeInTheDocument()
})
it('renders fileinfo when arweave is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
transactionId: 'T6NL8Zc0LCbT3bF9HacAGQC4W0_hW7b3tXbm8OtWtlA',
type: 'arweave',
contentLength: '57043',
contentType: 'image/jpeg',
index: 0
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('✓ File confirmed')).toBeInTheDocument()
})
it('renders fileinfo without contentType', () => { it('renders fileinfo without contentType', () => {
;(useField as jest.Mock).mockReturnValue([ ;(useField as jest.Mock).mockReturnValue([
{ {
@ -93,6 +137,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
{ {
valid: true, valid: true,
url: 'https://hello.com', url: 'https://hello.com',
type: 'url',
contentLength: 100 contentLength: 100
} }
] ]
@ -100,7 +145,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta, mockMeta,
mockHelpers mockHelpers
]) ])
render(<FilesInput {...props} />) render(<FilesInput {...props} field={mockField} />)
}) })
it('renders fileinfo placeholder when hideUrl is passed', () => { it('renders fileinfo placeholder when hideUrl is passed', () => {
@ -117,7 +162,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta, mockMeta,
mockHelpers mockHelpers
]) ])
render(<FilesInput {...props} />) render(<FilesInput {...props} field={mockField} />)
expect( expect(
screen.getByText('https://oceanprotocol/placeholder') screen.getByText('https://oceanprotocol/placeholder')
).toBeInTheDocument() ).toBeInTheDocument()

View File

@ -1,9 +1,9 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import { useField } from 'formik' import { useField } from 'formik'
import FileInfo from './Info' import FileInfo from './Info'
import UrlInput from '../URLInput' import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput' import { InputProps } from '@shared/FormInput'
import { getFileUrlInfo } from '@utils/provider' import { getFileInfo } from '@utils/provider'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
@ -12,16 +12,27 @@ export default function FilesInput(props: InputProps): ReactElement {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { asset } = useAsset() const { asset } = useAsset()
const providerUrl = props.form?.values?.services
? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
const storageType = field.value[0].type
async function handleValidation(e: React.SyntheticEvent, url: string) { async function handleValidation(e: React.SyntheticEvent, url: string) {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf' // File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e?.preventDefault() e?.preventDefault()
try { try {
const providerUrl = props.form?.values?.services
? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
setIsLoading(true) setIsLoading(true)
const checkedFile = await getFileUrlInfo(url, providerUrl)
// TODO: handled on provider
if (url.includes('drive.google')) {
throw Error(
'Google Drive is not a supported hosting service. Please use an alternative.'
)
}
const checkedFile = await getFileInfo(url, providerUrl, storageType)
// error if something's not right from response // error if something's not right from response
if (!checkedFile) if (!checkedFile)
@ -31,7 +42,7 @@ export default function FilesInput(props: InputProps): ReactElement {
throw Error('✗ No valid file detected. Check your URL and try again.') throw Error('✗ No valid file detected. Check your URL and try again.')
// if all good, add file to formik state // if all good, add file to formik state
helpers.setValue([{ url, ...checkedFile[0] }]) helpers.setValue([{ url, type: storageType, ...checkedFile[0] }])
} catch (error) { } catch (error) {
props.form.setFieldError(`${field.name}[0].url`, error.message) props.form.setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
@ -42,7 +53,9 @@ export default function FilesInput(props: InputProps): ReactElement {
function handleClose() { function handleClose() {
helpers.setTouched(false) helpers.setTouched(false)
helpers.setValue(meta.initialValue) helpers.setValue([
{ url: '', type: storageType === 'hidden' ? 'ipfs' : storageType }
])
} }
return ( return (
@ -56,7 +69,9 @@ export default function FilesInput(props: InputProps): ReactElement {
{...props} {...props}
name={`${field.name}[0].url`} name={`${field.name}[0].url`}
isLoading={isLoading} isLoading={isLoading}
checkUrl={true}
handleButtonClick={handleValidation} handleButtonClick={handleValidation}
storageType={storageType}
/> />
)} )}
</> </>

View File

@ -6,6 +6,7 @@ import styles from './index.module.css'
import InputGroup from '@shared/FormInput/InputGroup' import InputGroup from '@shared/FormInput/InputGroup'
import InputElement from '@shared/FormInput/InputElement' import InputElement from '@shared/FormInput/InputElement'
import isUrl from 'is-url-superb' import isUrl from 'is-url-superb'
import { isCID } from '@utils/ipfs'
export interface URLInputProps { export interface URLInputProps {
submitText: string submitText: string
@ -13,6 +14,7 @@ export interface URLInputProps {
isLoading: boolean isLoading: boolean
name: string name: string
checkUrl?: boolean checkUrl?: boolean
storageType?: string
} }
export default function URLInput({ export default function URLInput({
@ -21,6 +23,7 @@ export default function URLInput({
isLoading, isLoading,
name, name,
checkUrl, checkUrl,
storageType,
...props ...props
}: URLInputProps): ReactElement { }: URLInputProps): ReactElement {
const [field, meta] = useField(name) const [field, meta] = useField(name)
@ -32,7 +35,8 @@ export default function URLInput({
setIsButtonDisabled( setIsButtonDisabled(
!field?.value || !field?.value ||
field.value === '' || field.value === '' ||
(checkUrl && !isUrl(field.value)) || (checkUrl && storageType === 'url' && !isUrl(field.value)) ||
(checkUrl && storageType === 'ipfs' && !isCID(field.value)) ||
field.value.includes('javascript:') || field.value.includes('javascript:') ||
meta?.error meta?.error
) )

View File

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react' import React, { ReactElement, useCallback, useState } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
import { InputProps } from '..' import { InputProps } from '..'
import FilesInput from './FilesInput' import FilesInput from './FilesInput'
@ -11,6 +11,7 @@ import Nft from './Nft'
import InputRadio from './Radio' import InputRadio from './Radio'
import ContainerInput from '@shared/FormInput/InputElement/ContainerInput' import ContainerInput from '@shared/FormInput/InputElement/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete' import TagsAutoComplete from './TagsAutoComplete'
import TabsFile from '@shared/atoms/TabsFile'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -55,6 +56,7 @@ export default function InputElement({
...props ...props
}: InputProps): ReactElement { }: InputProps): ReactElement {
const styleClasses = cx({ select: true, [size]: size }) const styleClasses = cx({ select: true, [size]: size })
switch (props.type) { switch (props.type) {
case 'select': { case 'select': {
const sortedOptions = const sortedOptions =
@ -80,6 +82,26 @@ export default function InputElement({
</select> </select>
) )
} }
case 'tabs': {
const tabs: any = []
props.fields.map((field: any, i) => {
return tabs.push({
title: field.title,
field,
props,
content: (
<FilesInput
key={`fileInput_${i}`}
{...field}
form={form}
{...props}
/>
)
})
})
return <TabsFile items={tabs} className={styles.pricing} />
}
case 'textarea': case 'textarea':
return <textarea id={props.name} className={styles.textarea} {...props} /> return <textarea id={props.name} className={styles.textarea} {...props} />

View File

@ -32,6 +32,7 @@ export interface InputProps {
type?: string type?: string
options?: string[] | AssetSelectionAsset[] | BoxSelectionOption[] options?: string[] | AssetSelectionAsset[] | BoxSelectionOption[]
sortOptions?: boolean sortOptions?: boolean
fields?: FieldInputProps<any>[]
additionalComponent?: ReactElement additionalComponent?: ReactElement
value?: string | number value?: string | number
onChange?( onChange?(

View File

@ -1,17 +1,8 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { formatCurrency } from '@coingecko/cryptoformat'
import Conversion from './Conversion' import Conversion from './Conversion'
import styles from './PriceUnit.module.css' import styles from './PriceUnit.module.css'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { formatNumber } from '@utils/numbers'
export function formatPrice(price: number, locale: string): string {
return formatCurrency(price, '', locale, false, {
// Not exactly clear what `significant figures` are for this library,
// but setting this seems to give us the formatting we want.
// See https://github.com/oceanprotocol/market/issues/70
significantFigures: 4
})
}
export default function PriceUnit({ export default function PriceUnit({
price, price,
@ -19,7 +10,8 @@ export default function PriceUnit({
size = 'small', size = 'small',
conversion, conversion,
symbol, symbol,
type type,
decimals
}: { }: {
price: number price: number
type?: string type?: string
@ -27,6 +19,7 @@ export default function PriceUnit({
size?: 'small' | 'mini' | 'large' size?: 'small' | 'mini' | 'large'
conversion?: boolean conversion?: boolean
symbol?: string symbol?: string
decimals?: string
}): ReactElement { }): ReactElement {
const { locale } = useUserPreferences() const { locale } = useUserPreferences()
@ -37,7 +30,7 @@ export default function PriceUnit({
) : ( ) : (
<> <>
<div> <div>
{Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '} {Number.isNaN(price) ? '-' : formatNumber(price, locale, decimals)}{' '}
<span className={styles.symbol}>{symbol}</span> <span className={styles.symbol}>{symbol}</span>
</div> </div>
{conversion && <Conversion price={price} symbol={symbol} />} {conversion && <Conversion price={price} symbol={symbol} />}

View File

@ -1,15 +1,16 @@
import React from 'react' import React from 'react'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import * as axios from 'axios' import axios from 'axios'
import Publisher from './' import Publisher from './'
const account = '0x0000000000000000000000000000000000000000' const account = '0x0000000000000000000000000000000000000000'
jest.mock('axios') jest.mock('axios')
const axiosMock = axios as jest.Mocked<typeof axios>
describe('@shared/Publisher', () => { describe('@shared/Publisher', () => {
test('should return correct markup by default', async () => { test('should return correct markup by default', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } }) Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
) )
@ -22,7 +23,7 @@ describe('@shared/Publisher', () => {
}) })
test('should truncate account by default', async () => { test('should truncate account by default', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } }) Promise.resolve({ data: { name: null } })
) )
@ -33,7 +34,7 @@ describe('@shared/Publisher', () => {
}) })
test('should return correct markup in minimal state', async () => { test('should return correct markup in minimal state', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } }) Promise.resolve({ data: { name: null } })
) )
@ -44,7 +45,7 @@ describe('@shared/Publisher', () => {
}) })
test('should return markup with empty account', async () => { test('should return markup with empty account', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } }) Promise.resolve({ data: { name: null } })
) )

View File

@ -0,0 +1,91 @@
.tabListContainer {
margin-left: auto;
margin-right: auto;
border-bottom: 1px solid var(--border-color);
max-width: 100%;
}
.tabList {
text-align: center;
padding: calc(var(--spacer) / 2) 0 0 0;
display: flex;
overflow-x: auto;
scrollbar-width: none;
}
.tab {
display: inline-block;
padding: calc(var(--spacer) / 8) calc(var(--spacer) / 2);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
text-transform: uppercase;
color: var(--color-secondary);
margin-right: -1px;
background: none;
border: 0;
}
.tabHidden {
display: none !important;
}
.tab,
.tab label {
cursor: pointer;
}
.tab:focus-visible {
outline: none;
}
.tab[aria-selected='true'] {
background-color: inherit;
color: inherit;
list-style: none;
background-color: var(--background-body);
border: 1px solid var(--border-color);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
border-bottom: 0;
}
.tab[aria-disabled='true'] {
cursor: not-allowed;
}
.tab > div {
margin: 0;
}
.tabContent {
padding: calc(var(--spacer) / 2);
border: 1px solid var(--border-color);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-top: 0;
}
.tabLabel {
color: var(--font-color-text);
font-size: var(--font-size-small);
font-family: var(--font-family-heading);
line-height: 1.2;
display: block;
margin-bottom: calc(var(--spacer) / 2);
}
@media (min-width: 40rem) {
.tabContent {
padding: calc(var(--spacer) / 2);
}
}
@media (min-width: 60rem) {
.tab {
padding: calc(var(--spacer) / 8) var(--spacer);
}
}
.radio {
composes: radio from '../../FormInput/InputElement/Radio/index.module.css';
}

View File

@ -0,0 +1,52 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Tabs, { TabsProps } from '@shared/atoms/Tabs'
export default {
title: 'Component/@shared/atoms/Tabs',
component: Tabs
} as ComponentMeta<typeof Tabs>
const Template: ComponentStory<typeof Tabs> = (args) => <Tabs {...args} />
interface Props {
args: TabsProps
}
const items = [
{
title: 'First tab',
content: 'this is the content for the first tab'
},
{
title: 'Second tab',
content: 'this is the content for the second tab'
},
{
title: 'Third tab',
content: 'this is the content for the third tab'
}
]
export const Default = Template.bind({})
Default.args = {
items
}
export const WithRadio: Props = Template.bind({})
WithRadio.args = {
items,
showRadio: true
}
export const WithDefaultIndex: Props = Template.bind({})
WithDefaultIndex.args = {
items,
defaultIndex: 1
}
export const LotsOfTabs: Props = Template.bind({})
LotsOfTabs.args = {
items: items.flatMap((i) => [i, i, i])
}

View File

@ -0,0 +1,25 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { Default, WithRadio } from './index.stories'
describe('Tabs', () => {
test('should be able to change', async () => {
render(<Default {...Default.args} />)
fireEvent.click(screen.getByText('Second tab'))
const secondTab = await screen.findByText(/content for the second tab/i)
expect(secondTab).toBeInTheDocument()
})
test('should fire custom change handler', async () => {
const handler = jest.fn()
render(<Default {...Default.args} handleTabChange={handler} />)
fireEvent.click(screen.getByText('Second tab'))
expect(handler).toBeCalledTimes(1)
})
test('renders WithRadio', () => {
render(<Default {...WithRadio.args} />)
})
})

View File

@ -0,0 +1,109 @@
import Label from '@shared/FormInput/Label'
import Markdown from '@shared/Markdown'
import { useFormikContext } from 'formik'
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import { FormPublishData } from 'src/components/Publish/_types'
import Tooltip from '../Tooltip'
import styles from './index.module.css'
export interface TabsItem {
field: any
title: string
content: ReactNode
disabled?: boolean
props: any
}
export interface TabsProps {
items: TabsItem[]
className?: string
}
export default function TabsFile({
items,
className
}: TabsProps): ReactElement {
const { values, setFieldValue } = useFormikContext<FormPublishData>()
const [tabIndex, setTabIndex] = useState(0)
// hide tabs if are hidden
const isHidden = items[tabIndex].props.value[0].type === 'hidden'
const setIndex = (tabName: string) => {
const index = items.findIndex((tab: any) => {
if (tab.title !== tabName) return false
return tab
})
setTabIndex(index)
setFieldValue(`${items[index].props.name}[0]`, {
url: '',
type: items[index].field.value
})
}
const handleTabChange = (tabName: string) => {
setIndex(tabName)
}
let textToolTip = false
if (values?.services) {
textToolTip = values.services[0].access === 'compute'
}
return (
<ReactTabs className={`${className || ''}`} defaultIndex={tabIndex}>
<div className={styles.tabListContainer}>
<TabList className={styles.tabList}>
{items.map((item, index) => {
return (
<Tab
className={`${styles.tab} ${
isHidden ? styles.tabHidden : null
}`}
key={`tab_${items[tabIndex].props.name}_${index}`}
onClick={
handleTabChange ? () => handleTabChange(item.title) : null
}
disabled={item.disabled}
>
{item.title}
</Tab>
)
})}
</TabList>
</div>
<div className={styles.tabContent}>
{items.map((item, index) => {
return (
<>
<TabPanel key={`tabpanel_${items[tabIndex].props.name}_${index}`}>
{!isHidden && (
<label className={styles.tabLabel}>
{item.field.label}
{item.field.required && (
<span title="Required" className={styles.required}>
*
</span>
)}
{item.field.help && item.field.prominentHelp && (
<Tooltip
content={
<Markdown
text={`${item.field.help} ${
textToolTip ? item.field.computeHelp : ''
}`}
/>
}
/>
)}
</label>
)}
{item.content}
</TabPanel>
</>
)
})}
</div>
</ReactTabs>
)
}

View File

@ -24,4 +24,9 @@ describe('Tags', () => {
it('renders WithoutLinks', () => { it('renders WithoutLinks', () => {
render(<Tags {...argsWithoutLinks} />) render(<Tags {...argsWithoutLinks} />)
}) })
it('renders with faulty tags', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render(<Tags items={'tags' as any} />)
})
}) })

View File

@ -30,6 +30,9 @@ export default function Tags({
className, className,
noLinks noLinks
}: TagsProps): ReactElement { }: TagsProps): ReactElement {
// safeguard against faults in the metadata
if (!(items instanceof Array)) return null
max = max || items.length max = max || items.length
const remainder = items.length - max const remainder = items.length - max
// filter out empty array items, and restrict to `max` // filter out empty array items, and restrict to `max`

View File

@ -17,7 +17,8 @@ const DefaultTrigger = React.forwardRef((props, ref: any) => {
}) })
export default function Tooltip(props: TippyProps): ReactElement { export default function Tooltip(props: TippyProps): ReactElement {
const { content, children, trigger, disabled, className, placement } = props const { className, ...restProps } = props
const { content, children, trigger, disabled, placement } = props
const [styles, api] = useSpring(() => animation.from) const [styles, api] = useSpring(() => animation.from)
function onMount() { function onMount() {
@ -60,7 +61,7 @@ export default function Tooltip(props: TippyProps): ReactElement {
onMount={onMount} onMount={onMount}
onHide={onHide} onHide={onHide}
// animation // animation
{...props} {...restProps}
> >
<div className={styleClasses}>{children || <DefaultTrigger />}</div> <div className={styleClasses}>{children || <DefaultTrigger />}</div>
</Tippy> </Tippy>

View File

@ -2,7 +2,7 @@ import { useAsset } from '@context/Asset'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Tooltip from '@shared/atoms/Tooltip' import Tooltip from '@shared/atoms/Tooltip'
import { formatPrice } from '@shared/Price/PriceUnit' import { formatNumber } from '@utils/numbers'
import { getNftOwnAllocation } from '@utils/veAllocation' import { getNftOwnAllocation } from '@utils/veAllocation'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
@ -33,8 +33,8 @@ export default function AssetStats() {
{asset?.stats?.allocated && asset?.stats?.allocated > 0 ? ( {asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
<span className={styles.stat}> <span className={styles.stat}>
<span className={styles.number}> <span className={styles.number}>
{formatPrice(asset.stats.allocated, locale)} {formatNumber(asset.stats.allocated, locale, '0')}
</span> </span>{' '}
veOCEAN veOCEAN
</span> </span>
) : null} ) : null}

View File

@ -0,0 +1,143 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import ButtonBuy, { ButtonBuyProps } from './'
const downloadProps: ButtonBuyProps = {
action: 'download',
disabled: false,
hasPreviousOrder: false,
hasDatatoken: false,
btSymbol: 'btSymbol',
dtSymbol: 'dtSymbol',
dtBalance: '100000000000',
assetTimeout: '1 day',
assetType: 'Dataset',
stepText: 'TEST',
priceType: 'fixed',
isConsumable: true,
isBalanceSufficient: true,
consumableFeedback: 'TEST: consumableFeedback'
}
const computeProps: ButtonBuyProps = {
action: 'compute',
disabled: false,
hasPreviousOrder: false,
hasDatatoken: true,
btSymbol: 'btSymbol',
dtSymbol: 'dtSymbol',
dtBalance: '100000000000',
assetTimeout: '1 day',
assetType: 'algorithm',
hasPreviousOrderSelectedComputeAsset: false,
hasDatatokenSelectedComputeAsset: true,
dtSymbolSelectedComputeAsset: 'dtSymbol',
dtBalanceSelectedComputeAsset: 'dtBalance',
selectedComputeAssetType: 'selectedComputeAssetType',
stepText: ' ',
isLoading: false,
type: 'submit',
priceType: 'fixed',
algorithmPriceType: 'free',
isBalanceSufficient: true,
isConsumable: true,
consumableFeedback: 'consumableFeedback',
isAlgorithmConsumable: true,
hasProviderFee: false,
retry: false
}
describe('Asset/AssetActions/ButtonBuy', () => {
// TESTS FOR LOADING
it('Renders Buy button without crashing', () => {
render(<ButtonBuy {...downloadProps} isLoading />)
const button = screen.getByText('TEST')
expect(button).toContainHTML('<Loader')
})
// TESTS FOR DOWNLOAD
it('Renders Buy button without crashing', () => {
render(<ButtonBuy {...downloadProps} />)
const button = screen.getByText('Buy for 1 day')
expect(button).toContainHTML('<button')
})
it('Renders Buy button without crashing when hasPreviousOrder=true', () => {
render(<ButtonBuy {...downloadProps} hasPreviousOrder />)
const button = screen.getByText('Download')
expect(button).toContainHTML('<button')
})
it('Renders retry button for download without crashing', () => {
render(<ButtonBuy {...downloadProps} retry />)
const button = screen.getByText('Retry')
expect(button).toContainHTML('<button')
})
it('Renders get button for free download without crashing', () => {
render(<ButtonBuy {...downloadProps} priceType="free" hasPreviousOrder />)
const button = screen.getByText('Download')
expect(button).toContainHTML('<button')
})
it('Renders "Get" button for free assets without crashing', () => {
render(<ButtonBuy {...downloadProps} priceType="free" />)
const button = screen.getByText('Get')
expect(button).toContainHTML('<button')
})
it('Renders Buy button without crashing', () => {
render(
<ButtonBuy
{...downloadProps}
assetTimeout="Forever"
isConsumable={false}
/>
)
const button = screen.getByText('Buy')
expect(button).toContainHTML('<button')
})
// TESTS FOR COMPUTE
it('Renders "Buy Compute Job" button for compute without crashing', () => {
render(<ButtonBuy {...computeProps} />)
const button = screen.getByText('Buy Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Buy Compute Job" button for compute without crashing', () => {
render(<ButtonBuy {...computeProps} hasDatatokenSelectedComputeAsset />)
const button = screen.getByText('Buy Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Start Compute Job" button', () => {
render(
<ButtonBuy
{...computeProps}
hasPreviousOrder
hasPreviousOrderSelectedComputeAsset
/>
)
const button = screen.getByText('Start Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Order Compute Job" button', () => {
render(<ButtonBuy {...computeProps} priceType="free" hasProviderFee />)
const button = screen.getByText('Order Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Order Compute Job" button', () => {
render(<ButtonBuy {...computeProps} priceType="free" hasProviderFee />)
const button = screen.getByText('Order Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "retry" button for compute without crashing', () => {
render(<ButtonBuy {...computeProps} retry />)
const button = screen.getByText('Retry')
expect(button).toContainHTML('<button')
})
})

View File

@ -2,8 +2,10 @@ import React, { FormEvent, ReactElement } from 'react'
import Button from '../../../@shared/atoms/Button' import Button from '../../../@shared/atoms/Button'
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 { useWeb3 } from '@context/Web3'
import Web3 from 'web3'
interface ButtonBuyProps { export interface ButtonBuyProps {
action: 'download' | 'compute' action: 'download' | 'compute'
disabled: boolean disabled: boolean
hasPreviousOrder: boolean hasPreviousOrder: boolean
@ -28,11 +30,11 @@ interface ButtonBuyProps {
priceType?: string priceType?: string
algorithmPriceType?: string algorithmPriceType?: string
isAlgorithmConsumable?: boolean isAlgorithmConsumable?: boolean
isSupportedOceanNetwork?: boolean
hasProviderFee?: boolean hasProviderFee?: boolean
retry?: boolean
} }
// TODO: we need to take a look at these messages
function getConsumeHelpText( function getConsumeHelpText(
btSymbol: string, btSymbol: string,
dtBalance: string, dtBalance: string,
@ -42,12 +44,14 @@ function getConsumeHelpText(
assetType: string, assetType: string,
isConsumable: boolean, isConsumable: boolean,
isBalanceSufficient: boolean, isBalanceSufficient: boolean,
consumableFeedback: string consumableFeedback: string,
isSupportedOceanNetwork: boolean,
web3: Web3
) { ) {
const text = const text =
isConsumable === false isConsumable === false
? consumableFeedback ? consumableFeedback
: hasPreviousOrder : hasPreviousOrder && web3 && isSupportedOceanNetwork
? `You bought this ${assetType} already allowing you to use it without paying again.` ? `You bought this ${assetType} already allowing you to use it without paying again.`
: hasDatatoken : hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this dataset by spending 1 ${dtSymbol}, but without paying ${btSymbol} again.` ? `You own ${dtBalance} ${dtSymbol} allowing you to use this dataset by spending 1 ${dtSymbol}, but without paying ${btSymbol} again.`
@ -57,6 +61,35 @@ function getConsumeHelpText(
return text return text
} }
function getAlgoHelpText(
dtSymbolSelectedComputeAsset: string,
dtBalanceSelectedComputeAsset: string,
isConsumable: boolean,
isAlgorithmConsumable: boolean,
hasPreviousOrderSelectedComputeAsset: boolean,
selectedComputeAssetType: string,
hasDatatokenSelectedComputeAsset: boolean,
isBalanceSufficient: boolean,
isSupportedOceanNetwork: boolean,
web3: Web3
) {
const text =
(!dtSymbolSelectedComputeAsset && !dtBalanceSelectedComputeAsset) ||
isConsumable === false ||
isAlgorithmConsumable === false
? ''
: hasPreviousOrderSelectedComputeAsset && web3 && isSupportedOceanNetwork
? `You already bought the selected ${selectedComputeAssetType}, allowing you to use it without paying again.`
: hasDatatokenSelectedComputeAsset
? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying OCEAN again.`
: web3 && !isSupportedOceanNetwork
? `Connect to the correct network to interact with this asset.`
: isBalanceSufficient === false
? ''
: `Additionally, you will buy 1 ${dtSymbolSelectedComputeAsset} for the ${selectedComputeAssetType} and spend it back to its publisher and pool.`
return text
}
function getComputeAssetHelpText( function getComputeAssetHelpText(
hasPreviousOrder: boolean, hasPreviousOrder: boolean,
hasDatatoken: boolean, hasDatatoken: boolean,
@ -73,6 +106,8 @@ function getComputeAssetHelpText(
dtBalanceSelectedComputeAsset?: string, dtBalanceSelectedComputeAsset?: string,
selectedComputeAssetType?: string, selectedComputeAssetType?: string,
isAlgorithmConsumable?: boolean, isAlgorithmConsumable?: boolean,
isSupportedOceanNetwork?: boolean,
web3?: Web3,
hasProviderFee?: boolean hasProviderFee?: boolean
) { ) {
const computeAssetHelpText = getConsumeHelpText( const computeAssetHelpText = getConsumeHelpText(
@ -84,21 +119,24 @@ function getComputeAssetHelpText(
assetType, assetType,
isConsumable, isConsumable,
isBalanceSufficient, isBalanceSufficient,
consumableFeedback consumableFeedback,
isSupportedOceanNetwork,
web3
)
const computeAlgoHelpText = getAlgoHelpText(
dtSymbolSelectedComputeAsset,
dtBalanceSelectedComputeAsset,
isConsumable,
isAlgorithmConsumable,
hasPreviousOrderSelectedComputeAsset,
selectedComputeAssetType,
hasDatatokenSelectedComputeAsset,
isBalanceSufficient,
isSupportedOceanNetwork,
web3
) )
const computeAlgoHelpText =
(!dtSymbolSelectedComputeAsset && !dtBalanceSelectedComputeAsset) ||
isConsumable === false ||
isAlgorithmConsumable === false
? ''
: hasPreviousOrderSelectedComputeAsset
? `You already bought the selected ${selectedComputeAssetType}, allowing you to use it without paying again.`
: hasDatatokenSelectedComputeAsset
? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying ${btSymbol} again.`
: isBalanceSufficient === false
? ''
: `Additionally, you will buy 1 ${dtSymbolSelectedComputeAsset} for the ${selectedComputeAssetType} and spend it back to its publisher.`
const providerFeeHelpText = hasProviderFee const providerFeeHelpText = hasProviderFee
? 'In order to start the job you also need to pay the fees for renting the c2d resources.' ? '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.' : 'C2D resources required to start the job are available, no payment required for those fees.'
@ -131,22 +169,26 @@ export default function ButtonBuy({
priceType, priceType,
algorithmPriceType, algorithmPriceType,
isAlgorithmConsumable, isAlgorithmConsumable,
hasProviderFee hasProviderFee,
retry,
isSupportedOceanNetwork
}: ButtonBuyProps): ReactElement { }: ButtonBuyProps): ReactElement {
const buttonText = const { web3 } = useWeb3()
action === 'download' const buttonText = retry
? hasPreviousOrder ? 'Retry'
? 'Download' : action === 'download'
: priceType === 'free' ? hasPreviousOrder
? 'Get' ? 'Download'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}` : priceType === 'free'
: hasPreviousOrder && ? 'Get'
hasPreviousOrderSelectedComputeAsset && : `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
!hasProviderFee : hasPreviousOrder &&
? 'Start Compute Job' hasPreviousOrderSelectedComputeAsset &&
: priceType === 'free' && algorithmPriceType === 'free' !hasProviderFee
? 'Order Compute Job' ? 'Start Compute Job'
: `Buy Compute Job` : priceType === 'free' && algorithmPriceType === 'free'
? 'Order Compute Job'
: `Buy Compute Job`
return ( return (
<div className={styles.actions}> <div className={styles.actions}>
@ -174,7 +216,9 @@ export default function ButtonBuy({
assetType, assetType,
isConsumable, isConsumable,
isBalanceSufficient, isBalanceSufficient,
consumableFeedback consumableFeedback,
isSupportedOceanNetwork,
web3
) )
: getComputeAssetHelpText( : getComputeAssetHelpText(
hasPreviousOrder, hasPreviousOrder,
@ -192,6 +236,8 @@ export default function ButtonBuy({
dtBalanceSelectedComputeAsset, dtBalanceSelectedComputeAsset,
selectedComputeAssetType, selectedComputeAssetType,
isAlgorithmConsumable, isAlgorithmConsumable,
isSupportedOceanNetwork,
web3,
hasProviderFee hasProviderFee
)} )}
</div> </div>

View File

@ -9,7 +9,7 @@ import PriceOutput from './PriceOutput'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import content from '../../../../../content/pages/startComputeDataset.json' import content from '../../../../../content/pages/startComputeDataset.json'
import { Asset } from '@oceanprotocol/lib' import { Asset, ZERO_ADDRESS } from '@oceanprotocol/lib'
import { getAccessDetails } from '@utils/accessDetailsAndPricing' import { getAccessDetails } from '@utils/accessDetailsAndPricing'
import { useMarketMetadata } from '@context/MarketMetadata' import { useMarketMetadata } from '@context/MarketMetadata'
import Alert from '@shared/atoms/Alert' import Alert from '@shared/atoms/Alert'
@ -43,7 +43,8 @@ export default function FormStartCompute({
datasetOrderPriceAndFees, datasetOrderPriceAndFees,
algoOrderPriceAndFees, algoOrderPriceAndFees,
providerFeeAmount, providerFeeAmount,
validUntil validUntil,
retry
}: { }: {
algorithms: AssetSelectionAsset[] algorithms: AssetSelectionAsset[]
ddoListAlgorithms: Asset[] ddoListAlgorithms: Asset[]
@ -71,9 +72,10 @@ export default function FormStartCompute({
algoOrderPriceAndFees?: OrderPriceAndFees algoOrderPriceAndFees?: OrderPriceAndFees
providerFeeAmount?: string providerFeeAmount?: string
validUntil?: string validUntil?: string
retry: boolean
}): ReactElement { }): ReactElement {
const { siteContent } = useMarketMetadata() const { siteContent } = useMarketMetadata()
const { accountId, balance } = useWeb3() const { accountId, balance, isSupportedOceanNetwork } = useWeb3()
const { isValid, values }: FormikContextType<{ algorithm: string }> = const { isValid, values }: FormikContextType<{ algorithm: string }> =
useFormikContext() useFormikContext()
const { asset, isAssetNetwork } = useAsset() const { asset, isAssetNetwork } = useAsset()
@ -96,7 +98,7 @@ export default function FormStartCompute({
} }
useEffect(() => { useEffect(() => {
if (!values.algorithm || !accountId || !isConsumable) return if (!values.algorithm || !isConsumable) return
async function fetchAlgorithmAssetExtended() { async function fetchAlgorithmAssetExtended() {
const algorithmAsset = getAlgorithmAsset(values.algorithm) const algorithmAsset = getAlgorithmAsset(values.algorithm)
@ -104,7 +106,7 @@ export default function FormStartCompute({
algorithmAsset.chainId, algorithmAsset.chainId,
algorithmAsset.services[0].datatokenAddress, algorithmAsset.services[0].datatokenAddress,
algorithmAsset.services[0].timeout, algorithmAsset.services[0].timeout,
accountId accountId || ZERO_ADDRESS // if user is not connected, use ZERO_ADDRESS as accountId
) )
const extendedAlgoAsset: AssetExtended = { const extendedAlgoAsset: AssetExtended = {
...algorithmAsset, ...algorithmAsset,
@ -196,15 +198,20 @@ export default function FormStartCompute({
} }
setTotalPrices(totalPrices) setTotalPrices(totalPrices)
}, [ }, [
asset?.accessDetails, asset,
selectedAlgorithmAsset?.accessDetails,
hasPreviousOrder, hasPreviousOrder,
hasDatatoken, hasDatatoken,
hasPreviousOrderSelectedComputeAsset, hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset, hasDatatokenSelectedComputeAsset,
datasetOrderPriceAndFees, datasetOrderPriceAndFees,
algoOrderPriceAndFees, algoOrderPriceAndFees,
providerFeeAmount providerFeeAmount,
isAssetNetwork,
selectedAlgorithmAsset?.accessDetails,
datasetOrderPrice,
algoOrderPrice,
algorithmSymbol,
datasetSymbol
]) ])
useEffect(() => { useEffect(() => {
@ -214,12 +221,13 @@ export default function FormStartCompute({
setIsBalanceSufficient(false) setIsBalanceSufficient(false)
return return
} }
// if one comparison of baseTokenBalance and token price comparison is false then the state will be false // if one comparison of baseTokenBalance and token price comparison is false then the state will be false
setIsBalanceSufficient( setIsBalanceSufficient(
isBalanceSufficient && compareAsBN(baseTokenBalance, `${price.value}`) baseTokenBalance && compareAsBN(baseTokenBalance, `${price.value}`)
) )
}) })
}, [balance, dtBalance, datasetSymbol, algorithmSymbol]) }, [balance, dtBalance, datasetSymbol, algorithmSymbol, totalPrices])
return ( return (
<Form className={styles.form}> <Form className={styles.form}>
@ -293,7 +301,9 @@ export default function FormStartCompute({
isAlgorithmConsumable={ isAlgorithmConsumable={
selectedAlgorithmAsset?.accessDetails?.isPurchasable selectedAlgorithmAsset?.accessDetails?.isPurchasable
} }
isSupportedOceanNetwork={isSupportedOceanNetwork}
hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'} hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'}
retry={retry}
/> />
</Form> </Form>
) )

View File

@ -5,6 +5,7 @@ import Tooltip from '@shared/atoms/Tooltip'
import styles from './PriceOutput.module.css' import styles from './PriceOutput.module.css'
import { MAX_DECIMALS } from '@utils/constants' import { MAX_DECIMALS } from '@utils/constants'
import Decimal from 'decimal.js' import Decimal from 'decimal.js'
import { useWeb3 } from '@context/Web3'
interface PriceOutputProps { interface PriceOutputProps {
hasPreviousOrder: boolean hasPreviousOrder: boolean
@ -40,13 +41,21 @@ function Row({
sign?: string sign?: string
type?: string type?: string
}) { }) {
const { isSupportedOceanNetwork } = useWeb3()
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 className={styles.type}>{type}</div>
<div> <div>
<PriceUnit <PriceUnit
price={hasPreviousOrder || hasDatatoken ? 0 : Number(price)} price={
!isSupportedOceanNetwork
? hasPreviousOrder || hasDatatoken
? 0
: Number(price)
: Number(price)
}
symbol={symbol} symbol={symbol}
size="small" size="small"
className={styles.price} className={styles.price}

View File

@ -45,6 +45,7 @@ import { getComputeFeedback } from '@utils/feedback'
import { getDummyWeb3 } from '@utils/web3' import { getDummyWeb3 } from '@utils/web3'
import { initializeProviderForCompute } from '@utils/provider' import { initializeProviderForCompute } from '@utils/provider'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useAsset } from '@context/Asset'
const refreshInterval = 10000 // 10 sec. const refreshInterval = 10000 // 10 sec.
export default function Compute({ export default function Compute({
@ -60,8 +61,10 @@ export default function Compute({
fileIsLoading?: boolean fileIsLoading?: boolean
consumableFeedback?: string consumableFeedback?: string
}): ReactElement { }): ReactElement {
const { accountId, web3 } = useWeb3() const { accountId, web3, isSupportedOceanNetwork } = useWeb3()
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { isAssetNetwork } = useAsset()
const newAbortController = useAbortController() const newAbortController = useAbortController()
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
@ -97,6 +100,7 @@ export default function Compute({
const [refetchJobs, setRefetchJobs] = useState(false) const [refetchJobs, setRefetchJobs] = useState(false)
const [isLoadingJobs, setIsLoadingJobs] = useState(false) const [isLoadingJobs, setIsLoadingJobs] = useState(false)
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([]) const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const [retry, setRetry] = useState<boolean>(false)
const hasDatatoken = Number(dtBalance) >= 1 const hasDatatoken = Number(dtBalance) >= 1
const isComputeButtonDisabled = const isComputeButtonDisabled =
@ -115,7 +119,7 @@ export default function Compute({
const datatokenInstance = new Datatoken(web3) const datatokenInstance = new Datatoken(web3)
const dtBalance = await datatokenInstance.balance( const dtBalance = await datatokenInstance.balance(
asset?.services[0].datatokenAddress, asset?.services[0].datatokenAddress,
accountId accountId || ZERO_ADDRESS // if the user is not connected, we use ZERO_ADDRESS as accountId
) )
setAlgorithmDTBalance(new Decimal(dtBalance).toString()) setAlgorithmDTBalance(new Decimal(dtBalance).toString())
const hasAlgoDt = Number(dtBalance) >= 1 const hasAlgoDt = Number(dtBalance) >= 1
@ -133,9 +137,10 @@ export default function Compute({
const initializedProvider = await initializeProviderForCompute( const initializedProvider = await initializeProviderForCompute(
asset, asset,
selectedAlgorithmAsset, selectedAlgorithmAsset,
accountId, accountId || ZERO_ADDRESS, // if the user is not connected, we use ZERO_ADDRESS as accountId
computeEnv computeEnv
) )
if ( if (
!initializedProvider || !initializedProvider ||
!initializedProvider?.datasets || !initializedProvider?.datasets ||
@ -144,13 +149,17 @@ export default function Compute({
throw new Error(`Error initializing provider for the compute job!`) throw new Error(`Error initializing provider for the compute job!`)
setInitializedProviderResponse(initializedProvider) setInitializedProviderResponse(initializedProvider)
setProviderFeeAmount(
await unitsToAmount( const feeAmount = await unitsToAmount(
web3, !isSupportedOceanNetwork || !isAssetNetwork
initializedProvider?.datasets?.[0]?.providerFee?.providerFeeToken, ? await getDummyWeb3(asset?.chainId)
initializedProvider?.datasets?.[0]?.providerFee?.providerFeeAmount : web3,
) initializedProvider?.datasets?.[0]?.providerFee?.providerFeeToken,
initializedProvider?.datasets?.[0]?.providerFee?.providerFeeAmount
) )
setProviderFeeAmount(feeAmount)
const computeDuration = ( const computeDuration = (
parseInt(initializedProvider?.datasets?.[0]?.providerFee?.validUntil) - parseInt(initializedProvider?.datasets?.[0]?.providerFee?.validUntil) -
Math.floor(Date.now() / 1000) Math.floor(Date.now() / 1000)
@ -216,7 +225,7 @@ export default function Compute({
}, [asset?.accessDetails, accountId, isUnsupportedPricing]) }, [asset?.accessDetails, accountId, isUnsupportedPricing])
useEffect(() => { useEffect(() => {
if (!selectedAlgorithmAsset?.accessDetails || !accountId) return if (!selectedAlgorithmAsset?.accessDetails) return
setIsRequestingAlgoOrderPrice(true) setIsRequestingAlgoOrderPrice(true)
setIsConsumableAlgorithmPrice( setIsConsumableAlgorithmPrice(
@ -291,7 +300,8 @@ export default function Compute({
useEffect(() => { useEffect(() => {
const newError = error const newError = error
if (!newError) return if (!newError) return
toast.error(newError) const errorMsg = newError + '. Please retry.'
toast.error(errorMsg)
}, [error]) }, [error])
async function startJob(): Promise<void> { async function startJob(): Promise<void> {
@ -304,6 +314,7 @@ export default function Compute({
documentId: selectedAlgorithmAsset.id, documentId: selectedAlgorithmAsset.id,
serviceId: selectedAlgorithmAsset.services[0].id serviceId: selectedAlgorithmAsset.services[0].id
} }
const allowed = await isOrderable( const allowed = await isOrderable(
asset, asset,
computeService.id, computeService.id,
@ -386,6 +397,7 @@ export default function Compute({
initPriceAndFees() initPriceAndFees()
} catch (error) { } catch (error) {
setError(error.message) setError(error.message)
setRetry(true)
LoggerInstance.error(`[compute] ${error.message} `) LoggerInstance.error(`[compute] ${error.message} `)
} finally { } finally {
setIsOrdering(false) setIsOrdering(false)
@ -447,14 +459,12 @@ export default function Compute({
setSelectedAlgorithm={setSelectedAlgorithmAsset} setSelectedAlgorithm={setSelectedAlgorithmAsset}
isLoading={isOrdering || isRequestingAlgoOrderPrice} isLoading={isOrdering || isRequestingAlgoOrderPrice}
isComputeButtonDisabled={isComputeButtonDisabled} isComputeButtonDisabled={isComputeButtonDisabled}
hasPreviousOrder={validOrderTx !== undefined} hasPreviousOrder={!!validOrderTx}
hasDatatoken={hasDatatoken} hasDatatoken={hasDatatoken}
dtBalance={dtBalance} dtBalance={dtBalance}
assetType={asset?.metadata.type} assetType={asset?.metadata.type}
assetTimeout={secondsToString(asset?.services[0].timeout)} assetTimeout={secondsToString(asset?.services[0].timeout)}
hasPreviousOrderSelectedComputeAsset={ hasPreviousOrderSelectedComputeAsset={!!validAlgorithmOrderTx}
validAlgorithmOrderTx !== undefined
}
hasDatatokenSelectedComputeAsset={hasAlgoAssetDatatoken} hasDatatokenSelectedComputeAsset={hasAlgoAssetDatatoken}
datasetSymbol={asset?.accessDetails?.baseToken?.symbol || 'OCEAN'} datasetSymbol={asset?.accessDetails?.baseToken?.symbol || 'OCEAN'}
algorithmSymbol={ algorithmSymbol={
@ -477,6 +487,7 @@ export default function Compute({
algoOrderPriceAndFees={algoOrderPriceAndFees} algoOrderPriceAndFees={algoOrderPriceAndFees}
providerFeeAmount={providerFeeAmount} providerFeeAmount={providerFeeAmount}
validUntil={computeValidUntil} validUntil={computeValidUntil}
retry={retry}
/> />
</Formik> </Formik>
)} )}

View File

@ -33,7 +33,7 @@ export default function Download({
fileIsLoading?: boolean fileIsLoading?: boolean
consumableFeedback?: string consumableFeedback?: string
}): ReactElement { }): ReactElement {
const { accountId, web3 } = useWeb3() const { accountId, web3, isSupportedOceanNetwork } = useWeb3()
const { getOpcFeeForToken } = useMarketMetadata() const { getOpcFeeForToken } = useMarketMetadata()
const { isInPurgatory, isAssetNetwork } = useAsset() const { isInPurgatory, isAssetNetwork } = useAsset()
const isMounted = useIsMounted() const isMounted = useIsMounted()
@ -48,6 +48,7 @@ export default function Download({
const [isOrderDisabled, setIsOrderDisabled] = useState(false) const [isOrderDisabled, setIsOrderDisabled] = useState(false)
const [orderPriceAndFees, setOrderPriceAndFees] = const [orderPriceAndFees, setOrderPriceAndFees] =
useState<OrderPriceAndFees>() useState<OrderPriceAndFees>()
const [retry, setRetry] = useState<boolean>(false)
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED' const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
@ -155,9 +156,10 @@ export default function Download({
} }
} catch (error) { } catch (error) {
LoggerInstance.error(error) LoggerInstance.error(error)
setRetry(true)
const message = isOwned const message = isOwned
? 'Failed to download file!' ? 'Failed to download file!'
: 'An error occurred. Check console for more information.' : 'An error occurred, please retry. Check console for more information.'
toast.error(message) toast.error(message)
} }
setIsLoading(false) setIsLoading(false)
@ -181,6 +183,8 @@ export default function Download({
isConsumable={asset.accessDetails?.isPurchasable} isConsumable={asset.accessDetails?.isPurchasable}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
consumableFeedback={consumableFeedback} consumableFeedback={consumableFeedback}
retry={retry}
isSupportedOceanNetwork={isSupportedOceanNetwork}
/> />
) )

View File

@ -7,7 +7,7 @@ import { compareAsBN } from '@utils/numbers'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Web3Feedback from '@shared/Web3Feedback' import Web3Feedback from '@shared/Web3Feedback'
import { getFileDidInfo, getFileUrlInfo } from '@utils/provider' import { getFileDidInfo, getFileInfo } from '@utils/provider'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
@ -52,14 +52,20 @@ export default function AssetActions({
formikState?.values?.services[0].providerUrl.url || formikState?.values?.services[0].providerUrl.url ||
asset?.services[0]?.serviceEndpoint asset?.services[0]?.serviceEndpoint
const storageType = formikState?.values?.services
? formikState?.values?.services[0].files[0].type
: null
try { try {
const fileInfoResponse = formikState?.values?.services?.[0].files?.[0] const fileInfoResponse = formikState?.values?.services?.[0].files?.[0]
.url .url
? await getFileUrlInfo( ? await getFileInfo(
formikState?.values?.services?.[0].files?.[0].url, formikState?.values?.services?.[0].files?.[0].url,
providerUrl providerUrl,
storageType
) )
: await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl) : await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl)
fileInfoResponse && setFileMetadata(fileInfoResponse[0]) fileInfoResponse && setFileMetadata(fileInfoResponse[0])
// set the content type in the Dataset Schema // set the content type in the Dataset Schema

View File

@ -1,12 +1,29 @@
import React, { ReactElement } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import MetaItem from './MetaItem' import MetaItem from './MetaItem'
import styles from './MetaFull.module.css' import styles from './MetaFull.module.css'
import Publisher from '@shared/Publisher' import Publisher from '@shared/Publisher'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { Asset } from '@oceanprotocol/lib' import { useWeb3 } from '@context/Web3'
import { Asset, Datatoken, LoggerInstance } from '@oceanprotocol/lib'
export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement { export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement {
const [paymentCollector, setPaymentCollector] = useState<string>()
const { isInPurgatory } = useAsset() const { isInPurgatory } = useAsset()
const { web3 } = useWeb3()
useEffect(() => {
async function getInitialPaymentCollector() {
try {
const datatoken = new Datatoken(web3)
setPaymentCollector(
await datatoken.getPaymentCollector(ddo.datatokens[0].address)
)
} catch (error) {
LoggerInstance.error('[MetaFull: getInitialPaymentCollector]', error)
}
}
getInitialPaymentCollector()
}, [ddo, web3])
function DockerImage() { function DockerImage() {
const containerInfo = ddo?.metadata?.algorithm?.container const containerInfo = ddo?.metadata?.algorithm?.container
@ -23,6 +40,12 @@ export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement {
title="Owner" title="Owner"
content={<Publisher account={ddo?.nft?.owner} />} content={<Publisher account={ddo?.nft?.owner} />}
/> />
{paymentCollector && paymentCollector !== ddo?.nft?.owner && (
<MetaItem
title="Revenue Sent To"
content={<Publisher account={paymentCollector} />}
/>
)}
{ddo?.metadata?.type === 'algorithm' && ddo?.metadata?.algorithm && ( {ddo?.metadata?.type === 'algorithm' && ddo?.metadata?.algorithm && (
<MetaItem title="Docker Image" content={<DockerImage />} /> <MetaItem title="Docker Image" content={<DockerImage />} />

View File

@ -7,7 +7,7 @@
.wrapper img { .wrapper img {
margin: 0; margin: 0;
width: 128px; width: 128px;
height: 128px; height: auto;
} }
.info { .info {

View File

@ -14,11 +14,13 @@ const openSeaTestNetworks = [4]
export default function NftTooltip({ export default function NftTooltip({
nft, nft,
nftImage,
address, address,
chainId, chainId,
isBlockscoutExplorer isBlockscoutExplorer
}: { }: {
nft: NftMetadata nft: NftMetadata
nftImage: string
address: string address: string
chainId: number chainId: number
isBlockscoutExplorer: boolean isBlockscoutExplorer: boolean
@ -39,7 +41,7 @@ export default function NftTooltip({
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{nft && <img src={nft.image_data || nft.image} alt={nft?.name} />} {nftImage && <img src={nftImage} alt={nft?.name} />}
<div className={styles.info}> <div className={styles.info}>
{nft && <h5>{nft.name}</h5>} {nft && <h5>{nft.name}</h5>}
{address && ( {address && (

View File

@ -4,14 +4,19 @@
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
width: calc(var(--spacer) * 2); width: calc(var(--spacer) * 2);
height: calc(var(--spacer) * 2); height: calc(var(--spacer) * 2);
display: flex;
align-items: center;
} }
.nftImage img,
.nftImage > svg:first-of-type { .nftImage > svg:first-of-type {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.nftImage img {
height: auto;
}
.nftImage > svg:first-of-type { .nftImage > svg:first-of-type {
transform: scale(0.7); transform: scale(0.7);
} }

View File

@ -48,6 +48,7 @@ export default function Nft({
content={ content={
<NftTooltip <NftTooltip
nft={nftMetadata} nft={nftMetadata}
nftImage={nftImage}
address={asset?.nftAddress} address={asset?.nftAddress}
chainId={asset?.chainId} chainId={asset?.chainId}
isBlockscoutExplorer={isBlockscoutExplorer} isBlockscoutExplorer={isBlockscoutExplorer}

View File

@ -1,11 +1,12 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import { Formik } from 'formik' import { Formik } from 'formik'
import { import {
LoggerInstance, LoggerInstance,
Metadata, Metadata,
FixedRateExchange, FixedRateExchange,
Asset, Asset,
Service Service,
Datatoken
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { validationSchema } from './_validation' import { validationSchema } from './_validation'
import { getInitialValues } from './_constants' import { getInitialValues } from './_constants'
@ -36,10 +37,28 @@ export default function Edit({
const { accountId, web3 } = useWeb3() const { accountId, web3 } = useWeb3()
const newAbortController = useAbortController() const newAbortController = useAbortController()
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const [paymentCollector, setPaymentCollector] = useState<string>()
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const isComputeType = asset?.services[0]?.type === 'compute' const isComputeType = asset?.services[0]?.type === 'compute'
const hasFeedback = error || success const hasFeedback = error || success
useEffect(() => {
async function getInitialPaymentCollector() {
try {
const datatoken = new Datatoken(web3)
setPaymentCollector(
await datatoken.getPaymentCollector(asset?.datatokens[0].address)
)
} catch (error) {
LoggerInstance.error(
'[EditMetadata: getInitialPaymentCollector]',
error
)
}
}
getInitialPaymentCollector()
}, [asset, web3])
async function updateFixedPrice(newPrice: string) { async function updateFixedPrice(newPrice: string) {
const config = getOceanConfig(asset.chainId) const config = getOceanConfig(asset.chainId)
@ -81,13 +100,22 @@ export default function Edit({
values.price !== asset.accessDetails.price && values.price !== asset.accessDetails.price &&
(await updateFixedPrice(values.price)) (await updateFixedPrice(values.price))
if (values.paymentCollector !== paymentCollector) {
const datatoken = new Datatoken(web3)
await datatoken.setPaymentCollector(
asset?.datatokens[0].address,
accountId,
values.paymentCollector
)
}
if (values.files[0]?.url) { if (values.files[0]?.url) {
const file = { const file = {
nftAddress: asset.nftAddress, nftAddress: asset.nftAddress,
datatokenAddress: asset.services[0].datatokenAddress, datatokenAddress: asset.services[0].datatokenAddress,
files: [ files: [
{ {
type: 'url', type: values.files[0].type,
index: 0, index: 0,
url: values.files[0].url, url: values.files[0].url,
method: 'GET' method: 'GET'
@ -147,7 +175,8 @@ export default function Edit({
initialValues={getInitialValues( initialValues={getInitialValues(
asset?.metadata, asset?.metadata,
asset?.services[0]?.timeout, asset?.services[0]?.timeout,
asset?.accessDetails?.price asset?.accessDetails?.price,
paymentCollector
)} )}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {

View File

@ -0,0 +1,15 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import { useFormikContext } from 'formik'
import FormActions from './FormActions'
jest.mock('formik')
describe('src/components/Asset/Edit/FormActions.tsx', () => {
it('renders fixed price', () => {
const isValid = true
;(useFormikContext as jest.Mock).mockReturnValue([isValid])
render(<FormActions />)
expect(screen.getByText('Submit')).toBeInTheDocument()
})
})

View File

@ -1,10 +1,11 @@
import React, { ReactElement, useEffect } from 'react' import React, { ReactElement, useEffect } from 'react'
import { Field, Form, useField, useFormikContext } from 'formik' import { Field, Form, useFormikContext } from 'formik'
import Input, { InputProps } from '@shared/FormInput' import Input from '@shared/FormInput'
import FormActions from './FormActions' import FormActions from './FormActions'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { FormPublishData } from 'src/components/Publish/_types' import { FormPublishData } from 'src/components/Publish/_types'
import { getFileUrlInfo } from '@utils/provider' import { getFileInfo } from '@utils/provider'
import { getFieldContent } from '@utils/form'
export function checkIfTimeoutInPredefinedValues( export function checkIfTimeoutInPredefinedValues(
timeout: string, timeout: string,
@ -21,11 +22,11 @@ export default function FormEditMetadata({
showPrice, showPrice,
isComputeDataset isComputeDataset
}: { }: {
data: InputProps[] data: FormFieldContent[]
showPrice: boolean showPrice: boolean
isComputeDataset: boolean isComputeDataset: boolean
}): ReactElement { }): ReactElement {
const { oceanConfig, asset } = useAsset() const { asset } = useAsset()
const { values, setFieldValue } = useFormikContext<FormPublishData>() const { values, setFieldValue } = useFormikContext<FormPublishData>()
// This component is handled by Formik so it's not rendered like a "normal" react component, // This component is handled by Formik so it's not rendered like a "normal" react component,
@ -46,25 +47,38 @@ export default function FormEditMetadata({
useEffect(() => { useEffect(() => {
// let's initiate files with empty url (we can't access the asset url) with type hidden (for UI frontend) // let's initiate files with empty url (we can't access the asset url) with type hidden (for UI frontend)
setFieldValue('files', [ setTimeout(() => {
{ setFieldValue('files', [
url: '', {
type: 'hidden' url: '',
} type: 'hidden'
]) }
])
}, 500)
const providerUrl = values?.services const providerUrl = values?.services
? values?.services[0].providerUrl.url ? values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint : asset.services[0].serviceEndpoint
// if we have a sample file, we need to get the files' info before setting defaults links value // if we have a sample file, we need to get the files' info before setting defaults links value
asset?.metadata?.links?.[0] && asset?.metadata?.links?.[0] &&
getFileUrlInfo(asset.metadata.links[0], providerUrl).then( getFileInfo(asset.metadata.links[0], providerUrl, 'url').then(
(checkedFile) => { (checkedFile) => {
console.log(checkedFile) // set valid false if url is using google drive
if (asset.metadata.links[0].includes('drive.google')) {
setFieldValue('links', [
{
url: asset.metadata.links[0],
valid: false
}
])
return
}
// initiate link with values from asset metadata // initiate link with values from asset metadata
setFieldValue('links', [ setFieldValue('links', [
{ {
url: asset.metadata.links[0], url: asset.metadata.links[0],
type: 'url',
...checkedFile[0] ...checkedFile[0]
} }
]) ])
@ -74,23 +88,48 @@ export default function FormEditMetadata({
return ( return (
<Form> <Form>
{data.map( <Field {...getFieldContent('name', data)} component={Input} name="name" />
(field: InputProps) =>
(!showPrice && field.name === 'price') || ( <Field
<Field {...getFieldContent('description', data)}
key={field.name} component={Input}
options={ name="description"
field.name === 'timeout' && isComputeDataset === true />
? timeoutOptionsArray
: field.options {showPrice && (
} <Field
{...field} {...getFieldContent('price', data)}
component={Input} component={Input}
prefix={field.name === 'price' && oceanConfig?.oceanTokenSymbol} name="price"
/> />
)
)} )}
<Field
{...getFieldContent('files', data)}
component={Input}
name="files"
/>
<Field
{...getFieldContent('links', data)}
component={Input}
name="links"
/>
<Field
{...getFieldContent('timeout', data)}
component={Input}
name="timeout"
/>
<Field
{...getFieldContent('author', data)}
component={Input}
name="author"
/>
<Field {...getFieldContent('tags', data)} component={Input} name="tags" />
<FormActions /> <FormActions />
</Form> </Form>
) )

View File

@ -5,17 +5,19 @@ import { ComputeEditForm, MetadataEditForm } from './_types'
export function getInitialValues( export function getInitialValues(
metadata: Metadata, metadata: Metadata,
timeout: number, timeout: number,
price: string price: string,
paymentCollector: string
): Partial<MetadataEditForm> { ): Partial<MetadataEditForm> {
return { return {
name: metadata?.name, name: metadata?.name,
description: metadata?.description, description: metadata?.description,
price, price,
links: [{ url: '', type: '' }], links: [{ url: '', type: 'url' }],
files: [{ url: '', type: '' }], files: [{ url: '', type: 'ipfs' }],
timeout: secondsToString(timeout), timeout: secondsToString(timeout),
author: metadata?.author, author: metadata?.author,
tags: metadata?.tags tags: metadata?.tags,
paymentCollector
} }
} }

View File

@ -3,6 +3,7 @@ export interface MetadataEditForm {
name: string name: string
description: string description: string
timeout: string timeout: string
paymentCollector: string
price?: string price?: string
files: FileInfo[] files: FileInfo[]
links?: FileInfo[] links?: FileInfo[]

View File

@ -1,5 +1,7 @@
import { FileInfo } from '@oceanprotocol/lib' import { FileInfo } from '@oceanprotocol/lib'
import * as Yup from 'yup' import * as Yup from 'yup'
import web3 from 'web3'
import { testLinks } from '../../../@utils/yup'
export const validationSchema = Yup.object().shape({ export const validationSchema = Yup.object().shape({
name: Yup.string() name: Yup.string()
@ -10,38 +12,38 @@ export const validationSchema = Yup.object().shape({
files: Yup.array<FileInfo[]>() files: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string() url: testLinks(true),
.url('Must be a valid URL.') valid: Yup.boolean().test((value, context) => {
.test( const { type } = context.parent
'GoogleNotSupported', // allow user to submit if the value type is hidden
'Google Drive is not a supported hosting service. Please use an alternative.', if (type === 'hidden') return true
(value) => { return value || false
return !value?.toString().includes('drive.google') })
}
),
valid: Yup.boolean().isTrue()
}) })
) )
.nullable(), .nullable(),
links: Yup.array<FileInfo[]>() links: Yup.array<FileInfo[]>().of(
.of( Yup.object().shape({
Yup.object().shape({ url: testLinks(true),
url: Yup.string() valid: Yup.boolean().test((value, context) => {
.url('Must be a valid URL.') // allow user to submit if the value is null
.test( const { valid, url } = context.parent
'GoogleNotSupported', // allow user to continue if the url is empty
'Google Drive is not a supported hosting service. Please use an alternative.', if (!url) return true
(value) => { return valid
return !value?.toString().includes('drive.google')
}
),
valid: Yup.boolean().isTrue()
}) })
) })
.nullable(), ),
timeout: Yup.string().required('Required'), timeout: Yup.string().required('Required'),
author: Yup.string().nullable(), author: Yup.string().nullable(),
tags: Yup.array<string[]>().nullable() tags: Yup.array<string[]>().nullable(),
paymentCollector: Yup.string().test(
'ValidAddress',
'Must be a valid Ethereum Address.',
(value) => {
return web3.utils.isAddress(value)
}
)
}) })
export const computeSettingsValidationSchema = Yup.object().shape({ export const computeSettingsValidationSchema = Yup.object().shape({

View File

@ -23,7 +23,6 @@ export default function RelatedAssets(): ReactElement {
!asset?.nft || !asset?.nft ||
!asset?.metadata !asset?.metadata
) { ) {
setIsLoading(false)
return return
} }
@ -31,11 +30,17 @@ export default function RelatedAssets(): ReactElement {
setIsLoading(true) setIsLoading(true)
try { try {
const tagQuery = generateBaseQuery( let tagResults: Asset[] = []
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
) // safeguard against faults in the metadata
const tagResults = (await queryMetadata(tagQuery, newCancelToken())) if (asset.metadata.tags instanceof Array) {
?.results const tagQuery = generateBaseQuery(
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
)
tagResults = (await queryMetadata(tagQuery, newCancelToken()))
?.results
}
if (tagResults.length === 4) { if (tagResults.length === 4) {
setRelatedAssets(tagResults) setRelatedAssets(tagResults)

View File

@ -124,8 +124,19 @@ export default function MarketStats(): ReactElement {
/> />
</div> </div>
<div> <div>
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.{' '} <PriceUnit
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '} decimals="0"
price={total.veLocked}
symbol="OCEAN"
size="small"
/>{' '}
locked.{' '}
<PriceUnit
decimals="0"
price={total.veAllocated}
symbol="veOCEAN"
size="small"
/>{' '}
allocated. allocated.
</div> </div>
</div> </div>

View File

@ -0,0 +1,56 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import MostViews from '.'
import axios from 'axios'
import { queryMetadata } from '@utils/aquarius'
import { assetAquarius } from '../../../../.jest/__fixtures__/assetAquarius'
jest.mock('axios')
jest.mock('@utils/aquarius')
const axiosMock = axios as jest.Mocked<typeof axios>
const queryMetadataMock = queryMetadata as jest.Mock
const queryMetadataBaseReturn: PagedAssets = {
results: [assetAquarius],
page: 1,
totalPages: 1,
totalResults: 1,
aggregations: {}
}
describe('components/Home/MostViews', () => {
beforeEach(() => {
jest.resetAllMocks()
})
it('renders without crashing', async () => {
axiosMock.get.mockImplementation(() =>
Promise.resolve({
data: [{ count: 666, did: assetAquarius.id }]
})
)
queryMetadataMock.mockResolvedValue(queryMetadataBaseReturn)
render(<MostViews />)
await screen.findByText('666')
})
it('catches errors', async () => {
queryMetadataMock.mockImplementation(() => {
throw new Error('Hello error')
})
// prevent console error from showing up in test log
const originalError = console.error
console.error = jest.fn()
try {
render(<MostViews />)
await screen.findByText('No results found')
} catch (error) {
expect(error).toEqual({ message: 'Hello error' })
}
console.error = originalError
})
})

View File

@ -0,0 +1,73 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import styles from '../index.module.css'
import {
generateBaseQuery,
getFilterTerm,
queryMetadata
} from '@utils/aquarius'
import { useCancelToken } from '@hooks/useCancelToken'
import Tooltip from '@shared/atoms/Tooltip'
import AssetList from '@shared/AssetList'
import { LoggerInstance } from '@oceanprotocol/lib'
import { sortAssets } from '@utils/index'
import axios, { AxiosResponse } from 'axios'
export default function MostViews(): ReactElement {
const [loading, setLoading] = useState<boolean>()
const [mostViewed, setMostViewed] = useState<AssetExtended[]>([])
const newCancelToken = useCancelToken()
const getMostViewed = useCallback(async () => {
try {
setLoading(true)
const response: AxiosResponse<PageViews[]> = await axios.get(
'https://market-analytics.oceanprotocol.com/pages?limit=6',
{ cancelToken: newCancelToken() }
)
const dids = response?.data?.map((x: PageViews) => x.did)
const assetsWithViews: AssetExtended[] = []
const baseParams = {
esPaginationOptions: { size: 6 },
filters: [getFilterTerm('_id', dids)]
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const result = await queryMetadata(query, newCancelToken())
if (result?.totalResults > 0) {
const sortedAssets = sortAssets(result.results, dids)
const overflow = sortedAssets.length - 6
sortedAssets.splice(sortedAssets.length - overflow, overflow)
sortedAssets.forEach((asset) => {
assetsWithViews.push({
...asset,
views: response.data.filter((x) => x.did === asset.id)?.[0]?.count
})
})
setMostViewed(assetsWithViews)
}
} catch (error) {
LoggerInstance.error(error.message)
} finally {
setLoading(false)
}
}, [newCancelToken])
useEffect(() => {
getMostViewed()
}, [getMostViewed])
return (
<section className={styles.section}>
<h3>
Most Views <span>last 30 days</span>
<Tooltip content="Assets from all supported chains. Not affected by your selected networks." />
</h3>
<AssetList
assets={mostViewed}
showPagination={false}
isLoading={loading}
/>
</section>
)
}

View File

@ -1,29 +1,27 @@
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import AssetList from '@shared/AssetList' import AssetList from '@shared/AssetList'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import { queryMetadata } from '@utils/aquarius' import { queryMetadata } from '@utils/aquarius'
import { sortAssets } from '@utils/index'
import React, { ReactElement, useState, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
function sortElements(items: Asset[], sorted: string[]) {
items.sort(function (a, b) {
return sorted.indexOf(a.nftAddress) - sorted.indexOf(b.nftAddress)
})
return items
}
export default function SectionQueryResult({ export default function SectionQueryResult({
title, title,
query, query,
action, action,
queryData queryData,
tooltip
}: { }: {
title: ReactElement | string title: ReactElement | string
query: SearchQuery query: SearchQuery
action?: ReactElement action?: ReactElement
queryData?: string[] queryData?: string[]
tooltip?: string
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const [result, setResult] = useState<PagedAssets>() const [result, setResult] = useState<PagedAssets>()
@ -52,7 +50,7 @@ export default function SectionQueryResult({
const result = await queryMetadata(query, newCancelToken()) const result = await queryMetadata(query, newCancelToken())
if (!isMounted()) return if (!isMounted()) return
if (queryData && result?.totalResults > 0) { if (queryData && result?.totalResults > 0) {
const sortedAssets = sortElements(result.results, queryData) const sortedAssets = sortAssets(result.results, queryData)
const overflow = sortedAssets.length - 6 const overflow = sortedAssets.length - 6
sortedAssets.splice(sortedAssets.length - overflow, overflow) sortedAssets.splice(sortedAssets.length - overflow, overflow)
result.results = sortedAssets result.results = sortedAssets
@ -69,7 +67,9 @@ export default function SectionQueryResult({
return ( return (
<section className={styles.section}> <section className={styles.section}>
<h3>{title}</h3> <h3>
{title} {tooltip && <Tooltip content={<Markdown text={tooltip} />} />}
</h3>
<AssetList <AssetList
assets={result?.results} assets={result?.results}

View File

@ -13,6 +13,13 @@
color: var(--color-secondary); color: var(--color-secondary);
} }
.section h3 span {
font-size: var(--font-size-small);
color: var(--color-secondary);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
}
.section [class*='button'] { .section [class*='button'] {
margin-top: var(--spacer); margin-top: var(--spacer);
} }

View File

@ -9,12 +9,14 @@ import TopTags from './TopTags'
import SectionQueryResult from './SectionQueryResult' import SectionQueryResult from './SectionQueryResult'
import styles from './index.module.css' import styles from './index.module.css'
import Allocations from './Allocations' import Allocations from './Allocations'
import MostViews from './MostViews'
export default function HomePage(): ReactElement { export default function HomePage(): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const [queryLatest, setQueryLatest] = useState<SearchQuery>() const [queryLatest, setQueryLatest] = useState<SearchQuery>()
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>() const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>() const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
useEffect(() => { useEffect(() => {
@ -66,7 +68,7 @@ export default function HomePage(): ReactElement {
/> />
<SectionQueryResult title="Most Sales" query={queryMostSales} /> <SectionQueryResult title="Most Sales" query={queryMostSales} />
<MostViews />
<TopSales title="Publishers With Most Sales" /> <TopSales title="Publishers With Most Sales" />
<TopTags title="Top Tags By Sales" /> <TopTags title="Top Tags By Sales" />

View File

@ -5,7 +5,6 @@ import Downloads from './Downloads'
import ComputeJobs from './ComputeJobs' import ComputeJobs from './ComputeJobs'
import styles from './index.module.css' import styles from './index.module.css'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { chainIds } from 'app.config'
import { getComputeJobs } from '@utils/compute' import { getComputeJobs } from '@utils/compute'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'

View File

@ -17,6 +17,7 @@ export default function Preview(): ReactElement {
asset.accessDetails = { asset.accessDetails = {
type: values.pricing.type, type: values.pricing.type,
addressOrId: ZERO_ADDRESS, addressOrId: ZERO_ADDRESS,
templateId: 1,
price: `${values.pricing.price}`, price: `${values.pricing.price}`,
baseToken: { baseToken: {
address: ZERO_ADDRESS, address: ZERO_ADDRESS,

View File

@ -72,8 +72,8 @@ export const initialValues: FormPublishData = {
}, },
services: [ services: [
{ {
files: [{ url: '', type: '' }], files: [{ url: '', type: 'ipfs' }],
links: [{ url: '', type: '' }], links: [{ url: '', type: 'url' }],
dataTokenOptions: { name: '', symbol: '' }, dataTokenOptions: { name: '', symbol: '' },
timeout: '', timeout: '',
access: 'access', access: 'access',

View File

@ -23,7 +23,8 @@ import { FormPublishData, MetadataAlgorithmContainer } from './_types'
import { import {
marketFeeAddress, marketFeeAddress,
publisherMarketOrderFee, publisherMarketOrderFee,
publisherMarketFixedSwapFee publisherMarketFixedSwapFee,
defaultDatatokenTemplateIndex
} from '../../../app.config' } from '../../../app.config'
import { sanitizeUrl } from '@utils/url' import { sanitizeUrl } from '@utils/url'
import { getContainerChecksum } from '@utils/docker' import { getContainerChecksum } from '@utils/docker'
@ -138,18 +139,24 @@ export async function transformPublishFormToDdo(
} }
// this is the default format hardcoded // this is the default format hardcoded
const file = { const file = {
nftAddress, nftAddress,
datatokenAddress, datatokenAddress,
files: [ files: [
{ {
type: 'url', type: files[0].type,
index: 0, index: 0,
url: files[0].url, [files[0].type === 'ipfs'
? 'hash'
: files[0].type === 'arweave'
? 'transactionId'
: 'url']: files[0].url,
method: 'GET' method: 'GET'
} }
] ]
} }
const filesEncrypted = const filesEncrypted =
!isPreview && !isPreview &&
files?.length && files?.length &&
@ -211,7 +218,7 @@ export async function createTokensAndPricing(
// TODO: cap is hardcoded for now to 1000, this needs to be discussed at some point // TODO: cap is hardcoded for now to 1000, this needs to be discussed at some point
const ercParams: DatatokenCreateParams = { const ercParams: DatatokenCreateParams = {
templateIndex: 2, templateIndex: defaultDatatokenTemplateIndex,
minter: accountId, minter: accountId,
paymentCollector: accountId, paymentCollector: accountId,
mpFeeAddress: marketFeeAddress, mpFeeAddress: marketFeeAddress,

View File

@ -3,6 +3,7 @@ import * as Yup from 'yup'
import { getMaxDecimalsValidation } from '@utils/numbers' import { getMaxDecimalsValidation } from '@utils/numbers'
import { validateFieldSchaclSchema } from '@utils/schaclSchema' import { validateFieldSchaclSchema } from '@utils/schaclSchema'
import { FileInfo } from '@oceanprotocol/lib' import { FileInfo } from '@oceanprotocol/lib'
import { testLinks } from '../../@utils/yup'
// TODO: conditional validation // TODO: conditional validation
// e.g. when algo is selected, Docker image is required // e.g. when algo is selected, Docker image is required
@ -37,16 +38,7 @@ const validationService = {
files: Yup.array<FileInfo[]>() files: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string() url: testLinks().required('Required'),
.test(
'GoogleNotSupported',
'Google Drive is not a supported hosting service. Please use an alternative.',
(value) => {
return !value?.toString().includes('drive.google')
}
)
.url('Must be a valid URL.')
.required('Required'),
valid: Yup.boolean().isTrue().required('File must be valid.') valid: Yup.boolean().isTrue().required('File must be valid.')
}) })
@ -56,16 +48,7 @@ const validationService = {
links: Yup.array<FileInfo[]>() links: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string() url: testLinks(),
.url('Must be a valid URL.')
.test(
'GoogleNotSupported',
'Google Drive is not a supported hosting service. Please use an alternative.',
(value) => {
return !value?.toString().includes('drive.google')
}
),
// TODO: require valid file only when URL is given
valid: Yup.boolean() valid: Yup.boolean()
// valid: Yup.boolean().isTrue('File must be valid.') // valid: Yup.boolean().isTrue('File must be valid.')
}) })

View File

@ -79,8 +79,6 @@ export default function SearchPage({
fetchAssets(parsed, chainIds) fetchAssets(parsed, chainIds)
}, [parsed, chainIds, newCancelToken, fetchAssets]) }, [parsed, chainIds, newCancelToken, fetchAssets])
console.log(queryResult?.results)
return ( return (
<> <>
<div className={styles.search}> <div className={styles.search}>