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-1244-edit-ui-highlights-clear

This commit is contained in:
EnzoVezzaro 2023-01-19 07:30:42 -04:00
commit a9cf3bdc0a
253 changed files with 21715 additions and 7614 deletions

View File

@ -42,7 +42,7 @@ exclude_patterns:
- '**/*.d.ts'
- '**/@types/'
- '**/_types.*'
- '**/*.stories.tsx'
- '**/*.test.tsx'
- '**/*.stories.*'
- '**/*.test.*'
- '.storybook/'
- '.jest/'

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @mihaisc @kremalicious @claudiaHash @bogdanfazakas @EnzoVezzaro
* @jamiehewitt15 @mihaisc @kremalicious @bogdanfazakas @EnzoVezzaro

View File

@ -4,7 +4,6 @@ on:
push:
branches:
- main
- v4
- v3
tags:
- '**'
@ -20,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16']
node: ['18']
steps:
- uses: actions/checkout@v3
@ -47,7 +46,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16']
node: ['18']
steps:
- uses: actions/checkout@v3
@ -82,7 +81,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '18'
- name: Cache node_modules
uses: actions/cache@v3
env:
@ -110,7 +109,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16']
node: ['18']
steps:
- uses: actions/checkout@v3

View File

@ -12,6 +12,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run build:static

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ coverage
.next
.artifacts
.vercel
.swc
repo-metadata.json
networks-metadata.json
src/@types/subgraph

View File

@ -0,0 +1,91 @@
import { Asset } from '@oceanprotocol/lib'
export const algorithmAquarius: Asset = {
'@context': ['https://w3id.org/did/v1'],
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0',
chainId: 1,
metadata: {
created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z',
type: 'algorithm',
name: 'algorithmTestitest',
description: 'This is an algorithm test.',
links: ['https://www.oceanprotocol.com/sample'],
tags: [
'trading',
'defi',
'algorithm',
'algorithmic-crypto-trading',
'algo-trading',
'trading-strategy',
'cryptocurrency',
'crypto'
],
author: 'Test User',
license: 'https://market.oceanprotocol.com/terms',
additionalInformation: {
termsAndConditions: true
},
algorithm: {
language: 'json',
version: '0.1',
container: {
entrypoint: 'python $algo',
tag: 'latest',
image: 'https://docker.com/test.img',
checksum: ''
}
}
},
services: [
{
id: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1',
type: 'access',
files:
'0x04022ef1afafe340f41b261ef721b8dd55dee094975cc70330803d760beef38871948ce572ff1c533d56cda2665749ed2eb8283e243ec5ee19011f510b6b263b2da0af537e3f1fdff7ddd90fa26c7a4761a6d26928bc1348a302634012aac7998e92c84456ab73e9a847120c44ebda15781787e8c382391b2eaefc8b8d36998f3998d1c4647f4f7bb28f4278093c1d231f66e78f81452049443b9e540aeb42ebbdc1b748c024eb10218532814736e241efa1c2a687685b4e2ea7a877685aa0ea325d1a8cf765d1b423b32d81ec3c3e22fc9c15c6b9b71f2862edaec4e4cf7c3a638ffc0ecb88ede3cabb511d4780543a53c001a95f42de1877796e13c997b57bc671507e92198934b4ea7c2e6554993388421253e8c2f10458dec872a7ebfa71b6e77ed359222c93261ba252028c5da06ccf8defcd529885b2125816325a47e23728b513',
datatokenAddress: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
serviceEndpoint: 'https://v4.provider.goerli.oceanprotocol.com',
timeout: 604800
}
],
event: {
tx: '0x3e07a75c1cc5d4146222a93ab4319144e60ecca3ebfb8b15f1ff339d6f479dc9',
block: 7680195,
from: '0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7',
contract: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
datetime: '2022-09-29T11:31:12'
},
nft: {
address: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
name: 'Ocean Data NFT',
symbol: 'OCEAN-NFT',
state: 0,
tokenURI:
'data:application/json;base64,eyJuYW1lIjoiT2NlYW4gRGF0YSBORlQiLCJzeW1ib2wiOiJPQ0VBTi1ORlQiLCJkZXNjcmlwdGlvbiI6IlRoaXMgTkZUIHJlcHJlc2VudHMgYW4gYXNzZXQgaW4gdGhlIE9jZWFuIFByb3RvY29sIHY0IGVjb3N5c3RlbS5cblxuVmlldyBvbiBPY2VhbiBNYXJrZXQ6IGh0dHBzOi8vbWFya2V0Lm9jZWFucHJvdG9jb2wuY29tL2Fzc2V0L2RpZDpvcDo2NjU0YjA3OTM3NjViMjY5Njk2Y2VjOGQyZjBkMDc3ZDliYmNkZDNjNGYwMzNkOTQxYWI5Njg0ZThhZDA2NjMwIiwiZXh0ZXJuYWxfdXJsIjoiaHR0cHM6Ly9tYXJrZXQub2NlYW5wcm90b2NvbC5jb20vYXNzZXQvZGlkOm9wOjY2NTRiMDc5Mzc2NWIyNjk2OTZjZWM4ZDJmMGQwNzdkOWJiY2RkM2M0ZjAzM2Q5NDFhYjk2ODRlOGFkMDY2MzAiLCJiYWNrZ3JvdW5kX2NvbG9yIjoiMTQxNDE0IiwiaW1hZ2VfZGF0YSI6ImRhdGE6aW1hZ2Uvc3ZnK3htbCwlM0Nzdmcgdmlld0JveD0nMCAwIDk5IDk5JyBmaWxsPSd1bmRlZmluZWQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyclM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5Mjc3JyBkPSdNMCw5OUwwLDI5QzksMjUgMTksMjIgMjksMjFDMzgsMTkgNDksMTkgNjEsMjFDNzIsMjIgODUsMjUgOTksMjlMOTksOTlaJy8lM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5MmJiJyBkPSdNMCw5OUwwLDU1QzgsNDkgMTcsNDQgMjgsNDNDMzgsNDEgNTAsNDIgNjMsNDNDNzUsNDMgODcsNDIgOTksNDJMOTksOTlaJyUzRSUzQy9wYXRoJTNFJTNDcGF0aCBmaWxsPSclMjNmZjQwOTJmZicgZD0nTTAsOTlMMCw2OEMxMSw2NiAyMiw2NSAzMiw2N0M0MSw2OCA1MCw3MyA2MSw3NkM3MSw3OCA4NSw3OCA5OSw3OUw5OSw5OVonJTNFJTNDL3BhdGglM0UlM0Mvc3ZnJTNFIn0=',
owner: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
created: '2022-09-29T11:31:12'
},
datatokens: [
{
address: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59',
serviceId:
'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1'
}
],
stats: {
orders: 22,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
purgatory: {
state: false,
reason: ''
}
}

View File

@ -0,0 +1,80 @@
import { Asset } from '@oceanprotocol/lib'
export const datasetAquarius: Asset = {
'@context': ['https://w3id.org/did/v1'],
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0',
chainId: 1,
metadata: {
created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z',
type: 'dataset',
name: 'Testitest',
description: 'This is a test.',
tags: [
'trading',
'defi',
'algorithm',
'algorithmic-crypto-trading',
'algo-trading',
'trading-strategy',
'cryptocurrency',
'crypto'
],
author: 'Test User',
license: 'https://market.oceanprotocol.com/terms',
additionalInformation: {
termsAndConditions: true
}
},
services: [
{
id: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1',
type: 'access',
files:
'0x04022ef1afafe340f41b261ef721b8dd55dee094975cc70330803d760beef38871948ce572ff1c533d56cda2665749ed2eb8283e243ec5ee19011f510b6b263b2da0af537e3f1fdff7ddd90fa26c7a4761a6d26928bc1348a302634012aac7998e92c84456ab73e9a847120c44ebda15781787e8c382391b2eaefc8b8d36998f3998d1c4647f4f7bb28f4278093c1d231f66e78f81452049443b9e540aeb42ebbdc1b748c024eb10218532814736e241efa1c2a687685b4e2ea7a877685aa0ea325d1a8cf765d1b423b32d81ec3c3e22fc9c15c6b9b71f2862edaec4e4cf7c3a638ffc0ecb88ede3cabb511d4780543a53c001a95f42de1877796e13c997b57bc671507e92198934b4ea7c2e6554993388421253e8c2f10458dec872a7ebfa71b6e77ed359222c93261ba252028c5da06ccf8defcd529885b2125816325a47e23728b513',
datatokenAddress: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
serviceEndpoint: 'https://v4.provider.goerli.oceanprotocol.com',
timeout: 604800
}
],
event: {
tx: '0x3e07a75c1cc5d4146222a93ab4319144e60ecca3ebfb8b15f1ff339d6f479dc9',
block: 7680195,
from: '0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7',
contract: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
datetime: '2022-09-29T11:31:12'
},
nft: {
address: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
name: 'Ocean Data NFT',
symbol: 'OCEAN-NFT',
state: 0,
tokenURI:
'data:application/json;base64,eyJuYW1lIjoiT2NlYW4gRGF0YSBORlQiLCJzeW1ib2wiOiJPQ0VBTi1ORlQiLCJkZXNjcmlwdGlvbiI6IlRoaXMgTkZUIHJlcHJlc2VudHMgYW4gYXNzZXQgaW4gdGhlIE9jZWFuIFByb3RvY29sIHY0IGVjb3N5c3RlbS5cblxuVmlldyBvbiBPY2VhbiBNYXJrZXQ6IGh0dHBzOi8vbWFya2V0Lm9jZWFucHJvdG9jb2wuY29tL2Fzc2V0L2RpZDpvcDo2NjU0YjA3OTM3NjViMjY5Njk2Y2VjOGQyZjBkMDc3ZDliYmNkZDNjNGYwMzNkOTQxYWI5Njg0ZThhZDA2NjMwIiwiZXh0ZXJuYWxfdXJsIjoiaHR0cHM6Ly9tYXJrZXQub2NlYW5wcm90b2NvbC5jb20vYXNzZXQvZGlkOm9wOjY2NTRiMDc5Mzc2NWIyNjk2OTZjZWM4ZDJmMGQwNzdkOWJiY2RkM2M0ZjAzM2Q5NDFhYjk2ODRlOGFkMDY2MzAiLCJiYWNrZ3JvdW5kX2NvbG9yIjoiMTQxNDE0IiwiaW1hZ2VfZGF0YSI6ImRhdGE6aW1hZ2Uvc3ZnK3htbCwlM0Nzdmcgdmlld0JveD0nMCAwIDk5IDk5JyBmaWxsPSd1bmRlZmluZWQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyclM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5Mjc3JyBkPSdNMCw5OUwwLDI5QzksMjUgMTksMjIgMjksMjFDMzgsMTkgNDksMTkgNjEsMjFDNzIsMjIgODUsMjUgOTksMjlMOTksOTlaJy8lM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5MmJiJyBkPSdNMCw5OUwwLDU1QzgsNDkgMTcsNDQgMjgsNDNDMzgsNDEgNTAsNDIgNjMsNDNDNzUsNDMgODcsNDIgOTksNDJMOTksOTlaJyUzRSUzQy9wYXRoJTNFJTNDcGF0aCBmaWxsPSclMjNmZjQwOTJmZicgZD0nTTAsOTlMMCw2OEMxMSw2NiAyMiw2NSAzMiw2N0M0MSw2OCA1MCw3MyA2MSw3NkM3MSw3OCA4NSw3OCA5OSw3OUw5OSw5OVonJTNFJTNDL3BhdGglM0UlM0Mvc3ZnJTNFIn0=',
owner: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
created: '2022-09-29T11:31:12'
},
datatokens: [
{
address: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59',
serviceId:
'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1'
}
],
stats: {
orders: 22,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
purgatory: {
state: false,
reason: ''
}
}

View File

@ -0,0 +1,27 @@
import { datasetAquarius } from './datasetAquarius'
export const asset: AssetExtended = {
...datasetAquarius,
accessDetails: {
templateId: 1,
publisherMarketOrderFee: '0',
type: 'fixed',
addressOrId:
'0x00e3b740e4d8bf6e97010ecb5b14d1b7efc0913bfa291fcf5adb8eb9e6c29e93',
price: '3231343254',
isPurchasable: true,
isOwned: false,
validOrderTx: null,
baseToken: {
address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a',
name: 'Ocean Token',
symbol: 'OCEAN',
decimals: 18
},
datatoken: {
address: '0x067e1e6ec580f3f0f6781679a4a5ab07a6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59'
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
export default {
prices: {
h2o: {
btc: 0.00009013,
cad: 2.38,
cny: 12.36,
eth: 0.00132713,
eur: 1.78,
gbp: 1.55,
hkd: 13.53,
inr: 141.78,
jpy: 253.21,
link: 0.24050954,
rub: 109.81,
sgd: 2.47,
usd: 1.72
},
'matic-network': {
btc: 0.0000414,
cad: 1.093,
cny: 5.67,
eth: 0.00061102,
eur: 0.816959,
gbp: 0.715112,
hkd: 6.21,
inr: 65.06,
jpy: 116.23,
link: 0.11072143,
rub: 50.4,
sgd: 1.14,
usd: 0.790922
},
ethereum: {
btc: 0.06775775,
cad: 1789.03,
cny: 9288.09,
eth: 1,
eur: 1337.1,
gbp: 1170.41,
hkd: 10161.71,
inr: 106490,
jpy: 190239,
link: 181.216,
rub: 82491,
sgd: 1859.58,
usd: 1294.49
},
'ocean-protocol': {
btc: 0.00000809,
cad: 0.213554,
cny: 1.11,
eth: 0.00011937,
eur: 0.159608,
gbp: 0.13971,
hkd: 1.21,
inr: 12.71,
jpy: 22.71,
link: 0.02163146,
rub: 9.85,
sgd: 0.221976,
usd: 0.154521
}
}
}

View File

@ -0,0 +1,41 @@
export const columns = [
{
name: 'Name',
selector: (row: any) => row.name,
maxWidth: '45rem',
grow: 1
},
{
name: 'Symbol',
selector: (row: any) => row.symbol,
maxWidth: '10rem'
},
{
name: 'Price',
selector: (row: any) => row.price,
right: true
}
]
export const data = [
{
name: 'Title asset',
symbol: 'DATA-70',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
},
{
name: 'Title asset',
symbol: 'DATA-72',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
}
]

View File

@ -0,0 +1,9 @@
export default {
debug: true,
currency: 'EUR',
locale: 'en-US',
chainIds: [5, 1, 137, 56, 1285, 246],
bookmarks: [],
privacyPolicySlug: '/privacy/en',
showPPC: true
}

View File

@ -0,0 +1,33 @@
export default {
accountEns: 'jellymcjellyfish.eth',
accountEnsAvatar:
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth',
accountId: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
approvedBaseTokens: [
{
address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a',
symbol: 'OCEAN',
name: 'Ocean Token',
decimals: 18
}
],
balance: { eth: '0', ocean: '1000' },
block: 7751969,
chainId: 5,
connect: jest.fn(),
isSupportedOceanNetwork: true,
isTestnet: true,
logout: jest.fn(),
networkData: { name: 'Görli', title: 'Ethereum Testnet Görli', chain: 'ETH' },
networkDisplayName: 'ETH Görli',
networkId: 5,
web3: { currentProvider: {} },
web3Loading: false,
web3Modal: { show: false, eventController: {}, connect: jest.fn() },
web3Provider: {},
web3ProviderInfo: {
id: 'injected',
name: 'MetaMask',
logo: ''
}
}

View File

@ -0,0 +1,3 @@
import { assets } from '../../__fixtures__/datasetsWithAccessDetails'
export const getAccessDetailsForAssets = jest.fn().mockResolvedValue(assets)

View File

@ -0,0 +1,20 @@
import marketMetadata from '../__fixtures__/marketMetadata'
import userPreferences from '../__fixtures__/userPreferences'
import web3 from '../__fixtures__/web3'
import { asset } from '../__fixtures__/datasetWithAccessDetails'
jest.mock('../../src/@context/MarketMetadata', () => ({
useMarketMetadata: () => marketMetadata
}))
jest.mock('../../src/@context/UserPreferences', () => ({
useUserPreferences: () => userPreferences
}))
jest.mock('../../src/@context/Web3', () => ({
useWeb3: () => web3
}))
jest.mock('../../../@context/Asset', () => ({
useAsset: () => ({ asset })
}))

View File

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

View File

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

3
.jest/__mocks__/tar.ts Normal file
View File

@ -0,0 +1,3 @@
// mocked, as this module makes Jest go all
// "Uncaught SyntaxError: Octal escape sequences are not allowed in strict mode"
export default jest.fn().mockImplementation(() => 'hello')

View File

@ -9,13 +9,13 @@ const createJestConfig = nextJest({
const customJestConfig = {
rootDir: '../',
// Add more setup options before each test is run
setupFilesAfterEnv: ['<rootDir>/.jest/jest.setup.js'],
setupFilesAfterEnv: ['<rootDir>/.jest/jest.setup.tsx'],
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleDirectories: ['node_modules', '<rootDir>/src'],
testEnvironment: 'jest-environment-jsdom',
testEnvironment: 'jsdom',
moduleNameMapper: {
'^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
// '^@/components/(.*)$': '<rootDir>/components/$1',
'@components/(.*)$': '<rootDir>/src/components/$1',
'@shared(.*)$': '<rootDir>/src/components/@shared/$1',
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
'@context/(.*)$': '<rootDir>/src/@context/$1',
@ -29,8 +29,25 @@ const customJestConfig = {
'!src/**/*.{stories,test}.{ts,tsx}',
'!src/@types/**/*.{ts,tsx}'
],
testPathIgnorePatterns: ['node_modules', '\\.cache', '.next', 'coverage']
// Add ignores so ESM packages are not transformed by Jest
// note: this does not work with Next.js, hence workaround further down
// see: https://github.com/vercel/next.js/issues/35634#issuecomment-1115250297
// transformIgnorePatterns: ['node_modules/(?!(uuid|remark)/)'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/.next/',
'<rootDir>/coverage'
]
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
// https://github.com/vercel/next.js/issues/35634#issuecomment-1115250297
async function jestConfig() {
const nextJestConfig = await createJestConfig(customJestConfig)()
// Add ignores for specific ESM packages so they are transformed by Jest
// /node_modules/ is the first pattern
nextJestConfig.transformIgnorePatterns[0] = '/node_modules/(?!uuid|remark)/'
return nextJestConfig
}
module.exports = jestConfig

View File

@ -1,7 +0,0 @@
import '@testing-library/jest-dom/extend-expect'
import './__mocks__/matchMedia'
import marketMetadataMock from './__mocks__/MarketMetadata'
jest.mock('../../src/@context/MarketMetadata', () => ({
useMarketMetadata: () => marketMetadataMock
}))

20
.jest/jest.setup.tsx Normal file
View File

@ -0,0 +1,20 @@
import '@testing-library/jest-dom/extend-expect'
import { jest } from '@jest/globals'
import './__mocks__/matchMedia'
import './__mocks__/hooksMocks'
jest.mock('next/router', () => ({
useRouter: jest.fn().mockImplementation(() => ({
route: '/',
pathname: '/'
}))
}))
// jest.mock('next/head', () => {
// return {
// __esModule: true,
// default: ({ children }: { children: Array<React.ReactElement> }) => {
// return <>{children}</>
// }
// }
// })

2
.nvmrc
View File

@ -1 +1 @@
16
18

View File

@ -39,6 +39,12 @@
The app is a React app built with [Next.js](https://nextjs.org) + TypeScript + CSS modules and will connect to Ocean remote components by default.
Prerequisites:
- [Node.js](https://nodejs.org/en/) (required). Check the [.nvmrc](.nvmrc) file to make sure you are using the correct version of Node.js.
- [nvm](https://github.com/nvm-sh/nvm) (recommended). This is the recommend way to manage Node.js versions.
- [Git](https://git-scm.com/) is required to follow the instructions below.
To start local development:
```bash
@ -253,7 +259,7 @@ export default function NetworkName(): ReactElement {
const { networkId, isTestnet } = useWeb3()
const { networksList } = useNetworkMetadata()
const networkData = getNetworkDataById(networksList, networkId)
const networkName = getNetworkDisplayName(networkData, networkId)
const networkName = getNetworkDisplayName(networkData)
return (
<>

View File

@ -14,10 +14,11 @@ module.exports = {
chainIds: [1, 137, 56, 246, 1285],
// List of all supported chainIds. Used to populate the Chains user preferences list.
chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287],
chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001],
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
defaultDatatokenTemplateIndex: 2,
// The ETH address the marketplace fee will be sent to.
marketFeeAddress:
process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS ||

View File

@ -29,19 +29,66 @@
},
{
"name": "files",
"label": "New 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.** 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.",
"prominentHelp": true,
"type": "files"
"label": "File",
"prominentHelp": false,
"type": "tabs",
"fields": [
{
"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",
"label": "New sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"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.",
"prominentHelp": true,
"type": "files"
"label": "Sample file",
"prominentHelp": false,
"type": "tabs",
"fields": [
{
"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 +113,13 @@
"type": "tags",
"placeholder": "e.g. logistics",
"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

@ -24,8 +24,7 @@
},
"approval": {
"tooltips": {
"approveSpecific": "Give the smart contract permission to spend your COIN which has to be done for each transaction. You can optionally set this to infinite in your user preferences.",
"approveInfinite": "Give the smart contract permission to spend infinte amounts of your COIN so you have to do this only once. You can disable allowing infinite amounts in your user preferences."
"approveSpecific": "Give the smart contract permission to spend your COIN which has to be done for each transaction. You can optionally set this to infinite in your user preferences."
}
}
}

View File

@ -104,19 +104,65 @@
{
"name": "files",
"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.** 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",
"prominentHelp": false,
"type": "tabs",
"fields": [
{
"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",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"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.**",
"prominentHelp": true,
"type": "files"
"prominentHelp": false,
"type": "tabs",
"fields": [
{
"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",

View File

@ -16,6 +16,6 @@
],
"announcement": "[Lock your OCEAN](https://df.oceandao.org/) to get veOCEAN, earn rewards and curate data.",
"warning": {
"ctd": "Compute-to-Data is still in a testing phase, please use it only on test networks."
"ctd": "Please note that Compute-to-Data is still in alpha phase."
}
}

21507
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
"type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
"postinstall": "husky install",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook"
@ -26,99 +26,100 @@
"@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.2.3",
"@oceanprotocol/lib": "^2.6.0",
"@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6",
"@urql/exchange-refocus": "^1.0.0",
"@walletconnect/web3-provider": "^1.8.0",
"axios": "^0.27.2",
"axios": "^1.2.0",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"decimal.js": "^10.3.1",
"decimal.js": "^10.4.2",
"dom-confetti": "^0.2.2",
"dotenv": "^16.0.1",
"dotenv": "^16.0.3",
"filesize": "^10.0.5",
"formik": "^2.2.9",
"gray-matter": "^4.0.3",
"is-ipfs": "^7.0.3",
"is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0",
"match-sorter": "^6.3.1",
"myetherwallet-blockies": "^0.1.1",
"next": "12.3.1",
"next": "13.0.5",
"query-string": "^7.1.1",
"react": "^18.2.0",
"react-clipboard.js": "^2.0.16",
"react-data-table-component": "^7.5.2",
"react-dom": "^18.1.0",
"react-data-table-component": "^7.5.3",
"react-dom": "^18.2.0",
"react-dotdotdot": "^1.3.1",
"react-modal": "^3.15.1",
"react-paginate": "^8.1.3",
"react-select": "^5.4.0",
"react-spring": "^9.5.2",
"react-tabs": "^5.1.0",
"react-toastify": "^9.0.4",
"remark": "^13.0.0",
"remark-gfm": "^1.0.0",
"remark-html": "^13.0.1",
"react-modal": "^3.16.1",
"react-paginate": "^8.1.4",
"react-select": "^5.7.0",
"react-spring": "^9.5.5",
"react-tabs": "^6.0.0",
"react-toastify": "^9.1.1",
"remark": "^14.0.2",
"remark-gfm": "^3.0.1",
"remark-html": "^15.0.1",
"remove-markdown": "^0.5.0",
"slugify": "^1.6.5",
"swr": "^1.3.0",
"urql": "^3.0.3",
"web3": "^1.8.0",
"web3modal": "^1.9.9",
"web3": "^1.8.1",
"web3modal": "^1.9.10",
"yup": "^0.32.11"
},
"devDependencies": {
"@storybook/addon-essentials": "^6.5.12",
"@storybook/builder-webpack5": "^6.5.12",
"@storybook/manager-webpack5": "^6.5.12",
"@storybook/react": "^6.5.12",
"@svgr/webpack": "^6.3.1",
"@storybook/addon-essentials": "^6.5.15",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/jest": "^29.2.5",
"@types/js-cookie": "^3.0.2",
"@types/loadable__component": "^5.13.4",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.omit": "^4.5.7",
"@types/node": "^18.7.18",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.5",
"@types/node": "^18.8.5",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"apollo": "^2.34.0",
"cross-env": "^7.0.3",
"eslint": "^8.23.1",
"eslint": "^8.28.0",
"eslint-config-oceanprotocol": "^2.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-jest-dom": "^4.0.3",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.7.0",
"eslint-plugin-testing-library": "^5.9.1",
"https-browserify": "^1.0.0",
"husky": "^8.0.1",
"jest": "^29.1.2",
"jest-environment-jsdom": "^29.0.3",
"prettier": "^2.7.1",
"husky": "^8.0.2",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"prettier": "^2.8.1",
"pretty-quick": "^3.1.3",
"process": "^0.11.10",
"serve": "^14.0.1",
"serve": "^14.1.2",
"stream-http": "^3.2.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.3"
"typescript": "^4.9.3"
},
"overrides": {
"graphql": "15.8.0"
},
"repository": {
"type": "git",
"url": "https://github.com/oceanprotocol/market"
},
"engines": {
"node": ">=14"
"node": "18"
},
"browserslist": [
">0.2%",

View File

@ -9,13 +9,14 @@ import React, {
} from 'react'
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
import { CancelToken } from 'axios'
import { retrieveAsset } from '@utils/aquarius'
import { getAsset } from '@utils/aquarius'
import { useWeb3 } from './Web3'
import { useCancelToken } from '@hooks/useCancelToken'
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
import { getAccessDetails } from '@utils/accessDetailsAndPricing'
import { useIsMounted } from '@hooks/useIsMounted'
import { useMarketMetadata } from './MarketMetadata'
import { isValidDid } from '@utils/ddo'
export interface AssetProviderValue {
isInPurgatory: boolean
@ -63,10 +64,17 @@ function AssetProvider({
const fetchAsset = useCallback(
async (token?: CancelToken) => {
if (!did) return
const isDid = isValidDid(did)
if (!isDid) {
setError(`The url is not for a valid DID`)
LoggerInstance.error(`[asset] Not a valid DID`)
return
}
LoggerInstance.log('[asset] Fetching asset...')
setLoading(true)
const asset = await retrieveAsset(did, token)
const asset = await getAsset(did, token)
if (!asset) {
setError(
@ -77,8 +85,8 @@ function AssetProvider({
return
}
if (asset.nft.state) {
// handle nft states as documented in https://docs.oceanprotocol.com/concepts/did-ddo/#state
if ([1, 2, 3].includes(asset.nft.state)) {
// handle nft states as documented in https://docs.oceanprotocol.com/core-concepts/did-ddo/#state
let state
switch (asset.nft.state) {
case 1:
@ -120,7 +128,7 @@ function AssetProvider({
// Helper: Get and set asset access details
// -----------------------------------
const fetchAccessDetails = useCallback(async (): Promise<void> => {
if (!asset?.chainId || !asset?.services) return
if (!asset?.chainId || !asset?.services?.length) return
const accessDetails = await getAccessDetails(
asset.chainId,

View File

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

View File

@ -7,7 +7,7 @@ import React, {
useEffect,
useState
} from 'react'
import { OpcQuery } from 'src/@types/subgraph/OpcQuery'
import { OpcQuery } from '../../../src/@types/subgraph/OpcQuery'
import { OperationResult } from 'urql'
import { opcQuery } from './_queries'
import { MarketMetadataProviderValue, OpcFee } from './_types'

View File

@ -29,6 +29,7 @@ interface ProfileProviderValue {
downloadsTotal: number
isDownloadsLoading: boolean
sales: number
ownAccount: boolean
}
const ProfileContext = createContext({} as ProfileProviderValue)
@ -46,17 +47,18 @@ const clearedProfile: Profile = {
function ProfileProvider({
accountId,
accountEns,
ownAccount,
children
}: {
accountId: string
accountEns: string
ownAccount: boolean
children: ReactNode
}): ReactElement {
const { chainIds } = useUserPreferences()
const { appConfig } = useMarketMetadata()
const [isEthAddress, setIsEthAddress] = useState<boolean>()
//
// Do nothing in all following effects
// when accountId is no ETH address
@ -111,7 +113,8 @@ function ProfileProvider({
const result = await getPublishedAssets(
accountId,
chainIds,
cancelTokenSource.token
cancelTokenSource.token,
ownAccount
)
setAssets(result.results)
setAssetsTotal(result.totalResults)
@ -134,7 +137,13 @@ function ProfileProvider({
return () => {
cancelTokenSource.cancel()
}
}, [accountId, appConfig.metadataCacheUri, chainIds, isEthAddress])
}, [
accountId,
appConfig.metadataCacheUri,
chainIds,
isEthAddress,
ownAccount
])
//
// DOWNLOADS
@ -154,11 +163,13 @@ function ProfileProvider({
for (let i = 0; i < tokenOrders?.length; i++) {
dtList.push(tokenOrders[i].datatoken.address)
}
const downloads = await getDownloadAssets(
dtList,
tokenOrders,
chainIds,
cancelToken
cancelToken,
ownAccount
)
setDownloads(downloads)
setDownloadsTotal(downloads.length)
@ -167,7 +178,7 @@ function ProfileProvider({
downloads
)
},
[accountId, chainIds]
[accountId, chainIds, ownAccount]
)
useEffect(() => {
@ -230,6 +241,7 @@ function ProfileProvider({
downloads,
downloadsTotal,
isDownloadsLoading,
ownAccount,
sales
}}
>

View File

@ -24,8 +24,6 @@ interface UserPreferencesValue {
removeBookmark: (did: string) => void
setPrivacyPolicySlug: (slug: string) => void
setShowPPC: (value: boolean) => void
infiniteApproval: boolean
setInfiniteApproval: (value: boolean) => void
locale: string
}
@ -73,10 +71,6 @@ function UserPreferencesProvider({
localStorage?.showPPC !== false
)
const [infiniteApproval, setInfiniteApproval] = useState(
localStorage?.infiniteApproval || false
)
// Write values to localStorage on change
useEffect(() => {
setLocalStorage({
@ -85,18 +79,9 @@ function UserPreferencesProvider({
currency,
bookmarks,
privacyPolicySlug,
showPPC,
infiniteApproval
showPPC
})
}, [
chainIds,
debug,
currency,
bookmarks,
privacyPolicySlug,
showPPC,
infiniteApproval
])
}, [chainIds, debug, currency, bookmarks, privacyPolicySlug, showPPC])
// Set ocean.js log levels, default: Error
useEffect(() => {
@ -152,8 +137,6 @@ function UserPreferencesProvider({
bookmarks,
privacyPolicySlug,
showPPC,
infiniteApproval,
setInfiniteApproval,
setChainIds,
setDebug,
setCurrency,

View File

@ -307,7 +307,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
)
// Construct network display name
const networkDisplayName = getNetworkDisplayName(networkData, networkId)
const networkDisplayName = getNetworkDisplayName(networkData)
setNetworkDisplayName(networkDisplayName)
setIsTestnet(getNetworkType(networkData) !== NetworkType.Mainnet)

View File

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

View File

@ -0,0 +1,43 @@
import { getNetworkType, getNetworkDisplayName } from './utils'
describe('useNetworkMetadata/utils', () => {
test('getNetworkType returns mainnet', () => {
const type = getNetworkType({
name: 'Eth',
title: 'Eth'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
expect(type).toBe('mainnet')
})
test('getNetworkType returns testnet if "Test" is in name', () => {
const type = getNetworkType({
name: 'Testnet',
title: 'Testnet'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
expect(type).toBe('testnet')
})
test('getNetworkDisplayName returns correct values', () => {
/* eslint-disable @typescript-eslint/no-explicit-any */
const type1 = getNetworkDisplayName({
chainId: 1,
chain: 'ETH',
name: 'Ethereum Mainnet'
} as any)
expect(type1).toBe('ETH')
const type2 = getNetworkDisplayName({ chainId: 80001 } as any)
expect(type2).toBe('Mumbai')
const type3 = getNetworkDisplayName({ chainId: 8996 } as any)
expect(type3).toBe('Development')
const type4 = getNetworkDisplayName({ chainId: 2021000 } as any)
expect(type4).toBe('GAIA-X')
/* eslint-enable @typescript-eslint/no-explicit-any */
})
})

View File

@ -11,9 +11,8 @@ export function getNetworkType(network: EthereumListsChain): string {
// We hack in mainnet detection for moonriver.
if (
network &&
!network.name.includes('Testnet') &&
!network.title?.includes('Testnet') &&
network.name !== 'Moonbase Alpha'
!network.name?.includes('Testnet') &&
!network.title?.includes('Testnet')
) {
return NetworkType.Mainnet
} else {
@ -21,19 +20,14 @@ export function getNetworkType(network: EthereumListsChain): string {
}
}
export function getNetworkDisplayName(
data: EthereumListsChain,
networkId: number
): string {
export function getNetworkDisplayName(data: EthereumListsChain): string {
let displayName
if (!data) return 'Unknown'
switch (networkId) {
switch (data.chainId) {
case 137:
displayName = 'Polygon'
break
case 1287:
displayName = 'Moonbase'
break
case 1285:
displayName = 'Moonriver'
break
@ -43,9 +37,6 @@ export function getNetworkDisplayName(
case 8996:
displayName = 'Development'
break
case 3:
displayName = 'Ropsten'
break
case 5:
displayName = 'Görli'
break
@ -54,7 +45,9 @@ export function getNetworkDisplayName(
break
default:
displayName = data
? `${data.chain} ${getNetworkType(data) === 'mainnet' ? '' : data.name}`
? `${data.chain}${
getNetworkType(data) === 'mainnet' ? '' : ` ${data.name}`
}`
: 'Unknown'
break
}

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 {
interface AssetExtended extends Asset {
accessDetails?: AccessDetails
views?: number
}
}

View File

@ -27,4 +27,9 @@ declare global {
computeJobs: ComputeJobMetaData[]
isLoaded: boolean
}
interface totalPriceMap {
value: string
symbol: string
}
}

View File

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

View File

@ -12,4 +12,5 @@ interface BaseQueryParams {
aggs?: any
filters?: FilterTerm[]
ignorePurgatory?: boolean
ignoreState?: boolean
}

View File

@ -46,6 +46,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
query: any
sort?: { [jsonPath: string]: SortDirectionOptions }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
aggs?: any
}
}

View File

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

View File

@ -0,0 +1,70 @@
import {
SortDirectionOptions,
SortTermOptions
} from '../../@types/aquarius/SearchQuery'
import { escapeEsReservedCharacters, getFilterTerm, generateBaseQuery } from '.'
const defaultBaseQueryReturn = {
from: 0,
query: {
bool: {
filter: [
{ term: { _index: 'aquarius' } },
{ terms: { chainId: [1, 3] } },
{ term: { 'purgatory.state': false } },
{ bool: { must_not: [{ term: { 'nft.state': 5 } }] } }
]
}
},
size: 1000
}
describe('@utils/aquarius', () => {
test('escapeEsReservedCharacters', () => {
expect(escapeEsReservedCharacters('<')).toBe('\\<')
})
test('getFilterTerm with string value', () => {
expect(getFilterTerm('hello', 'world')).toStrictEqual({
term: { hello: 'world' }
})
})
test('getFilterTerm with array value', () => {
expect(getFilterTerm('hello', ['world', 'domination'])).toStrictEqual({
terms: { hello: ['world', 'domination'] }
})
})
test('generateBaseQuery', () => {
expect(generateBaseQuery({ chainIds: [1, 3] })).toStrictEqual(
defaultBaseQueryReturn
)
})
test('generateBaseQuery aggs are passed through', () => {
expect(
generateBaseQuery({ chainIds: [1, 3], aggs: 'hello world' })
).toStrictEqual({
...defaultBaseQueryReturn,
aggs: 'hello world'
})
})
test('generateBaseQuery sortOptions are passed through', () => {
expect(
generateBaseQuery({
chainIds: [1, 3],
sortOptions: {
sortBy: SortTermOptions.Created,
sortDirection: SortDirectionOptions.Ascending
}
})
).toStrictEqual({
...defaultBaseQueryReturn,
sort: {
'nft.created': 'asc'
}
})
})
})

View File

@ -1,13 +1,13 @@
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../app.config'
import { OrdersData_orders as OrdersData } from '../../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../../app.config'
import {
SortDirectionOptions,
SortTermOptions
} from '../@types/aquarius/SearchQuery'
import { transformAssetToAssetSelection } from './assetConvertor'
} from '../../@types/aquarius/SearchQuery'
import { transformAssetToAssetSelection } from '../assetConvertor'
export interface UserSales {
id: string
@ -19,7 +19,7 @@ export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
export function escapeEsReservedCharacters(value: string): string {
// eslint-disable-next-line no-useless-escape
const pattern = /([\!\*\+\-\=\<\>\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g
return value.replace(pattern, '\\$1')
return value?.replace(pattern, '\\$1')
}
/**
@ -42,6 +42,24 @@ export function getFilterTerm(
export function generateBaseQuery(
baseQueryParams: BaseQueryParams
): SearchQuery {
const filters: unknown[] = [getFilterTerm('_index', 'aquarius')]
baseQueryParams.filters && filters.push(...baseQueryParams.filters)
baseQueryParams.chainIds &&
filters.push(getFilterTerm('chainId', baseQueryParams.chainIds))
!baseQueryParams.ignorePurgatory &&
filters.push(getFilterTerm('purgatory.state', false))
!baseQueryParams.ignoreState &&
filters.push({
bool: {
must_not: [
{
term: {
'nft.state': 5
}
}
]
}
})
const generatedQuery = {
from: baseQueryParams.esPaginationOptions?.from || 0,
size:
@ -51,16 +69,7 @@ export function generateBaseQuery(
query: {
bool: {
...baseQueryParams.nestedQuery,
filter: [
...(baseQueryParams.filters || []),
baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds)
: [],
getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory
? []
: [getFilterTerm('purgatory.state', false)])
]
filter: filters
}
}
} as SearchQuery
@ -75,7 +84,6 @@ export function generateBaseQuery(
baseQueryParams.sortOptions.sortDirection ||
SortDirectionOptions.Descending
}
return generatedQuery
}
@ -128,7 +136,7 @@ export async function queryMetadata(
}
}
export async function retrieveAsset(
export async function getAsset(
did: string,
cancelToken: CancelToken
): Promise<Asset> {
@ -171,73 +179,7 @@ export async function getAssetsNames(
}
}
export async function getAssetsFromDidList(
didList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<PagedAssets> {
try {
if (!(didList.length > 0)) return
const baseParams = {
chainIds,
filters: [getFilterTerm('_id', didList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function getAssetsFromDtList(
dtList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<Asset[]> {
try {
if (!(dtList.length > 0)) return
const baseParams = {
chainIds,
filters: [getFilterTerm('services.datatokenAddress', dtList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult?.results
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function getAssetsFromNftList(
nftList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<Asset[]> {
try {
if (!(nftList.length > 0)) return
const baseParams = {
chainIds,
filters: [getFilterTerm('nftAddress', nftList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult?.results
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function retrieveDDOListByDIDs(
export async function getAssetsFromDids(
didList: string[],
chainIds: number[],
cancelToken: CancelToken
@ -276,7 +218,7 @@ export async function getAlgorithmDatasetsForCompute(
must: {
match: {
'services.compute.publisherTrustedAlgorithms.did': {
query: escapeEsReservedCharacters(algorithmId)
query: algorithmId
}
}
}
@ -289,7 +231,6 @@ export async function getAlgorithmDatasetsForCompute(
const query = generateBaseQuery(baseQueryParams)
const computeDatasets = await queryMetadata(query, cancelToken)
if (computeDatasets?.totalResults === 0) return []
const datasets = await transformAssetToAssetSelection(
@ -304,6 +245,7 @@ export async function getPublishedAssets(
accountId: string,
chainIds: number[],
cancelToken: CancelToken,
ignoreState = false,
page?: number,
type?: string,
accesType?: string
@ -332,6 +274,7 @@ export async function getPublishedAssets(
}
},
ignorePurgatory: true,
ignoreState,
esPaginationOptions: {
from: (Number(page) - 1 || 0) * 9,
size: 9
@ -352,7 +295,7 @@ export async function getPublishedAssets(
}
}
export async function getTopPublishers(
async function getTopPublishers(
chainIds: number[],
cancelToken: CancelToken,
page?: number,
@ -445,14 +388,17 @@ export async function getDownloadAssets(
dtList: string[],
tokenOrders: OrdersData[],
chainIds: number[],
cancelToken: CancelToken
cancelToken: CancelToken,
ignoreState = false
): Promise<DownloadedAsset[]> {
const baseQueryparams = {
chainIds,
filters: [
getFilterTerm('services.datatokenAddress', dtList),
getFilterTerm('services.type', 'access')
]
],
ignorePurgatory: true,
ignoreState
} as BaseQueryParams
const query = generateBaseQuery(baseQueryparams)
try {

View File

@ -1,6 +1,6 @@
import { getAccessDetailsForAssets } from './accessDetailsAndPricing'
import { PublisherTrustedAlgorithm, Asset } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { getServiceByName } from './ddo'
export async function transformAssetToAssetSelection(
@ -14,11 +14,12 @@ export async function transformAssetToAssetSelection(
const algorithmList: AssetSelectionAsset[] = []
for (const asset of extendedAssets) {
const algoComputeService = getServiceByName(asset, 'compute')
const algoService =
getServiceByName(asset, 'compute') || getServiceByName(asset, 'access')
if (
asset?.accessDetails?.price &&
algoComputeService?.serviceEndpoint === datasetProviderEndpoint
algoService?.serviceEndpoint === datasetProviderEndpoint
) {
let selected = false
selectedAlgorithms?.forEach((algorithm: PublisherTrustedAlgorithm) => {

View File

@ -17,14 +17,14 @@ import {
queryMetadata,
getFilterTerm,
generateBaseQuery,
retrieveDDOListByDIDs
getAssetsFromDids
} from './aquarius'
import { fetchDataForMultipleChains } from './subgraph'
import { getServiceById, getServiceByName } from './ddo'
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { SortTermOptions } from '../@types/aquarius/SearchQuery'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { transformAssetToAssetSelection } from './assetConvertor'
import { ComputeEditForm } from 'src/components/Asset/Edit/_types'
import { ComputeEditForm } from '../components/Asset/Edit/_types'
import { getFileDidInfo } from './provider'
const getComputeOrders = gql`
@ -338,7 +338,7 @@ export async function createTrustedAlgorithmList(
if (!selectedAlgorithms || selectedAlgorithms.length === 0)
return trustedAlgorithms
const selectedAssets = await retrieveDDOListByDIDs(
const selectedAssets = await getAssetsFromDids(
selectedAlgorithms,
[assetChainId],
cancelToken
@ -393,31 +393,3 @@ export async function transformComputeFormToServiceComputeOptions(
return privacy
}
export async function checkComputeResourcesValidity(
asset: Asset,
accountId: string,
computeEnvMaxJobDuration: number,
datasetTimeout?: number,
algorithmTimeout?: number,
cancelToken?: CancelToken
): Promise<boolean> {
const jobs = await getComputeJobs(
[asset?.chainId],
accountId,
asset,
cancelToken
)
if (jobs.computeJobs.length <= 0) return false
const inputValues = []
computeEnvMaxJobDuration && inputValues.push(computeEnvMaxJobDuration * 60)
datasetTimeout && inputValues.push(datasetTimeout)
algorithmTimeout && inputValues.push(algorithmTimeout)
const minValue = Math.min(...inputValues)
const jobStartDate = new Date(
parseInt(jobs.computeJobs[0].dateCreated) * 1000
)
jobStartDate.setMinutes(jobStartDate.getMinutes() + Math.floor(minValue / 60))
const currentTime = new Date().getTime() / 1000
return Math.floor(jobStartDate.getTime() / 1000) > currentTime
}

View File

@ -1,5 +1,10 @@
import { Asset, DDO, Service } from '@oceanprotocol/lib'
export function isValidDid(did: string): boolean {
const regex = /did:op:[A-Za-z0-9]{64}/
return regex.test(did)
}
export function getServiceByName(
ddo: Asset | DDO,
name: 'access' | 'compute'

View File

@ -1,18 +1,13 @@
import { LoggerInstance, Dispenser, Datatoken } from '@oceanprotocol/lib'
import { LoggerInstance, Datatoken } from '@oceanprotocol/lib'
import Web3 from 'web3'
import { TransactionReceipt } from 'web3-core'
export async function setMinterToPublisher(
web3: Web3,
dispenserAddress: string,
datatokenAddress: string,
accountId: string,
setError: (msg: string) => void
): Promise<TransactionReceipt> {
const dispenserInstance = new Dispenser(dispenserAddress, web3)
const status = await dispenserInstance.status(datatokenAddress)
if (!status?.active) return
const datatokenInstance = new Datatoken(web3)
const response = await datatokenInstance.removeMinter(
@ -20,6 +15,7 @@ export async function setMinterToPublisher(
accountId,
accountId
)
if (!response) {
setError('Updating DDO failed.')
LoggerInstance.error('Failed at cancelMinter')

View File

@ -1,5 +1,7 @@
import { getEnsName, getEnsAddress, getEnsProfile } from './ens'
import { getEnsName, getEnsAddress, getEnsProfile } from '.'
// TODO: this directly hits the ENS registry, which is not ideal
// so we need to rewrite this to mock responses instead for more reliable test runs.
describe('@utils/ens', () => {
jest.setTimeout(10000)
jest.retryTimes(2)

View File

@ -1,4 +1,4 @@
import { fetchData } from './fetch'
import { fetchData } from '../fetch'
const apiUrl = 'https://ens-proxy.oceanprotocol.com/api'

View File

@ -1,12 +1,8 @@
import { Asset } from '@oceanprotocol/lib'
// Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined'
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
const index = arr.indexOf(value)
if (index > -1) {
@ -14,3 +10,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
}
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

@ -1,4 +1,4 @@
import remark from 'remark'
import { remark } from 'remark'
import remarkHtml from 'remark-html'
import remarkGfm from 'remark-gfm'
@ -6,7 +6,7 @@ export function markdownToHtml(markdown: string): string {
const result = remark()
.use(remarkGfm)
.use(remarkHtml) // serializes through remark-rehype and rehype-stringify
.processSync(markdown).contents
.processSync(markdown)
return result.toString()
}

View File

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

View File

@ -1,5 +1,20 @@
import { formatCurrency } from '@coingecko/cryptoformat'
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
// http://mikemcl.github.io/decimal.js/#cmp
export function compareAsBN(balance: string, price: string): boolean {

View File

@ -3,12 +3,15 @@ import {
approve,
approveWei,
Datatoken,
Dispenser,
FixedRateExchange,
FreOrderParams,
LoggerInstance,
OrderParams,
ProviderComputeInitialize,
ProviderFees,
ProviderInstance
ProviderInstance,
ProviderInitialize
} from '@oceanprotocol/lib'
import Web3 from 'web3'
import { getOceanConfig } from './ocean'
@ -20,6 +23,26 @@ import {
} from '../../app.config'
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 asset
@ -40,15 +63,11 @@ export async function order(
const datatoken = new Datatoken(web3)
const config = getOceanConfig(asset.chainId)
const initializeData =
!providerFees &&
(await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
))
const initializeData = await initializeProvider(
asset,
accountId,
providerFees
)
const orderParams = {
consumer: computeConsumerAddress || accountId,
@ -66,22 +85,6 @@ export async function order(
switch (asset.accessDetails?.type) {
case 'fixed': {
// 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 = {
exchangeContract: config.fixedRateExchangeAddress,
@ -92,23 +95,96 @@ export async function order(
swapMarketFee: consumeMarketFixedSwapFee,
marketFeeAddress
} 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': {
const tx = await datatoken.buyFromDispenserAndOrder(
asset.services[0].datatokenAddress,
accountId,
orderParams,
config.dispenserAddress
)
return tx
if (asset.accessDetails.templateId === 1) {
const dispenser = new Dispenser(config.dispenserAddress, web3)
const dispenserTx = await dispenser.dispense(
asset.accessDetails?.datatoken.address,
accountId,
'1',
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
): Promise<TransactionReceipt> {
const datatoken = new Datatoken(web3)
const initializeData =
!providerFees &&
(await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
))
const initializeData = await initializeProvider(
asset,
accountId,
providerFees
)
const tx = await datatoken.reuseOrder(
asset.accessDetails.datatoken.address,

View File

@ -1,12 +1,15 @@
import {
Arweave,
ComputeAlgorithm,
ComputeAsset,
ComputeEnvironment,
downloadFileBrowser,
FileInfo,
Ipfs,
LoggerInstance,
ProviderComputeInitializeResults,
ProviderInstance
ProviderInstance,
UrlFile
} from '@oceanprotocol/lib'
import Web3 from 'web3'
import { getValidUntilTime } from './compute'
@ -82,12 +85,45 @@ export async function getFileDidInfo(
}
}
export async function getFileUrlInfo(
url: string,
providerUrl: string
export async function getFileInfo(
file: string,
providerUrl: string,
storageType: string
): Promise<FileInfo[]> {
try {
const response = await ProviderInstance.checkFileUrl(url, providerUrl)
let response
switch (storageType) {
case 'ipfs': {
const fileIPFS: Ipfs = {
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
}
}
return response
} catch (error) {
LoggerInstance.error(error.message)

View File

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

View File

@ -1,9 +0,0 @@
import { sanitizeUrl } from './url'
describe('@utils/url', () => {
test('sanitizeUrl', () => {
expect(sanitizeUrl('http://example.com')).toBe('http://example.com')
expect(sanitizeUrl('https://example.com')).toBe('https://example.com')
expect(sanitizeUrl('ftp://example.com')).toBe('about:blank')
})
})

View File

@ -0,0 +1,26 @@
import { sanitizeUrl, isGoogleUrl } from '.'
describe('@utils/url', () => {
test('sanitizeUrl', () => {
expect(sanitizeUrl('http://example.com')).toBe('http://example.com')
expect(sanitizeUrl('https://example.com')).toBe('https://example.com')
expect(sanitizeUrl('ftp://example.com')).toBe('about:blank')
})
})
describe('isGoogleUrl', () => {
it('should return true if the url is a google domain', () => {
expect(isGoogleUrl('https://google.com')).toBe(true)
expect(isGoogleUrl('https://drive.google.com')).toBe(true)
expect(isGoogleUrl('https://docs.google.com')).toBe(true)
expect(isGoogleUrl('https://sheets.google.com')).toBe(true)
expect(isGoogleUrl('https://meet.google.com')).toBe(true)
expect(isGoogleUrl('https://calendar.google.com')).toBe(true)
})
it('should return false if the url is not a google domain', () => {
expect(isGoogleUrl('https://google.test.com')).toBe(false)
expect(isGoogleUrl('https://drive.gloogle.com')).toBe(false)
expect(isGoogleUrl('https://drive.google.test.com')).toBe(false)
expect(isGoogleUrl('https://google.com.test.com')).toBe(false)
})
})

View File

@ -3,3 +3,10 @@ export function sanitizeUrl(url: string) {
const isAllowedUrlScheme = u.startsWith('http://') || u.startsWith('https://')
return isAllowedUrlScheme ? url : 'about:blank'
}
// check if the url is a google domain
export const isGoogleUrl = (url: string): boolean => {
if (!url) return
const googleUrl = new URL(url)
return googleUrl.hostname.endsWith('google.com')
}

View File

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

View File

@ -33,7 +33,7 @@ export async function addCustomNetwork(
const newNetworkData = {
chainId: `0x${network.chainId.toString(16)}`,
chainName: getNetworkDisplayName(network, network.chainId),
chainName: getNetworkDisplayName(network),
nativeCurrency: network.nativeCurrency,
rpcUrls: network.rpc,
blockExplorerUrls

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

@ -0,0 +1,58 @@
import { isCID } from '@utils/ipfs'
import isUrl from 'is-url-superb'
import * as Yup from 'yup'
import { isGoogleUrl } from './url/index'
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 (isGoogleUrl(value?.toString())) {
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

@ -1,24 +0,0 @@
.accountList {
display: grid;
grid-template-columns: 1fr;
gap: calc(var(--spacer) / 2);
}
@media screen and (min-width: 25rem) {
.accountList {
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: var(--spacer);
}
}
.empty {
color: var(--color-secondary);
font-size: var(--font-size-small);
font-style: italic;
}
.loaderWrap {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,29 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import AddToken, { AddTokenProps } from '@shared/AddToken'
export default {
title: 'Component/@shared/AddToken',
component: AddToken
} as ComponentMeta<typeof AddToken>
const Template: ComponentStory<typeof AddToken> = (args: AddTokenProps) => {
return <AddToken {...args} />
}
interface Props {
args: AddTokenProps
}
export const Default: Props = Template.bind({})
Default.args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN'
}
export const Minimal: Props = Template.bind({})
Minimal.args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN',
minimal: true
}

View File

@ -0,0 +1,24 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AddToken from './index'
jest.mock('../../../@utils/web3', () => ({ addTokenToWallet: jest.fn() }))
describe('@shared/AddToken', () => {
const propsBase = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN'
}
testRender(<AddToken {...propsBase} />)
it('renders with custom text', () => {
render(<AddToken {...propsBase} text="Hello Text" />)
expect(screen.getByText('Hello Text')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button'))
})
it('renders minimal', () => {
render(<AddToken {...propsBase} minimal />)
})
})

View File

@ -8,19 +8,21 @@ import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface AddTokenProps {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}
export default function AddToken({
address,
symbol,
text,
className,
minimal
}: {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}): ReactElement {
}: AddTokenProps): ReactElement {
const { web3Provider } = useWeb3()
const styleClasses = cx({

View File

@ -0,0 +1,65 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import AddAnnouncementBanner, {
AnnouncementBannerProps
} from '@shared/AnnouncementBanner'
export default {
title: 'Component/@shared/AnnouncementBanner',
component: AddAnnouncementBanner
} as ComponentMeta<typeof AddAnnouncementBanner>
const Template: ComponentStory<typeof AddAnnouncementBanner> = (
args: AnnouncementBannerProps
) => <AddAnnouncementBanner {...args} />
interface Props {
args: AnnouncementBannerProps
}
export const Default: Props = Template.bind({})
Default.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Success: Props = Template.bind({})
Success.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'success',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Warning: Props = Template.bind({})
Warning.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'warning',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Error: Props = Template.bind({})
Error.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'error',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}

View File

@ -0,0 +1,13 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AnnouncementBanner from './index'
describe('@shared/AnnouncementBanner', () => {
testRender(
<AnnouncementBanner
text="# Hello World!"
action={{ name: 'hello', handleAction: jest.fn() }}
state="success"
/>
)
})

View File

@ -12,17 +12,19 @@ export interface AnnouncementAction {
handleAction: () => void
}
export interface AnnouncementBannerProps {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}
export default function AnnouncementBanner({
text,
action,
state,
className
}: {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}): ReactElement {
}: AnnouncementBannerProps): ReactElement {
const styleClasses = cx({
banner: true,
error: state === 'error',

View File

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

View File

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

View File

@ -0,0 +1,35 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import MarketMetadataProvider from '@context/MarketMetadata'
import { UserPreferencesProvider } from '@context/UserPreferences'
import AssetList, { AssetListProps } from '.'
import { assets } from '../../../../.jest/__fixtures__/datasetsWithAccessDetails'
export default {
title: 'Component/@shared/AssetList',
component: AssetList
} as ComponentMeta<typeof AssetList>
const Template: ComponentStory<typeof AssetList> = (args: AssetListProps) => {
return (
<MarketMetadataProvider>
<UserPreferencesProvider>
<AssetList {...args} />
</UserPreferencesProvider>
</MarketMetadataProvider>
)
}
export const Default: { args: AssetListProps } = Template.bind({})
Default.args = {
assets,
showPagination: true,
page: 1,
totalPages: 10
}
export const Empty: { args: AssetListProps } = Template.bind({})
Empty.args = {
assets: [],
showPagination: false
}

View File

@ -0,0 +1,32 @@
import { render, screen, fireEvent } from '@testing-library/react'
import React from 'react'
import AssetList from './index'
import { datasetAquarius } from '../../../../.jest/__fixtures__/datasetAquarius'
describe('@shared/AssetList', () => {
it('renders without crashing', async () => {
const onPageChange = jest.fn()
render(
<AssetList
assets={[datasetAquarius]}
showPagination
page={1}
totalPages={10}
onPageChange={onPageChange}
/>
)
await screen.findAllByText('OCEAN')
fireEvent.click(screen.getByLabelText('Page 2'))
expect(onPageChange).toBeCalled()
})
it('renders empty', async () => {
render(<AssetList assets={[]} showPagination={false} isLoading={false} />)
await screen.findByText('No results found')
})
it('renders loading', async () => {
render(<AssetList assets={[]} showPagination={false} isLoading />)
})
})

View File

@ -2,15 +2,11 @@ import AssetTeaser from '@shared/AssetTeaser'
import React, { ReactElement, useEffect, useState } from 'react'
import Pagination from '@shared/Pagination'
import styles from './index.module.css'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import { useIsMounted } from '@hooks/useIsMounted'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { useWeb3 } from '@context/Web3'
const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
@ -19,7 +15,7 @@ function LoaderArea() {
)
}
declare type AssetListProps = {
export declare type AssetListProps = {
assets: AssetExtended[]
showPagination: boolean
page?: number
@ -28,6 +24,8 @@ declare type AssetListProps = {
onPageChange?: React.Dispatch<React.SetStateAction<number>>
className?: string
noPublisher?: boolean
noDescription?: boolean
noPrice?: boolean
}
export default function AssetList({
@ -38,16 +36,18 @@ export default function AssetList({
isLoading,
onPageChange,
className,
noPublisher
noPublisher,
noDescription,
noPrice
}: AssetListProps): ReactElement {
const { chainIds } = useUserPreferences()
const { accountId } = useWeb3()
const [assetsWithPrices, setAssetsWithPrices] = useState<AssetExtended[]>()
const [assetsWithPrices, setAssetsWithPrices] =
useState<AssetExtended[]>(assets)
const [loading, setLoading] = useState<boolean>(isLoading)
const isMounted = useIsMounted()
useEffect(() => {
if (!assets) return
if (!assets || !assets.length) return
setAssetsWithPrices(assets as AssetExtended[])
setLoading(false)
@ -67,24 +67,21 @@ export default function AssetList({
onPageChange(selected + 1)
}
const styleClasses = cx({
assetList: true,
[className]: className
})
const styleClasses = `${styles.assetList} ${className || ''}`
return chainIds.length === 0 ? (
<div className={styleClasses}>
<div className={styles.empty}>No network selected</div>
</div>
) : assetsWithPrices && !loading ? (
return loading ? (
<LoaderArea />
) : (
<>
<div className={styleClasses}>
{assetsWithPrices.length > 0 ? (
assetsWithPrices.map((assetWithPrice) => (
{assetsWithPrices?.length > 0 ? (
assetsWithPrices?.map((assetWithPrice) => (
<AssetTeaser
asset={assetWithPrice}
key={assetWithPrice.id}
noPublisher={noPublisher}
noDescription={noDescription}
noPrice={noPrice}
/>
))
) : (
@ -100,7 +97,5 @@ export default function AssetList({
/>
)}
</>
) : (
<LoaderArea />
)
}

View File

@ -0,0 +1,24 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AssetListTitle from '.'
import { render } from '@testing-library/react'
jest.mock('../../../@utils/aquarius', () => ({
getAssetsNames: () => Promise.resolve('Test')
}))
describe('AssetListTitle', () => {
testRender(
<AssetListTitle asset={{ metadata: { name: 'Hello world' } } as any} />
)
it('renders with passed title', () => {
render(<AssetListTitle title="Hello Title" />)
})
it('renders with passed DID', () => {
render(
<AssetListTitle did="did:op:764b81877039fa2651b919fc91c399799acb837f270e6d17bfb7973fbe6e9408" />
)
})
})

View File

@ -1,7 +1,7 @@
import Link from 'next/link'
import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '@utils/aquarius'
import styles from './AssetListTitle.module.css'
import styles from './index.module.css'
import axios from 'axios'
import { Asset } from '@oceanprotocol/lib'
import { useMarketMetadata } from '@context/MarketMetadata'
@ -41,9 +41,7 @@ export default function AssetListTitle({
return (
<h3 className={styles.title}>
<Link href={`/asset/${did || asset?.id}`}>
<a>{assetTitle}</a>
</Link>
<Link href={`/asset/${did || asset?.id}`}>{assetTitle}</Link>
</h3>
)
}

View File

@ -21,7 +21,7 @@
}
.detailLine {
margin-bottom: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 4);
}
.content {
@ -43,8 +43,12 @@
overflow-wrap: break-word;
}
.price {
margin-top: calc(var(--spacer) / 12);
}
.footer {
margin-top: calc(var(--spacer) / 4);
margin-top: calc(var(--spacer) / 24);
}
.typeLabel {

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AssetTeaser from './index'
import { asset } from '../../../../.jest/__fixtures__/datasetWithAccessDetails'
describe('@shared/AssetTeaser', () => {
testRender(<AssetTeaser asset={asset} />)
})

View File

@ -8,17 +8,21 @@ import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName'
import styles from './index.module.css'
import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences'
import { formatNumber } from '@utils/numbers'
declare type AssetTeaserProps = {
export declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
noDescription?: boolean
noPrice?: boolean
}
export default function AssetTeaser({
asset,
noPublisher
noPublisher,
noDescription,
noPrice
}: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata
const { datatokens } = asset
@ -31,55 +35,77 @@ export default function AssetTeaser({
return (
<article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}>
<a className={styles.link}>
<aside className={styles.detailLine}>
<AssetType
className={styles.typeLabel}
type={type}
accessType={accessType}
/>
<span className={styles.typeLabel}>
{datatokens[0]?.symbol.substring(0, 9)}
</span>
<NetworkName
networkId={asset.chainId}
className={styles.typeLabel}
/>
</aside>
<header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />}
</header>
<Link href={`/asset/${asset.id}`} className={styles.link}>
<aside className={styles.detailLine}>
<AssetType
className={styles.typeLabel}
type={type}
accessType={accessType}
/>
<span className={styles.typeLabel}>
{datatokens[0]?.symbol.substring(0, 9)}
</span>
<NetworkName networkId={asset.chainId} className={styles.typeLabel} />
</aside>
<header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />}
</header>
{!noDescription && (
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>
{isUnsupportedPricing ? (
<strong>No pricing schema available</strong>
) : (
<Price accessDetails={asset.accessDetails} size="small" />
)}
<footer className={styles.footer}>
{allocated && allocated > 0 ? (
<span className={styles.typeLabel}>
{allocated < 0
? ''
: `${formatPrice(allocated, locale)} veOCEAN`}
</span>
) : null}
{orders && orders > 0 ? (
<span className={styles.typeLabel}>
{orders < 0
? 'N/A'
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
</span>
) : null}
</footer>
</a>
)}
{!noPrice && (
<div className={styles.price}>
{isUnsupportedPricing || !asset.services.length ? (
<strong>No pricing schema available</strong>
) : (
<Price accessDetails={asset.accessDetails} size="small" />
)}
</div>
)}
<footer className={styles.footer}>
{allocated && allocated > 0 ? (
<span className={styles.typeLabel}>
{allocated < 0 ? (
''
) : (
<>
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
veOCEAN
</>
)}
</span>
) : null}
{orders && orders > 0 ? (
<span className={styles.typeLabel}>
{orders < 0 ? (
'N/A'
) : (
<>
<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>
) : null}
</footer>
</Link>
</article>
)

View File

@ -1,202 +0,0 @@
import React, { FormEvent, ReactElement } from 'react'
import Button from '../atoms/Button'
import styles from './index.module.css'
import Loader from '../atoms/Loader'
interface ButtonBuyProps {
action: 'download' | 'compute'
disabled: boolean
hasPreviousOrder: boolean
hasDatatoken: boolean
btSymbol: string
dtSymbol: string
dtBalance: string
assetType: string
assetTimeout: string
isConsumable: boolean
consumableFeedback: string
hasPreviousOrderSelectedComputeAsset?: boolean
hasDatatokenSelectedComputeAsset?: boolean
dtSymbolSelectedComputeAsset?: string
dtBalanceSelectedComputeAsset?: string
selectedComputeAssetType?: string
isBalanceSufficient: boolean
isLoading?: boolean
onClick?: (e: FormEvent<HTMLButtonElement>) => void
stepText?: string
type?: 'submit'
priceType?: string
algorithmPriceType?: string
isAlgorithmConsumable?: boolean
hasProviderFee?: boolean
}
// TODO: we need to take a look at these messages
function getConsumeHelpText(
btSymbol: string,
dtBalance: string,
dtSymbol: string,
hasDatatoken: boolean,
hasPreviousOrder: boolean,
assetType: string,
isConsumable: boolean,
isBalanceSufficient: boolean,
consumableFeedback: string
) {
const text =
isConsumable === false
? consumableFeedback
: hasPreviousOrder
? `You bought this ${assetType} already allowing you to use it without paying again.`
: hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this dataset by spending 1 ${dtSymbol}, but without paying ${btSymbol} again.`
: isBalanceSufficient === false
? `You do not have enough ${btSymbol} in your wallet to purchase this asset.`
: `For using this ${assetType}, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher.`
return text
}
function getComputeAssetHelpText(
hasPreviousOrder: boolean,
hasDatatoken: boolean,
btSymbol: string,
dtSymbol: string,
dtBalance: string,
isConsumable: boolean,
consumableFeedback: string,
isBalanceSufficient: boolean,
hasPreviousOrderSelectedComputeAsset?: boolean,
hasDatatokenSelectedComputeAsset?: boolean,
assetType?: string,
dtSymbolSelectedComputeAsset?: string,
dtBalanceSelectedComputeAsset?: string,
selectedComputeAssetType?: string,
isAlgorithmConsumable?: boolean,
hasProviderFee?: boolean
) {
const computeAssetHelpText = getConsumeHelpText(
btSymbol,
dtBalance,
dtSymbol,
hasDatatoken,
hasPreviousOrder,
assetType,
isConsumable,
isBalanceSufficient,
consumableFeedback
)
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
? 'In order to start the job you also need to pay the fees for renting the c2d resources.'
: 'C2D resources required to start the job are available, no payment required for those fees.'
const computeHelpText = `${computeAssetHelpText} ${computeAlgoHelpText} ${providerFeeHelpText}`
return computeHelpText
}
export default function ButtonBuy({
action,
disabled,
hasPreviousOrder,
hasDatatoken,
btSymbol,
dtSymbol,
dtBalance,
assetType,
assetTimeout,
isConsumable,
consumableFeedback,
isBalanceSufficient,
hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset,
dtSymbolSelectedComputeAsset,
dtBalanceSelectedComputeAsset,
selectedComputeAssetType,
onClick,
stepText,
isLoading,
type,
priceType,
algorithmPriceType,
isAlgorithmConsumable,
hasProviderFee
}: ButtonBuyProps): ReactElement {
const buttonText =
action === 'download'
? hasPreviousOrder
? 'Download'
: priceType === 'free'
? 'Get'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
: hasPreviousOrder &&
hasPreviousOrderSelectedComputeAsset &&
!hasProviderFee
? 'Start Compute Job'
: priceType === 'free' && algorithmPriceType === 'free'
? 'Order Compute Job'
: `Buy Compute Job`
return (
<div className={styles.actions}>
{isLoading ? (
<Loader message={stepText} />
) : (
<>
<Button
style="primary"
type={type}
onClick={onClick}
disabled={disabled}
className={action === 'compute' ? styles.actionsCenter : ''}
>
{buttonText}
</Button>
<div className={styles.help}>
{action === 'download'
? getConsumeHelpText(
btSymbol,
dtBalance,
dtSymbol,
hasDatatoken,
hasPreviousOrder,
assetType,
isConsumable,
isBalanceSufficient,
consumableFeedback
)
: getComputeAssetHelpText(
hasPreviousOrder,
hasDatatoken,
btSymbol,
dtSymbol,
dtBalance,
isConsumable,
consumableFeedback,
isBalanceSufficient,
hasPreviousOrderSelectedComputeAsset,
hasDatatokenSelectedComputeAsset,
assetType,
dtSymbolSelectedComputeAsset,
dtBalanceSelectedComputeAsset,
selectedComputeAssetType,
isAlgorithmConsumable,
hasProviderFee
)}
</div>
</>
)}
</div>
)
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import DebugOutput from './index'
describe('@shared/DebugOutput', () => {
testRender(<DebugOutput title="Debug" output="Hello Output" />)
})

View File

@ -0,0 +1,21 @@
import testRender from '../../../../.jest/testRender'
import { render, screen } from '@testing-library/react'
import React from 'react'
import ExplorerLink from './index'
describe('@shared/ExplorerLink', () => {
testRender(
<ExplorerLink networkId={1} path="/tx">
Hello Link
</ExplorerLink>
)
it('renders without networkId', () => {
render(
<ExplorerLink networkId={null} path="/tx">
Hello Link
</ExplorerLink>
)
expect(screen.getByRole('link')).toHaveTextContent('Hello Link')
})
})

View File

@ -1,12 +1,9 @@
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import External from '@images/external.svg'
import classNames from 'classnames/bind'
import { Config } from '@oceanprotocol/lib'
import styles from './index.module.css'
import { getOceanConfig } from '@utils/ocean'
const cx = classNames.bind(styles)
export default function ExplorerLink({
networkId,
path,
@ -20,10 +17,6 @@ export default function ExplorerLink({
}): ReactElement {
const [url, setUrl] = useState<string>()
const [oceanConfig, setOceanConfig] = useState<Config>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => {
if (!networkId) return
@ -39,7 +32,7 @@ export default function ExplorerLink({
title={`View on ${oceanConfig?.explorerUri}`}
target="_blank"
rel="noreferrer"
className={styleClasses}
className={`${styles.link} ${className || ''}`}
>
{children} <External />
</a>

View File

@ -0,0 +1,37 @@
import testRender from '../../../../.jest/testRender'
import { FileInfo } from '@oceanprotocol/lib'
import { render } from '@testing-library/react'
import React from 'react'
import FileIcon from './index'
describe('@shared/FileIcon', () => {
const file: FileInfo = {
type: 'url',
contentType: 'text/plain',
contentLength: '123'
}
testRender(<FileIcon file={file} />)
it('renders small', () => {
render(<FileIcon file={file} small />)
})
it('renders loading', () => {
render(<FileIcon file={file} isLoading />)
})
it('renders empty', () => {
const file: FileInfo = { type: 'url' }
render(<FileIcon file={file} />)
})
it('renders with 0 contentLength', () => {
const file: FileInfo = {
type: 'url',
contentType: 'text/plain',
contentLength: '0'
}
render(<FileIcon file={file} />)
})
})

View File

@ -30,9 +30,9 @@ export default function FileIcon({
return (
<ul className={styleClasses}>
{!isLoading && file ? (
{!isLoading ? (
<>
{file.contentType || file.contentLength ? (
{file?.contentType || file?.contentLength ? (
<>
<li>{cleanupContentType(file.contentType)}</li>
<li>
@ -40,6 +40,9 @@ export default function FileIcon({
? filesize(Number(file.contentLength)).toString()
: ''}
</li>
<li>
{file.type === 'smartcontract' ? 'smart\ncontract' : file.type}
</li>
</>
) : (
<li className={styles.empty}>No file info available</li>

View File

@ -1,17 +0,0 @@
export function prettySize(
bytes: number,
separator = ' ',
postFix = ''
): string {
if (bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
sizes.length - 1
)
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)}${separator}${
sizes[i]
}${postFix}`
}
return 'n/a'
}

View File

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Error from './Error'
describe('@shared/FormInput/Error', () => {
const propsBase = {
value: '',
touched: false,
initialTouched: false
}
it('renders without crashing', () => {
render(<Error meta={{ ...propsBase, error: 'Hello Error' }} />)
expect(screen.getByText('Hello Error')).toBeInTheDocument()
})
it('renders nothing without error passed', () => {
render(<Error meta={{ ...propsBase }} />)
expect(screen.queryByText('Hello Error')).not.toBeInTheDocument()
})
})

View File

@ -1,9 +1,6 @@
import React, { ReactElement } from 'react'
import styles from './Help.module.css'
import Markdown from '@shared/Markdown'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const FormHelp = ({
children,
@ -12,12 +9,9 @@ const FormHelp = ({
children: string
className?: string
}): ReactElement => {
const styleClasses = cx({
help: true,
[className]: className
})
return <Markdown className={styleClasses} text={children} />
return (
<Markdown className={`${styles.help} ${className || ''}`} text={children} />
)
}
export default FormHelp

View File

@ -58,11 +58,11 @@
}
.radio {
composes: radio from '@shared/FormInput/InputRadio.module.css';
composes: radio from '@shared/FormInput/InputElement/Radio/index.module.css';
}
.checkbox {
composes: checkbox from '@shared/FormInput/InputRadio.module.css';
composes: checkbox from '@shared/FormInput/InputElement/Radio/index.module.css';
}
.title {

View File

@ -0,0 +1,52 @@
import AssetSelection, { AssetSelectionAsset } from './'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
describe('@shared/FormInput/InputElement/AssetSelection', () => {
const assets: AssetSelectionAsset[] = [
{
did: 'did:op:xxx',
name: 'Asset',
price: '10',
checked: false,
symbol: 'OCEAN'
},
{
did: 'did:op:yyy',
name: 'Asset',
price: '10',
checked: true,
symbol: 'OCEAN'
},
{
did: 'did:op:zzz',
name: 'Asset',
price: '0',
checked: false,
symbol: 'OCEAN'
}
]
it('renders without crashing', () => {
render(<AssetSelection assets={assets} />)
const searchInput = screen.getByPlaceholderText(
'Search by title, datatoken, or DID...'
)
fireEvent.change(searchInput, { target: { value: 'Assets' } })
fireEvent.change(searchInput, { target: { value: '' } })
})
it('renders empty assetSelection', () => {
render(<AssetSelection assets={[]} />)
expect(screen.getByText('No assets found.')).toBeInTheDocument()
})
it('renders disabled assetSelection', () => {
render(<AssetSelection assets={[]} disabled />)
expect(screen.getByText('No assets found.')).toBeInTheDocument()
})
it('renders assetSelectionMultiple', () => {
render(<AssetSelection assets={assets} multiple />)
})
})

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